Slimming down what goes over the wire

Reducing the size of JavaScript bundles is one of the most direct ways to improve page load performance. This walkthrough looks at two techniques for shrinking a bundle: minification, which removes unnecessary characters from source code, and compression, which applies an encoding algorithm to the data itself.

App screenshot

Check the current state

To see what we're working with, open the app and inspect its network activity:

  1. In DevTools, press Control+Shift+J (or Command+Option+J on Mac).
  2. Open the Network panel.
  3. Enable the Disable cache checkbox.
  4. Reload the app.

Original bundle size in Network panel

Even after removing unused code, the main bundle in this sample app weighs in at 225 KB. That's still a substantial payload for a page that primarily handles a voting interaction.

Minification: removing what the parser ignores

Minification strips whitespace, shortens variable names, and simplifies expressions. Consider a small function saved on its own—at roughly 112 bytes, removing the whitespace drops it to 83 bytes. Further mangling, such as shortening identifiers and tightening expressions, brings it down to 62 bytes.

function soNice() {
  let counter = 0;

  while (counter < 100) {
    console.log('nice');
    counter++;
  }
}

function soNice(){let counter=0;while(counter<100){console.log("nice");counter++;}}

function soNice(){for(let i=0;i<100;)console.log("nice"),i++}

Each step makes the code harder for humans to read, but the browser's JavaScript engine interprets all three versions identically. In this case, that's roughly a 50% reduction from a modest starting point—the percentage savings only grow with larger files.

The sample app uses webpack version 4, which minifies bundles automatically when mode is set to production. This is handled through TerserWebpackPlugin, a wrapper around the Terser compression tool. You can inspect the minified output by selecting main.bundle.js in the Network panel and opening the Response tab.

Minified response

To see what the bundle would look like without minification, switch the mode setting in webpack.config.js to development and reload:

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

Bundle size of 767 KB

Revert the change before continuing:

module.exports = {
  mode: 'production',
  mode: 'none',
  //...

How you handle minification depends on your tooling:

  • With webpack v4 or newer, minification is already enabled by default in production mode.
  • Older webpack versions need TerserWebpackPlugin added to the build config.
  • Alternatives like BabelMinifyWebpackPlugin and ClosureCompilerPlugin also work.
  • Without a bundler, you can run Terser as a CLI tool or include it as a direct dependency.

Compression: encoding for transport

Compression is distinct from minification. Minified code is still valid JavaScript; compressed data must be decompressed before it can be parsed. Compression happens via content-encoding headers exchanged between the client and server.

In the Headers tab of the Network panel, the request headers include an accept-encoding field. This tells the server which compression algorithms the browser supports.

Accept encoding header

Three options are common for HTTP traffic:

  • Gzip (gzip): The most widely supported format, built on Deflate, and available in all current browsers.
  • Deflate (deflate): Rarely used.
  • Brotli (br): A newer algorithm with better compression ratios, supported by the latest versions of major browsers.

The sample app runs on Express, which makes it easy to try both dynamic and static compression.

Dynamic compression

Dynamic compression generates compressed assets on the fly for each request. Its main advantage is simplicity—there's no pre-compression step, and it's a natural fit for dynamically generated pages. The tradeoff is that higher compression levels take time, adding latency while the user waits.

To enable it in Express, add the compression middleware as a dev dependency and mount it before express.static in server.js:

"devDependencies": {
  //...
  "compression": "^1.7.3"
},

const express = require('express');
const compression = require('compression');

//...

const app = express();

app.use(compression());

app.use(express.static('public'));

//...

After reloading, the bundle drops from 225 KB to 61.6 KB. The response headers now include content-encoding: gzip, confirming the server is applying the encoding.

Bundle size with dynamic compression

Content encoding header

Static compression

With static compression, assets are compressed once, ahead of time, and served as pre-built files. This eliminates runtime compression latency but adds build time, especially when using aggressive compression settings.

To compress during the webpack build, add CompressionPlugin as a dev dependency, import it in webpack.config.js, and add it to the plugins array:

"devDependencies": {
  //...
  "compression-webpack-plugin": "^1.1.11"
},

const path = require("path");

//...

const CompressionPlugin = require("compression-webpack-plugin");

module.exports = {
  //...
  plugins: [
    //...
    new CompressionPlugin()
  ]
}

The plugin uses gzip by default and also compresses index.html. This produces a main.bundle.js.gz file in the output directory alongside the original bundle.

Final outputted files in public directory

The server then needs to serve those gzipped files when the original JavaScript is requested. Add a route in server.js before the static middleware:

const express = require('express');
const app = express();

app.get('*.js', (req, res, next) => {
  req.url = req.url + '.gz';
  res.set('Content-Encoding', 'gzip');
  next();
});

app.use(express.static('public'));

//...

That route intercepts GET requests for *.js files, appends .gz to the URL, sets Content-Encoding: gzip, and then calls next() so the static handler can serve the compressed file.

Bundle size reduction with static compression

The result is a bundle size reduction comparable to what dynamic compression achieved—but with no per-request processing cost.

Both minification and compression are now defaults in many build pipelines. The key is verifying whether your toolchain already handles them—webpack and Express both provide straightforward paths—or whether you need to add the corresponding plugins and middleware yourself.