Optimising Node.js and React with Compression

pikkue

Eero Kukko

Posted on December 7, 2022

Optimising Node.js and React with Compression

If you have a large web page/app, you're already familiar with this issue. Below you'll find few ways to compress data and decrease the page load time with Node.

Background

I have a single page React web application with a large amount of files, imports, css, fonts etc. Before the page is served, all the assets are compiled & bundled with Webpack. In this case, most of the assets (css, html and js) are bundled into a single html file to decrease the amount of network requests.

If you are sending a large amount of files over HTTP/HTTPS/HTTP/2, there is some overhead from the all the requests that the client needs to make and process. Every request has headers, checksums, authentication and authorisation fields that someone needs to transfer and process.

In the examples below, the backend is based on Node.js/Fastify, but the same tricks work with Express and on other Node frameworks.

Loading Time...

In the development environment the connection is fast. I didn't even notice that the file had grown to 14,9 MB. Despite the many database requests in the backend, the page load takes less than a second.

Image description

This isn't the case when the network connection is downgraded to "Fast 3G" in Chrome developer tools. It took 1.4 min to load the page! This is not a very good user experience.

Image description

On the Fly Compression

With on the fly compression, the server takes the uncompressed data and compresses it before sending it to the browser. There are different tools that you can use to compress the data on the fly. Here are few examples:

With fastify/compress the process is quite simple.

  1. Install the package npm i @fastify/compress
  2. Add the compression hook to your Fastify server

This code will enable global compression on Fastify.

import Fastify from 'fastify';

const fastify = Fastify();
fastify.register(import('@fastify/compress'));
Enter fullscreen mode Exit fullscreen mode

With the default settings, the compressed page is now only 1,7 MB and with the slower network connection it took only 20 seconds to load the page.

Unfortunately it took over 14 seconds to load the page with the fast connection. The server was too slow to compress the data and made the connection 14 x slower. If you have a powerful server, this might not be an issues.

Image description

Static Compression

When on the fly compression is too slow, you can configure e.g. Webpack to create an html bundle and a compressed version of the bundle.

new CompressionPlugin({
  algorithm: 'gzip',
  filename: '[path][base].gz',
  test: /\.(html)$/
}),
Enter fullscreen mode Exit fullscreen mode

With this Webpack plugin, the server now has two files it can serve:

  • /dist/page.html
  • /dist/page.html.gz

More information about the Webpack configuration: https://webpack.js.org/plugins/compression-webpack-plugin/

If you're not using a module bundler, you can just manually compress the file.

// To keep the original file use the -k option
gzip -k filename
Enter fullscreen mode Exit fullscreen mode

Because some clients might not support compression, the server needs to check which compression algorithms are supported, before sending the compressed data. This is done by checking the client's "Accept-Encoding" HTTP header. Here is an example header from a client that supports gzip, deflate and br (Brotli).

Image description

The server needs to set 'Content-Encoding' HTTP header to the right compression format e.g. gzip or br, otherwise the browser doesn't know it's being served a compressed stream.

Here's a Node/Fastify example of serving a compressed file:

fastify.get('/page',
  async (req, res) => {

    let encoding = req.headers['accept-encoding']
          ? req.headers['accept-encoding'].split(',')
          : [];
    let gzipReply = encoding.find(e => e == 'gzip') ? true : false;

    if (gzipReply) {
      return res.header('Content-Encoding', 'gzip')
              .send(createReadStream('./dist/page.html.gz'));
    }

    return res.send(createReadStream('./dist/page.html'));
  }
);
Enter fullscreen mode Exit fullscreen mode

With the fast connection, the load time was typically reduced by 100 - 200 ms.

With the slower "Fast 3G" connection, the load time was reduced from 1.4 min to 11 sec.

There is one caveat here for development. Brotli file compression increased the Webpack bundling time from 7 seconds to 23 seconds. Fortunately gzip doesn't have the same issue. Because of this, it might be a good idea to enable Brotli for the production build, but not necessarily for the development builds.

Webpack Production Mode

By setting Webpack mode to production, the build size can be further reduced. In the case above, the html file size decreased to 7,6 MB and the compressed gzip file to 1,6 MB in production mode.

module.exports = {
  mode: 'development',
};

module.exports = {
  mode: 'production',
};
Enter fullscreen mode Exit fullscreen mode

Conclusions

There are multiple ways to decrease the file size with Node. I hope this article helped you to make your next project faster.

💖 💪 🙅 🚩
pikkue
Eero Kukko

Posted on December 7, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related