Now web applications, content is generally very rich, the site needs to load a lot of resources, especially to load a lot of JS files. Js files are obtained from the server, and the volume determines the speed of transfer. After the browser gets the JS file, it also needs to be decompressed, parsed, compiled and executed. Therefore, it is very important to control the volume of JS code and load it on demand for front-end performance and user experience.

This article introduces JS packaging optimization from Tree Shaking and code segmentation, for those interested in following along. Clone the following projects github.com/jasonintju/… It’s a simple React SPA.

Tree Shaking

When you package, remove some unused code and keep the size of the packaged code to a minimum. See tree-Shaking Performance Optimization Practices – Principles for a detailed introduction.

Clone project, install dependencies, first NPM run build package the initial code, size and distribution as follows (SRC /utils/utils. Js file package size is 11.72Kb) :

SRC/containers/About/test. The js reference only but didn’t use to SRC/utils/utils js function sets the file is a tool, there are a lot of a lot of function, and we are only one of them. By default, the entire file is packaged into main.js, which is obviously a lot of redundancy and is just fine for Tree Shaking optimization.

Modify the.babelrc

{
  "presets": [["env", { "modules": false}]."react"."stage-0"]}Copy the code

Modify thepackage.json

{
  "name": "optimizing-js"."version": "1.0.0"."sideEffects": false
}
Copy the code

If this is set, all modules have no side effects and unused modules can be deleted. The package result is as follows:

import React from 'react';
// Only arraySum is introduced, other methods in utils.js are not packaged
import { arraySum } from '@utils/utils';
import './test'; // Reference, "unused", will not be packaged
import './About.scss'; // Reference, "unused", will not be packaged

class About extends React.Component {
  render() {
    const sum = arraySum([12.3]);
    return (
      <div className="page-about">
        <h1>About Page</h1>
        <div> 12 plus 3 equals {sum}</div>
      </div>); }}export default About;
Copy the code

As noted in the comments above, Tree Shaking thinks this is unused code, so it can be removed. In fact, we know that’s not the case. Test.js can be deleted, but CSS and SCSS are useful code that we just need to introduce. Therefore, we need to change the value of sideEffects:

{
  "sideEffects": [
    "*.css"."*.scss"."*.sass"]}Copy the code

[] indicates that, except for the file (type) in [], all other files have no side effects, can be relieved to delete. Package result at this time:

As you can see, CSS and other style files are now packed as expected. If there are other types of files that have sideEffects, but you also want to include them, add them in sideEffects: [], either a specific file or a specific file type.

As to why these two changes can achieve the effect of the Tree Shaking, can consult developers.google.com/web/fundame… Or other articles, I won’t go into detail here.

The code segment

Single-page application, if all resources are packaged in a JS, there is no doubt that the volume will be very large, the first screen will be blank for a long time, and the user experience will be terrible. So, to split the code into a small JS, optimize the load time.

Separate third-party library code

Third party library code is extracted separately, separated from business code, reducing JS file size. Add the following to webpack.base.conf.js:

module: {... },optimization: {
  splitChunks: {
    cacheGroups: {
      venders: {
        test: /node_modules/.name: 'vendors'.chunks: 'all'}}}},plugins:...Copy the code

Dynamic import

Components in a business can be loaded asynchronously using the Dynamic import syntax of the ECMAScript proposal. The usage method is as follows:

// src/containers/App/App.js

Comment out this line of code
// import About from '@containers/About/About';

// Change the module to dynamic import mode
<Route path="/about" render={() => import(/* webpackChunkName: "about" */ '@containers/About/About').then(module= > module.default)}/>
Copy the code

Package result at this time:

As you can see, the

component has been packaged separately by Webpack into the corresponding JS file. At the same time, with the React-Router, the separation of

components also allows on-demand loading: about.js is loaded by the browser only when the About page is accessed.

Note that we are simply using dynamic import now and do not take into account many boundary conditions such as load progress, load failure, timeout, etc. You can develop a higher-level component that includes all of these exception handling. The community has a great React-loadable

npm i react-loadable

// src/containers/App/App.js
import Loadable from 'react-loadable'; Const LoadableAbout = Loadable({loader: () => import(/* webpackChunkName:"about"* /'@containers/About/About'),
  loading() {
    return <div>Loading...</div>;
  }
});

class App extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <Header />

          <Route exact path="/" component={Home} />
          <Route path="/docs" component={Docs} />
          <Route path="/about"component={LoadableAbout} /> </div> </BrowserRouter> ); }}Copy the code

React-loadable also provides preload functionality. If statistics show that users are likely to enter the About page after entering the home page, we will load about.js when the home page is loaded, so that js resources will be loaded when users jump to the About page, and the user experience will be better.

// src/containers/App/App.js
componentDidMount() {
  LoadableAbout.preload();
}
Copy the code

If you’re not familiar with the Network panel, take a look at Chrome DevTools – Network.

Extract reusable business code

Third-party library code has been extracted separately, but there is also some reusable code in the business code, typically the utility library utils.js. Now, the About and Docs components refer to utils.js, and webpack only packs a copy of utils.js in main.js. Main.js is loaded on the home page, and other pages that use utils.js can be referenced normally. In line with our expectations. But so far we’ve only loaded the About page asynchronously. What if we also loaded the Docs page asynchronously?

// src/containers/App/App.js
Comment out this line of code
// import Docs from '@containers/Docs/Docs';

const LoadableDocs = Loadable({
  loader: (a)= > import(/* webpackChunkName: "docs" */ '@containers/Docs/Docs'),
  loading() {
    return <div>Loading...</div>; }});class App extends React.Component {
  render() {
    return( <BrowserRouter> <div> <Header /> <Route exact path="/" component={Home} /> <Route path="/docs" component={LoadableDocs} /> <Route path="/about" component={LoadableAbout} /> </div> </BrowserRouter> ); }}Copy the code

Package result at this time:

As you can see, both about. Js and docs. Js are packed with utils.js. Add the following to webpack.base.conf.js:

module: {... },optimization: {
  splitChunks: {
    cacheGroups: {
      venders: {
        test: /node_modules/.name: 'vendors'.chunks: 'all'
      },
      default: {
        minSize: 0.minChunks: 2.reuseExistingChunk: true.name: 'utils'}}}},plugins:...Copy the code

Then pack to see the result:

Utils.js was also packaged separately, as expected.

Separate third-party libraries that are not used on the home page and are not reusable

Suppose, now, that Docs. Js references lodash, a three-way library:

import React from 'react';
import _ from 'lodash';
import { arraySum } from '@utils/utils';
import './Docs.scss';

class Docs extends React.Component {
  render() {
    const sum = arraySum([1.3]);
    const b = _.sum([1.3]);
    return (
      <div className="page-docs">
        <h1>Docs Page</h1>
        <div> 1 plus 3 equals {sum}</div>
        <br />
        <div>use _.sum, 1 plus 3 equals {b} too.</div>
      </div>); }}export default Docs;
Copy the code

Packing results:

Lodash.js is only used in Docs pages and Docs pages are probably not visited very often, so it’s not wise to pack lodash.js in venders. Js that loads on the home page.

Modify the webpack. Base. Conf. Js:

. venders: {test: /node_modules\/(? ! (lodash)\/)/.// LoDash is removed, and the remaining third-party libraries are grouped into a package, vendor-common
  name: 'vendors-common'.chunks: 'all'
},
lodash: {
  test: /node_modules\/lodash\//.// The lodash library is packaged separately and named vender-Lodash
  name: 'vender-lodash'
},
default: {
  minSize: 0.minChunks: 2.reuseExistingChunk: true.name: 'utils'}...Copy the code

At this point, loDash was bundled into a separate package and loaded on demand with the Docs page to achieve the desired loading effect.

The cache

After the project is packaged, the resources are deployed on the server. The client needs to request the server to download these resources so that the user can see the content. With caching, clients can greatly reduce unnecessary requests and delays by downloading resources only when they are updated. To tell whether a file has been updated, use the file name + hash. In this case, ‘[name].[contenthash:8].js’ has been used.

However, when packaging, the Webpack runtime code can sometimes lead to situations where nothing changes and the two build codes have different hashes; Or, changing the code in file A causes the hash of some unmodified code files to change as well. This is caused by the injection of the runtime and manifest which changes every build.

Note: Different versions of Webpack may result in different packaging results. Newer versions may not have this hash problem, but to be safe, it is recommended to follow these steps.

Separate webPack runtimeChunk code

// webpack.base.conf.js
optimization: {
  runtimeChunk: {
    name: 'manifest'
  },
  splitChunks: {...}
}
Copy the code

If a file is modified, only the hash of this file and the manifest.js file will change, while the hash of other files will remain unchanged. Before packaging:

// About.scss
.page-about {
  padding-left: 30px;
  color: # 545880; // Change the font color
}
Copy the code

Revised:

HashedModuleIdsPlugin

Adding or deleting some modules may change the hash of irrelevant files. This is because when WebPack is packaged, the module.id of some modules will change according to the order of imported modules, which will change the hash of files.

Workaround: Use the HashedModuleIdsPlugin plugin built into WebPack, which generates the corresponding module.id based on the relative path of the imported module, so that if the content does not change plus the module.id does not change, the generated hash will not change.

// webpack.prod.conf.js
const webpack = require('webpack'); . plugins: [new webpack.HashedModuleIdsPlugin(), new BundleAnalyzerPlugin()]
Copy the code

The complete optimization code can be found at github.com/jasonintju/…


Useful articles: webpack separation third-party libraries and utilities file developers.google.com/web/fundame…