By Ayush Kumar
When building any React application developers often miss out taking into consideration the performance aspect of the application. An efficiently running application is vital for shipping an application which is bug free, light weight, resourceful and which posses great user experience. In this article we will discuss about the tools that will help you to understand the build size and ways to optimize it.
webpack-bundle-analyzer
for your React.js projectWebpack is a modern module bundler for modern javascript applications.
When using webpack for a React.js application, it processes our package.json
to build a dependency graph.
Using the entry points defined in it's configuration, it then then combines every module that our project needs
into one or more bundles.
Webpack-bundle-analyzer is a plugin that will help us go into the details of the generated chunks and to optimize it.
webpack-bundle-analyzer
for your React.js projectBelow is a guide on how to configure webpack-bundle-analyzer
on your React.js project.
1
yarn add -D webpack webpack-cli webpack-dev-server html-webpack-plugin webpack-bundle-analyzer @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react
Make a file in the root folder with the name .babelrc and configure babel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"regenerator": true
}
]
]
}
Now, make a directory in the root folder with the name webpack and make a file called webpack.config.js inside the webpack directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const path = require('path');
const HtmlWebPackPlugin = require('html-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
mode: 'development',
entry: path.resolve(__dirname, '..', './src/index.js'),
resolve: {
extensions: ['.js', '.jsx'],
},
plugins: [
new HtmlWebPackPlugin({
template: path.resolve(__dirname, '..', './public/index.html'),
}),
new BundleAnalyzerPlugin()
],
module: {
rules: [
{
test: /\.(js)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
]
},
output: {
path: path.resolve(__dirname, '..', './build'),
filename: 'bundle.js',
}
}
Now, change the scripts to start the server through package.json. our start script will be
1
"start": "webpack serve --config webpack/webpack.config.js --open"
Now, start server with the npm or yarn.
1
yarn start
webpack will open bundle analyzer on port number 8888. The analyser would generate a page like below.
From the above image, we can identify the packages which are causing our build size to increase significantly. eg: moment library occupies a whopping 1.84mb in the generated asset chunks.
A typical React application would consists of many components, pages, and third-party libraries.
Overtime all the javascript assets gets added up and the application generate a very large build. We've seen build size as big as 25mb.
Even on a fast internet, this takes up a lot of time to get downloaded, and thus leads to a bad user experience.
React 16.6 has introduced code spitting, which can split a large built into multiple smaller chunks.
Suspense
along with code spitting provides an intuitive way to keep our user engaged while our assets is downloaded.
Let's take a below code example -
1
2
3
4
5
6
import HomePage from './pages/Home;
import SomePrivatePage from './pages/SomePrivatePage;
<Switch>
<Route path="/" component={HomePage} />
<Route path="/private-page" component={SomePrivatePage} />
</Switch>
route /private-page
is accessible only after the user logs-in. But without code spitting the generated build would have all the imports needed for SomePrivatePage
.
This makes our user download the assets needed for the page which they might never visit.
React.lazy
function provides a built-in way to split the large build into smaller manageable javascript chunks with tiny legwork.
React.Suspense
accepts a fallback components which allows us to display any Component while the chunk is loaded.
1
2
3
4
5
6
7
8
9
10
import react, { lazy, Suspense } from 'react';
const LazyLoading = (importFunc, { fallback = null } = { fallback: null }) => {
const LazyComponent = lazy(importFunc);
return (props) => (
<Suspense fallback={fallback || 'Loading...'}>
<LazyComponent {...props} />
</Suspense>
);
};
Now, let's change the way we import.
1
2
3
4
5
6
7
8
//Filename: src/Utils/load/index.js
import load from '../Utils/load;'
const HomePage = load(() => import ('./pages/Home'));
const SomePrivatePage = load(() => import ('./pages/SomePrivatePage'));
<Switch>
<Route path="/" component={HomePage} />
<Route path="/private-page" component={SomePrivatePage} />
</Switch>
Optimizing static assets like Images (jpegs, pngs, gifs) is a easy low hanging fruit one could do to improve the build size. Modern lossless compression provides an easy way to compress the images without any loss in quality. We highly recommend using an external tool like tinypng to compress all your image assets.
It's recommended that developers take full cognizance of the impact before introducing any new Library/Framework into the codebase. Many a times, the library replacement would be a few 100 lines of code. If the introduced module is not Tree shaking compatible, it would introduce a lot of weight even though we might just it for a tiny feature.
Regularly upgrading the 3rd party libraries provides us a easy way to access performance improvements introduced into the external packages. Due care needs to be taken to see if there are any breaking changes and the impact of the change log is fully understood.
When generating the production React build, webpacker combines all js and css file into a minified version eg. bundle.js, style.css.
While minification is great, we can further reduce the build by serving our users compressed version of the same file. Most modern browsers supports the following compression
Brotli provides better compression than gzip. In our experience, Brotli compressed JS files are 14% smaller than gzip, HTML files are 21% smaller than gzip. Css files are 17% smaller than gzip.
Note: Some old browser doesn't support brotli. It better to generate both brotli & gzip compressed files to serve newer and older browsers.
Steps to follow to add compression while generating the build -
Step 1. Add the compression plugin
1
yarn add -D compression-webpack-plugin
Step 2. import compression-webpack-plugin in webpack.config.js file
1
const CompressionPlugin = require(‘compression-webpack-plugin’);
Step 3. Add CompressionPlugin to plugins of webpack.config.js file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
plugins: [
newCompressionPlugin(),
new CompressionPlugin({
filename: "[path][base].br",
algorithm: "brotliCompress",
test: /\.(js|css|html|svg)$/,
compressionOptions: {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
},
},
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false,
}),
]
If you are using Nginx to server the static assets, you would need to enable serving of compressed asset by the below nginx config.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
gzip on;
gzip_static on;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_proxied any;
gzip_vary on;
gzip_comp_level 9;
gzip_buffers 16 8k;
gzip_http_version 1.1;
brotli on;
brotli_static on;
brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
brotli_comp_level 11;
...
}
As the application grows, there may be stale/unused packages that are installed but can be removed.
Many popular Javascript library provides a tree shaking way to import functions. When done this way, it only introduces that particular function and it's dependency while generating the build.
Example let's take a popular library called lodash
1
import { has as _has } from 'lodash';
The above import statement includes the complete library of lodash into the build (530kb compressed).
Now, replacing the above import with tree shaking compatible import would significantly reduce lodash's footprint.
1
import _map from 'lodash/map';