Integrate gzip compression with your Webpack build pipeline to gain free performance benefits

by | Aug 1, 2020 | 0 comments

One of the easiest ways to gain performance benefits for your web application is to compress your files with a compression algorithm. All browsers automatically support gzip compression, which means the browser will know how to decompress it automatically by sending the proper response headers. Your clients will eventually download a much smaller bundle and will load the application faster.

When you create a new react project with create-react-app the created project will encapsulate many of the internal modules being used to build the project.

By default, this kind of project does not have access to the webpack configuration files. To be able to integrate the compression inside our build process we must be able to control the encapsulated modules and their files.

In-order to edit webpack’s configuration file in a react project we must start by ejecting the project.

Ejecting the project is a one-way operation, make sure to work on a separate branch to avoid any issues and be able to return to the previous project’s state.

 

If you do not wish to eject read the following article:

Learn How To Compress Your Responses With Express and Node.js

Now before we eject let’s look at the dependencies for a newly created project:

package.json dependencies before ejecting
{
 "name": "react-gzip",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1"
  },
  ...
  ...
  ...
}

Most of the dependencies are hidden from us so it’s a pretty short list, lets eject the project by running the following command and see the difference:

npm run eject

After ejecting we can see that many internal dependencies were added to the package.json file (webpack, babel, eslint, jest…).

Don’t be intimidated by the number of changes and added dependencies, you gain full control over the project when ejecting.

Package.json dependencies after ejecting
{
  "name": "react-gzip",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@babel/core": "7.9.0",
    "@svgr/webpack": "4.3.3",
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "@typescript-eslint/eslint-plugin": "^2.10.0",
    "@typescript-eslint/parser": "^2.10.0",
    "babel-eslint": "10.1.0",
    "babel-jest": "^24.9.0",
    "babel-loader": "8.1.0",
    "babel-plugin-named-asset-import": "^0.3.6",
    "babel-preset-react-app": "^9.1.2",
    "camelcase": "^5.3.1",
    "case-sensitive-paths-webpack-plugin": "2.3.0",
    "css-loader": "3.4.2",
    "dotenv": "8.2.0",
    "dotenv-expand": "5.1.0",
    "eslint": "^6.6.0",
    "eslint-config-react-app": "^5.2.1",
    "eslint-loader": "3.0.3",
    "eslint-plugin-flowtype": "4.6.0",
    "eslint-plugin-import": "2.20.1",
    "eslint-plugin-jsx-a11y": "6.2.3",
    "eslint-plugin-react": "7.19.0",
    "eslint-plugin-react-hooks": "^1.6.1",
    "file-loader": "4.3.0",
    "fs-extra": "^8.1.0",
    "html-webpack-plugin": "4.0.0-beta.11",
    "identity-obj-proxy": "3.0.0",
    "jest": "24.9.0",
    "jest-environment-jsdom-fourteen": "1.0.1",
    "jest-resolve": "24.9.0",
    "jest-watch-typeahead": "0.4.2",
    "mini-css-extract-plugin": "0.9.0",
    "optimize-css-assets-webpack-plugin": "5.0.3",
    "pnp-webpack-plugin": "1.6.4",
    "postcss-flexbugs-fixes": "4.1.0",
    "postcss-loader": "3.0.0",
    "postcss-normalize": "8.0.1",
    "postcss-preset-env": "6.7.0",
    "postcss-safe-parser": "4.0.1",
    "react": "^16.13.1",
    "react-app-polyfill": "^1.0.6",
    "react-dev-utils": "^10.2.1",
    "react-dom": "^16.13.1",
    "resolve": "1.15.0",
    "resolve-url-loader": "3.1.1",
    "sass-loader": "8.0.2",
    "semver": "6.3.0",
    "style-loader": "0.23.1",
    "terser-webpack-plugin": "2.3.5",
    "ts-pnp": "1.1.6",
    "url-loader": "2.3.0",
    "webpack": "4.42.0",
    "webpack-dev-server": "3.10.3",
    "webpack-manifest-plugin": "2.2.0",
    "workbox-webpack-plugin": "4.3.1"
  },
  ...
  ...
  ...
  ...
  ...
  ...
}

 Now lets install our compression plugin:

npm install compression-webpack-plugin --save-dev

After installing go to webpack.config.js file and import the plugin:

const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
 // ...
 // ...
  plugins: [    
    ...
    ...
    new CompressionPlugin({
      algorithm: 'gzip',
      test: /.js$|.css$/,
    })
]
 // ...
 // ...
};

You can decide to avoid compressing files that are smaller than a certain size in bytes by using the threshold property.

caniuse-brotli
npm run build

At my previous position as a Fullstack Developer, I’ve used gzip compression on a production application, the project’s size before compression was 3MB which by any standard is way too large for the user to download, especially an e-commerce site.

 

After compression, the project size was 773KB which is a 74% decrease in package size!

 

By doing so our mobile users were able to download the application 74% faster for the initial entrance to the application.

After the build is finished webpack should generate the normal build files and the compressed files.

Serving the project to our clients

const cors = require('cors');
const express = require('express');
const path = require('path');
const fs = require('fs');

var port = process.env.PORT || 3000;

// Path to build directory
const clientDirPath = path.resolve(__dirname, 'build');

// Path to index.html file
const clientIndexHtml = path.join(clientDirPath, 'index.html');

// Init express
const app = express();
const serveRouter = express.Router();

// Enable cors
serveRouter.use(cors());

// For each request for .js file
// return the compressed version .gz
app.get('*.js', function (req, res, next) {
  const pathToGzipFile = req.url + '.gz';
  try {
    // Check if .gz file exists
    if (fs.existsSync(path.join(clientDirPath, pathToGzipFile))) {
      // Change the requested .js to return
      // the compressed version - filename.js.gz
      req.url = req.url + '.gz';
      // Tell the browser the file is compressed and it should decompress it.
      // You will get a blank screen without this header because it will try to parse
      // the compressed file.
      res.set('Content-Encoding', 'gzip');
      res.set('Content-Type', 'text/javascript');
    }
  } catch (err) {
    console.error(err);
  }

  next();
});

// Set the static files root directory
// from which it should serve the files from.
console.log('clientDirPath', clientDirPath);
app.use(express.static(clientDirPath));

// Always send the index.html file to the client
app.get('*', (req, res) => {
  res.sendFile(clientIndexHtml);
});

console.log('Starting server');
app.listen(port, () => {
  console.log(`Listening on port: ${port}`);
});

If this article was of value to you then add me on Linkedin
or join “I Read You Learn” Facebook Group by clicking the social icons to the right.

Happy Coding!

IF YOU GOT ANY VALUE SHARE 😄