• Node.js can HTTP/2 push!
  • Node.js Foundation
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Raoul1996
  • Proofread by: Starriers, FateZeros, LeviDing

Node.js can do HTTP/2 push!

This article was co-written by Matteo Collina, lead architect at @Nearform and a member of node.js technical Steering Committee, and Google software engineer Jinwoo Lee.

Since the introduction of HTTP/2 in Node.js in July 2017, the practice has gone through several rounds of refinement. Now we’re almost ready to remove the “experimental” sign. Of course, it’s best to try HTTP/2 support with Node.js version 9, which has the latest fixes and improvements.

The easiest way to get started is to use the compatibility layer provided in the new Http2 core module section:

const http2 = require('http2');
const options = {
 key: getKeySomehow(),
 cert: getCertSomehow()
};

// You must use HTTPS
// Otherwise, the browser cannot connect
const server = http2.createSecureServer(options, (req, res) => {
 res.end('Hello World! ');
});
server.listen(3000);
Copy the code

The compatibility layer provides the same high-level API as REQUIRE (‘ HTTP ‘) (with the same request listeners for request and response objects), allowing for a smooth migration to HTTP/2.

The compatibility layer also provides an easy upgrade path for Web framework authors, and so far both Restify and Fastify have implemented HTTP/2 support based on the Node.js HTTP/2 compatibility layer.

Fastify is a new Web framework that focuses on performance without sacrificing developer productivity or abandoning the rich plug-in ecosystem that was recently upgraded to version 1.0.0.

Using HTTP/2 in Fastify is simple:

const Fastify = require('fastify');

// You must use HTTPS
// Otherwise, the browser cannot connect
const fastify = Fastify({
 http2: true.// The comma is missing
 https: {
   key: getKeySomehow(),
   cert: getCertSomehow()
 }
});

fastify.get('/fastify'.async (request, reply) => {
 return 'Hello World! ';
});

server.listen(3000);
Copy the code

Although being able to run the same application code over HTTP/1.1 and HTTP/2 is important for your choice of protocol, the separate compatibility layer does not provide some of the more powerful features supported by HTTP/2. The http2 core module can use these additional features with the new core API (Http2Stream) via the “stream” listener:

const http2 = require('http2');
const options = {
 key: getKeySomehow(),
 cert: getCertSomehow()
};

// You must use HTTPS
// Otherwise, the browser cannot connect
const server = http2.createSecureServer(options);
server.on('stream', (stream, headers) => {
 // The stream is duplex
 // Headers is an object that contains the request header

 // The response sends headers to the client
 // meta headers starts with a colon (:)
 stream.respond({ ':status': 200 });

 / / this is stream. RespondWithFile ()
 / / and stream. PushStream ()

 stream.end('Hello World! ');
});

server.listen(3000);
Copy the code

In Fastify, Http2Stream can be accessed through the request.raw.stream API as follows:

fastify.get('/fastify'.async (request, reply) => {
 request.raw.stream.pushStream({
  ':path': '/a/resource'
 }, function (err, stream) {
  if (err) {
    request.log.warn(err);
    return
  }
  stream.respond({ ':status': 200 });
  stream.end('content');
 });

 return 'Hello World! ';
});
Copy the code

HTTP/2 Push — Opportunities and Challenges

HTTP/2 offers considerable performance improvements over HTTP/1, with server push being one of the big results.

A typical (or simplified) HTTP request and response flow would look like this (the screenshot below is a connection to Hack News) :

  1. The browser requests an HTML document.
  2. The server processes the request and generates and sends back HTML documents.
  3. The browser receives the response and parses the HTML document.
  4. The browser sends more requests for more resources, such as stylesheets, images, JavaScript files, etc. needed during the rendering of the HTML document.
  5. The server responds to requests for each resource.
  6. Browsers use HTML documents and related resources to render pages.

This means that rendering an HTML document typically requires multiple requests and responses because the browser needs additional resources associated with it to render the document properly. It would be nice if these related resources could be sent to the browser along with the original HTML document without a browser request. This is the purpose of HTTP/2 server push.

In HTTP/2, the server can proactively push additional resources that it thinks the browser will request later along with the original request response. If the browser really needs these extra resources later on, it will simply use the resources it has pushed instead of sending additional requests. For example, suppose the server is sending this /index.html file


      
<html>
<head>
  <title>Awesome Unicorn!</title>
  <link rel="stylesheet" type="text/css" href="/static/awesome.css">
</head>
<body>
  This is an awesome Unicorn! <img src="/static/unicorn.png">
</body>
</html>
Copy the code

The server will respond to the request by sending back this file. But it knows that /index.html needs /static/awesome. CSS and /static/unicorn.png to render properly. Therefore, the server pushes these files along with /index.html

for (const asset of ['/static/awesome.css'.'/static/unicorn.png']) {
  // Stream is ServerHttp2Stream.
  stream.pushStream({':path': asset}, (err, pushStream) => {
    if (err) throw err;
    pushStream.respondWithFile(asset);
  });
}
Copy the code

On the client side, as soon as the browser parses /index.html, it will indicate the need for /static/awesome. CSS and /static/unicorn.png, but the browser knows that they have been pushed and stored in the cache! So instead of sending two additional requests, he uses the resource that has already been pushed.

That sounds good. But there are some challenges. First, it is not easy for the server to know which additional resources to push for the original request. While this decision can be relegated to the application layer, it is not easy for developers to make the decision either. One way is to manually parse the HTML to find the list of resources it needs. But as the application iterates and the HTML files are updated, maintaining the list becomes tedious and error-prone.

Another challenge comes from the browser’s internal caching of previously retrieved resources. Using the example above, if the browser loaded /index.html yesterday, it will also load /static/unicorn.png, and this file will usually be cached in the browser. When the browser loads /index.html and then tries to load /static/unicorn.png, it knows that the latter is already cached and will only use it instead of requesting it again. In this case, if the server pushes /static/unicorn.png, it will waste bandwidth. So the server should have some way to determine if the resource is cached in the browser.

There are other types of challenges, rules of thumb for HTTP/2 push documents, and so on.

HTTP/2 automatic push

To make it easier for Node.js developers to support server push, Google has released an NPM package for automation: H2-auto-push. It is designed to address many of the challenges mentioned above and in the rules of thumb for HTTP/2 push documents.

It monitors the pattern of requests from the browser and identifies additional resources associated with the originally requested resource. If the original resource is then requested, the relevant resource is automatically pushed to the browser. It will also estimate whether the browser may have cached a resource and skip the push if it has.

H2-auto-push is designed as middleware for use by various Web frameworks. As a static file service middleware, it is very easy to develop an automatic push middleware using this NPM package. See, for example, fastify-auto-push. This is a fastify plug-in that supports HTTP/2 auto-push and uses the H2-auto-push package.

It is also very easy to use this middleware in your applications

const fastify = require('fastify');
const fastifyAutoPush = require('fastify-auto-push');
const fs = require('fs');
const path = require('path');
const {promisify} = require('util');

const fsReadFile = promisify(fs.readFile);

const STATIC_DIR = path.join(__dirname, 'static');
const CERTS_DIR = path.join(__dirname, 'certs');
const PORT = 8080;

async function createServerOptions() {
  const readCertFile = (filename) = > {
    return fsReadFile(path.join(CERTS_DIR, filename));
  };
  const [key, cert] = await Promise.all(
      [readCertFile('server.key'), readCertFile('server.crt')]);
  return {key, cert};
}

async function main() {
  const {key, cert} = await createServerOptions();
  // Browsers only support HTTPS using HTTP/2.
  const app = fastify({https: {key, cert}, http2: true});

  // Create and register the automatic push plugin
  // It should be registered at the beginning of the middleware chain.
  app.register(fastifyAutoPush.staticServe, {root: STATIC_DIR});

  await app.listen(PORT);
  console.log(`Listening on port ${PORT}`);
}

main().catch((err) = > {
  console.error(err);
});
Copy the code

Simple, right?

Our tests show that H2-auto-push is 12% better than HTTP/2 and approximately 135% better than HTTP/1. We hope this article gave you a better understanding of HTTP2 and the benefits it can bring to your application, including HTTP2 push.

Special thanks to nearForm’s James Snell and David Mark Clements and Google’s Ali Sheikh and Kelvin Jin for helping to edit this blog post. Many thanks to Matt Loring at Google for his initial efforts with automatic push.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.