Better streaming layouts for front-end microservices with Tailor
Learn a straightforward approach to front-end microservices with Tailor, a library that provides a middleware which you can integrate into any Node.js server.
Microservices get a lot of traction these days. They allow multiple teams to work independently from each other, choose their own technology stacks and establish their own release cycles.
Unfortunately, frontend development hasn’t fully capitalized yet on the benefits that microservices offer. The common practice for building websites remains “the monolith”: a single frontend codebase that consumes multiple APIs.
What if we could have microservices on the frontend? This would allow frontend developers to work together with their backend counterparts on the same feature and deploy parts of the website—“fragments” such as Header, Product, and Footer—independently. Bringing microservices to the frontend requires a layout service that composes a website out of fragments. It should also preserve the common requirements of most websites:
- Compose pre-rendered markup on the backend: This is important for SEO and fastens the initial render.
- Ensure a fast Time to First Byte: Request fragments in parallel and stream them as soon as possible, without blocking the rest of the page.
- Enforce performance budget: This is quite challenging, because there is no single point where you can control performance.
- Fault Tolerance: Render the meaningful output, even if a page fragment has failed or timed out.
The most straightforward approach would be to call each fragment, concatenate the results, and respond to the browser. However, this wouldn’t work in practice: The entire page would be blocked until all the fragments responded. In response to this dilemma, our team applied streams—allowing us to deliver a part of the page to the browser without having to wait for all fragments. You can read more about the benefits of using streams from Jake Archibald’s article.
Our prior exposure to streams, particularly in NodeJS, came from using GulpJS and creating custom plugins for it. Gulp is a fast-streaming build tool, because it allows to transform the files in a stream without writing any intermediate results to the file system.
We implemented two prototypes: Scala with Akka HTTP, and Node.js with streams from the core library. The latter, initially called “Streaming Layout Service,” performed faster, so we chose to keep it—renaming it Tailor and open-sourcing it on GitHub.
How the layout service works
In order to achieve a fast Time to First Byte, the service has to asynchronously fetch multiple fragments, assemble their response streams, and output the final output stream.
The request first goes to a router, matches the public URL with a template path, and calls the layout service. The layout service then:
- Fetches the template, based on the path
- Parses the template for fragment placeholders
- Asynchronously calls all fragments from the template
- Assembles multiple fragments streams into a single output stream
- Sets response headers based on the primary fragment and streams the output
Tailor is partially inspired by Facebook’s BigPipe, but BigPipe’s SEO limitations make it an impractical choice for an e-commerce platform. Even after reading multiple posts about the cleverness of modern search engines and their ability to execute JavaScript, we can’t be sure if rendering on the frontend affects the pagerank. Secondly, it’s not possible to retrospectively change the response code of the page from JavaScript, to e.g. prevent an error from being indexed, because the headers are flushed before the content. We developed Tailor to circumvent this limitation by having it identify and mark a primary fragment that becomes responsible for the status code of the page.
An example template looks like this:
Tailor takes advantage of Node.js’s event-driven, non-blocking I/O capabilities. It is built on top of streams from Node’s core library, and also implements some custom streams:
- A parser stream that internally uses sax parser to group together static chunks of HTML and parse the fragment placeholders;
- A stringifier stream that assembles static chunks of HTML and the response streams of the fragments;
- An async stream that is used to postpone the fragments that are marked “async”.
Not all the fragments are blocking in nature. The key to ensure faster Time to First Byte is to prioritize the fragments above the fold and flush them as soon as possible. It’s a good practice to mark the fragments below the fold as async. This means that we output only a placeholder and defer the fragment output to the end of the page, where it comes with inline JavaScript that moves the content into the placeholder. We achieve this behavior using Async Stream.
Besides the streaming on the backend, Tailor is also responsible for initializing fragments on the frontend. Each fragment exposes its static bundles (CSS and JavaScript) through an HTTP Link header. A CSS from the fragment becomes a <link>
tag that is output before the fragment to prevent the flash of un-styled content. For an async fragment we use loadCSS to not block the page rendering. All JavaScript bundles use AMD and expose a special “init” function that is called with the fragment’s DOM element.
How to use Tailor
Tailor is a library that provides a middleware which you can integrate into any Node.js server.
It is available from npm under the name node-tailor. The smallest setup should include the “templates” directory with HTML templates and an “index.js” file with the following content:
const http = require('http');
const Tailor = require('node-tailor');
const tailor = new Tailor({/* Options */});
const server = http.createServer(tailor.requestHandler);
server.listen(process.env.PORT || 8080);
After the server has started, you can open the url with the name of the template file in the browser, e.g. “http://localhost:8080/example-template”. You can also run the existing example that includes the minimum amount of code for the sample fragment.
Tailor is very flexible because it comes with a set of options that allows you to adjust it to your custom requirements. For example, the “fetchTemplate” option allows to integrate a custom logic to fetch the template from an S3 bucket, instead of the file system. Another option, “handleTag”, makes it possible to implement serialization of any HTML tag from the “handledTags” list. This may be helpful when you want to set an HTML “lang” attribute based on the Accept-Language header. The full list of options is available in the README.
Future plans
Tailor is still fresh, and we are actively working on it. We are currently experimenting with the way fragments are initialized on the frontend. Instead of enclosing fragment content in <div id=”fragment-id”>
, we surround it with script tags that find themselves in the DOM and identify the start and end of the content. This saves us from id collisions and allows to place fragments anywhere on the page, even in the <head>
of the document.
Tailor was built by an engineering team at Zalando—Europe’s largest online fashion platform, serving more than 17 million users in 15 markets, Zalando is currently migrating our customer-facing website from a monolith to a microservices architecture. Tailor is now powering our new shop rebuild project, and we’ve successfully tried it live with a small number of real customers. If you have an idea for Tailor or want to ask about the project internals, feel free to open an issue.