Bundle size measurement and graphing
To measure and get an overview of bundle sizes and components, we can use webpack-bundle-analyzer to do this. This tool will generate a fairly detailed and easy to understand chart of the components and their sizes in our bundle.
The easiest way to setup and use is to generate a stats.json
file using webpack and turn it on with npx
.
1 2 3 4 | webpack --profile --json <span class="token operator">></span> stats.json <span class="token comment"># ví dụ đống file bundle của chúng ta để trong thư mục dist</span> npx webpack-bundle-analyzer stats.json dist/ |
webpack-bundle-analyzer
will open a browser tab and display a chart like this:
To understand this chart, we need to understand some of the definitions of size here:
Stat size
is the size of input, after webpack bundle but before optimizers like Minify, Uglifier.Parsed size
is the size of the file in memory (after optimization). This is the size of the JavaScript code parsed by the browser.gzip size
is the size after compression by gzip (This is the size of the file transmitted over the network).
1, Avoid importing the entire library (global import)
Optimal level: High
With some large libraries, we absolutely can only import the part we need instead of importing the whole library. If done enough and correctly, we can reduce the size of bundle relatively by omitting unused components.
Libraries that can import each part include: lodash
, react-bootstrap
, antd
, …
However, if you do not do enough places, just one place using the wrong import statement will result in our app bundle with the library with unused parts.
An example of Lodash when importing the entire library:
As you can see, lodash
was lodash
up to 3 times (1 time at lodash.js
, once at lodash.min.js
and once at partial imports). This would be the worst case scenario we could run into. Imagine with 3-4 libraries like this, how big will our bundle be bloated.
There are two ways to make sure you import each part. Note that this is the way of code, and does not apply to a particular library.
Use the babel plugin
the babel-plugin-transform-imports plugin has the ability to replace the import destructured of an entire library with partial imports.
Config is as follows:
1 2 3 4 5 6 7 8 9 10 | # <span class="token punctuation">.</span> babelrc <span class="token string">"plugins"</span> <span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">[</span> <span class="token string">"transform-imports"</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token string">"lodash"</span> <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string">"transform"</span> <span class="token operator">:</span> <span class="token string">"lodash/${member}"</span> <span class="token punctuation">,</span> <span class="token string">"preventFullImport"</span> <span class="token operator">:</span> <span class="token boolean">true</span> <span class="token comment">// sẽ báo lỗi nếu phát hiện việc import cả thư viện</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">]</span> |
Will have the following effects:
1 2 3 4 5 | <span class="token keyword">import</span> <span class="token punctuation">{</span> map <span class="token punctuation">,</span> some <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'lodash'</span> <span class="token comment">// sẽ được thay thế thành</span> <span class="token keyword">import</span> map <span class="token keyword">from</span> <span class="token string">'lodash/map'</span> <span class="token keyword">import</span> some <span class="token keyword">from</span> <span class="token string">'lodash/some'</span> |
Use ESLint
We can use the no-restricted-imports rule to report an error if we encounter an import statement for the library.
1 2 3 4 5 6 7 8 9 10 | <span class="token comment">// .eslintrc</span> <span class="token string">"no-restricted-imports"</span> <span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">"error"</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token string">"paths"</span> <span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token string">"lodash"</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> |
ESLint will issue an error if it gets an import statement like this:
1 2 | <span class="token keyword">import</span> <span class="token punctuation">{</span> map <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'lodash'</span> |
And will pass this sentence
1 2 | <span class="token keyword">import</span> map <span class="token keyword">from</span> <span class="token string">'lodash/map'</span> |
2, Use code-splitting
Optimal level: Depends on each system
By using dynamic import or suspense, we can split our code into async chunks and only load until they are needed. This allows us to reduce the size of the bundle initially, but will not reduce the overall size (which may make the bundle a bit heavier).
Config:
1 2 3 4 5 6 7 8 | <span class="token comment">// webpack.config.js</span> optimization <span class="token operator">:</span> <span class="token punctuation">{</span> splitChunks <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">// chứa đủ các thể loại chunk</span> chunks <span class="token operator">:</span> <span class="token string">'all'</span> <span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
By default, it will create a vendor
chunk, which separates the application code from the libraries. This is quite fine when there is an update from the app side, only the app’s code changes, without changing anything to the libraries (only true when the resources are properly cached) the client side will reduce the download time. vendor
files.
However, it is necessary to research and consider whether to use it or not, because sometimes code splitting will slow down the user operation because we have to download, parse, and execute more code. Depending on the structure of the system, code splitting may cause the browser to download multiple files at the same time. (With HTTP / 1.1, there is a limit on the number of parallel connections to the same domain – see more )
The recommended way to use that is to divide a chunk by a route. However, this is not required.
Lazy way to load a component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token comment">// MyComponent.jsx</span> <span class="token keyword">import</span> React <span class="token punctuation">,</span> <span class="token punctuation">{</span> Suspense <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'react'</span> <span class="token keyword">import</span> Loading <span class="token keyword">from</span> <span class="token string">'..'</span> <span class="token comment">// Tạo một lazy component bằng React.lazy</span> <span class="token keyword">export</span> <span class="token keyword">const</span> MyLazyComponent <span class="token operator">=</span> React <span class="token punctuation">.</span> <span class="token function">lazy</span> <span class="token punctuation">(</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">import</span> <span class="token punctuation">(</span> <span class="token comment">/* webpackChunkName: "my-component" */</span> <span class="token string">'./MyComponent'</span> <span class="token punctuation">)</span> <span class="token punctuation">,</span> <span class="token punctuation">)</span> <span class="token keyword">const</span> <span class="token function-variable function">MyComponent</span> <span class="token operator">=</span> <span class="token parameter">props</span> <span class="token operator">=></span> <span class="token punctuation">(</span> <span class="token operator"><</span> Suspense fallback <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator"><</span> Loading <span class="token operator">/</span> <span class="token operator">></span> <span class="token punctuation">}</span> <span class="token operator">></span> <span class="token operator"><</span> MyLazyComponent <span class="token punctuation">{</span> <span class="token operator">...</span> props <span class="token punctuation">}</span> <span class="token operator">/</span> <span class="token operator">></span> <span class="token operator"><</span> <span class="token operator">/</span> Suspense <span class="token operator">></span> <span class="token punctuation">)</span> <span class="token keyword">export</span> <span class="token keyword">default</span> MyComponent |
Here we use dynamic import syntax to tell Webpack to separate a chunk for MyComponent
and its dependency co.
Setting webpackChunkName
not required, it helps us manage the name of the spit file (see config guide ). If two lazy components have the same name, they will be pushed together into a chunk.
React.lazy
is used to allow lazy components to be rendered as a normal component. Suspense
provides a fallback component (which will render if the import has not been successful). Suspense
can be used to wrap anywhere depending on what you allow the user to see while loading.
Read more about lazy
and Suspense
here
3, Do not include source map
Optimal level: Depends on each system
Source map is the link between source code and file bundle. It is useful for debugging, but should not appear in production environments.
With JS source-map, the devtool option will handle the generation of source-map.
At development, eval-source-map
will help us see the source of the file and speed up the rebuild
At production, we should set it to false
to turn off source-map generation
For source-map of CSS, Less or Sass, the configs depend on each loader used. When using css-loader, sass-loader and less-loader, we should set options: { sourceMap: true }
and false
at production in the loader’s config. The default production is false, so we don’t need to add this setting.
4, Remove replaceable libraries
Optimal level: Depends on each system
Sometimes to handle a request from the spec, we add a new library to handle, but in fact this library can do a lot of other things. Simply because you think that you may need to use it later, or you simply have not thought of how to handle it yet, you can add the library quickly.
Adding a bunch of redundant libraries will greatly affect the bundle file.
Sometimes I even use lodash
just to use some of its functions like isEmpty
or filter
, … Don’t get me wrong, lodash
extremely good, but in this case there is really no need to do so.
Rewriting the above functions with pure JS only takes 1-2 hours and results in a reduction of a huge library in bundle.
For each library, we need to analyze:
- We only use a small part of it, right?
- Can we rewrite the functions we need in a reasonable amount of time?
If both questions above are YES, then rewriting the necessary functions would be a wiser choice.
5, Eliminate prop-types
Optimal level: High
With React, the prop-types
declaration will ensure the data type for the props passed to a component. It is true that they are extremely effective during development, they are also disabled in production environment (for performance reasons). However, the declaration is still in the file bundle.
The Babel transform-react-remove-prop-types plugin will completely remove the prop-type
declaration from the bundle files. However, prop-types of dependencies will not be removed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <span class="token comment">// .babelrc</span> <span class="token punctuation">{</span> <span class="token string">"env"</span> <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string">"production"</span> <span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token string">"plugins"</span> <span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">[</span> <span class="token string">"transform-react-remove-prop-types"</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token string">"removeImport"</span> <span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Note: This plugin should only be enabled for production environments.
6, Aim for modern browsers
Optimal level: Medium
To include polyfills, you have already heard of core-js and regenerator-runtime , right?
By default, all polyfills are included and core-js weigh approximately 154KiB while the regenerator-runtime is only 6.3KiB.
By accepting only modern browsers and their recent versions, we can reduce the size of polyfills.
The Babel-preset-env plugin has the ability to replace the import of all core-js by choosing specific browsers to support.
Config:
1 2 3 4 5 6 7 8 9 10 11 | <span class="token comment">// .babelrc</span> <span class="token string">"presets"</span> <span class="token operator">:</span> <span class="token punctuation">[</span> <span class="token punctuation">[</span> <span class="token string">"@babel/preset-env"</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token string">"useBuiltIns"</span> <span class="token operator">:</span> <span class="token string">"entry"</span> <span class="token punctuation">,</span> <span class="token comment">// config này giúp ta chỉ việc import 2 dependency regenerator-runtime và core-js một lần </span> <span class="token string">"corejs"</span> <span class="token operator">:</span> <span class="token string">"3.6"</span> <span class="token comment">// phải cung cấp version của core-js</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">,</span> <span class="token punctuation">]</span> <span class="token punctuation">,</span> |
1 2 3 | <span class="token keyword">import</span> <span class="token string">'regenerator-runtime/runtime'</span> <span class="token keyword">import</span> <span class="token string">'core-js/stable'</span> |
To declare supported browsers, we use browserlist syntax
1 2 | <span class="token string">"browserslist"</span> <span class="token operator">:</span> <span class="token string">"last 2 Chrome versions, last 2 Firefox versions, last 2 safari versions"</span> <span class="token punctuation">,</span> |
Conclusion
Thank you for reading, if there is any other way or tip, please comment below (bow)