Node.js can HTTP/2 push!
Since introducing HTTP/2 into Node.js 8 in July of 2017, the implementation has undergone several rounds of improvements. Now we’re almost ready to lift the “experimental” flag. It’s best to try out HTTP/2 support with Node.js version 9, which has all the latest fixes and improvements.
The easiest way to get started is by using the compatibility layer provided as part of the new http2 core module:
The compatibility layer provides the same high-level API (a request listener with the familiar request and response objects) that require(‘http’) provides, which allows for a smooth initial migration path to HTTP/2.
Using HTTP/2 with fastify is straightforward:
While being able to run the same application code on top of both HTTP/1.1 and HTTP/2 is important for protocol adoption, the compatibility layer alone does not expose some of the more powerful capabilities available with HTTP/2. The core http2 module exposes these additional capabilities through a new core API (Http2Stream) which can be accessed via a “stream” listener:
In Fastify, the Http2Stream can be accessed via the request.raw.stream API, like so:
HTTP/2 Push — Opportunities and Challenges
HTTP/2 gives huge performance improvements over HTTP/1 in many ways, and server push is one of its features for performance.
A typical (and simplified) HTTP request/response flow is like this (The screenshot below is for connecting to Hacker News):
- The browser requests an HTML document.
- The server processes the request and generates/sends the HTML document.
- The browser receives the response and parses the HTML document.
- The server responds to each request with the corresponding resource.
- The browser renders the page using the HTML document and associated resources.
This means that there usually are multiple round-trips of requests/responses to render one HTML document because there are additional resources that are associated with it, and the browser needs them to render the document correctly. It would be great if all those associated resources could be sent to the browser together with the original HTML document without the browser requesting them. And that is what HTTP/2 server push is for.
In HTTP/2, the server can proactively push additional resources together with the response to the original request that it thinks the browser will request later. Later, if the browser really needs them, it just uses the already-pushed resources instead of sending additional requests for them.
For example, let’s suppose this /index.html file is being served
The server will respond by sending that file. But it knows that /index.html needs /static/awesome.css and /static/unicorn.png for it to be rendered correctly. So the server pushes those files together with /index.html
On the client side, once the browser parses /index.html, it figures that /static/awesome.css and /static/unicorn.png are needed, but it also figures that they have already been pushed and stored in the browser cache! So it doesn’t have to send two additional requests but uses the already-pushed resources instead.
This sounds good so far. But there are some challenges. First, it is not simple for a server to know which additional resources can be pushed for an original request. We can move that decision up to the application layer and have the developer decide. But it is not simple either for a developer to figure that out. One way to do so is to manually parse the HTML and figure out a list of additional resources that are needed, but it is tedious and error-prone to maintain that list as the application changes and the HTML files are updated.
Another challenge comes from the fact that browsers internally cache resources that have been previously retrieved. Using the example files above, if the browser loaded /index.html yesterday, it would have also loaded /static/unicorn.png and the file is usually cached in the browser. When the browser loads /index.html and in turn tries to load /static/unicorn.png, it knows that the latter is already cached and just uses it instead of requesting it again. In this case, it’ll be a waste of the network bandwidth if the server pushes /static/unicorn.png. The server should have some way to tell whether a resource is already cached in the browser.
There are other kinds of challenges as well, and Rules of Thumb for HTTP/2 Push documents well those challenges.
To make it easy for Node.js developers to support the server push feature, Google published an npm package for automating it: h2-auto-push. Its design goal is to deal with many challenges that are mentioned in the section above and in the Rules of Thumb for HTTP/2 Push document.
It monitors the patterns of requests coming from browsers and figures out what additional resources are associated with the originally requested resource. And later if that original resource is requested, the associated resources are automatically pushed to the browser. It also estimates whether the browser is likely to have a certain resource already cached, and skips pushing if it so determines.
h2-auto-push was designed to be used by middlewares for various web frameworks. The middlewares are assumed to be a static-file-serving middleware, and it is pretty easy to develop an auto-push middleware using this NPM package. For example, see fastify-auto-push. It is a fastify plugin for supporting HTTP/2 auto-push and uses the h2-auto-push package.
Using this middleware from an application is pretty easy too
Pretty easy, huh?
Our performance test shows that h2-auto-push gives ~12% performance improvement over HTTP/2 without push and ~135% improvement over HTTP/1. We hope that this article gives you a better understanding of HTTP2 and the benefits that it can bring to your application, including HTTP2 push.
A special thank you to James Snell and David Mark Clements of nearForm, and Ali Sheikh and Kelvin Jin of Google for helping edit this blog post. And a big thank you to Matt Loring of Google for his initial work on auto-push.