preface

In the optimization of front-end applications, it is extremely important to control the size of loaded resources. Most of the time, what we can do is to control the size, split and reuse resources in the process of packaging and compilation. This article is mainly based on Webpack packaging. React, Vue and other eco-developed single-page applications are used to illustrate how to deal with resources and cache from the perspective of Webpack packaging. The main thing we need to do is to optimize the configuration of Webpack. It also involves a small number of business code changes.

The webpack-bundle-Analyzer plug-in can also be used to analyze packaged resources. Of course, there are many analysis plug-ins available, which will be used as an example in this article.

TIP: WebPack version @3.6.0.


Package environment and code compression

First we have a basic WebPack configuration:

// webpack.config.js
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const PROJECT_ROOT = path.resolve(__dirname, './');
module.exports = {
  entry: {
    index: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash:4].js'
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: 'babel-loader',
        include: PROJECT_ROOT,
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new BundleAnalyzerPlugin()
  ],
  resolve: {
    extensions: ['.js', '.jsx']
  },
};Copy the code

Execute the package to see a project with more than 1M js:

Hash: e51AFC2635F08322670b Version: webPack 3.6.0 Time: Currently, 2769ms Asset Size Chunks Names index.caa7.js 1.3 MB 0 [emitted] [big] indexCopy the code

Add DefinePlugin and UglifyJSPlugin to reduce the size of your plugins.

// webpack.config.js ... {... plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }) ] ... }Copy the code

You can see the package output at this point:

Hash: 84338998472a6d3c5c25
Version: webpack 3.6.0
Time: 9940ms
        Asset    Size  Chunks                    Chunk Names
index.89c2.js  346 kB       0  [emitted]  [big]  indexCopy the code

The code size was reduced from 1.3m to 346K.

DefinePlugin

DefinePlugin allows you to create a global constant that can be configured at compile time. This can be very useful when the development mode and the release mode are built to allow different behaviors. If logging is not performed in a development build but in a release build, global constants can be used to determine whether logging is performed. This is where the DefinePlugin comes in, setting it so you can forget the rules for developing and publishing builds.

There are a lot of times in our business code and third-party package code that we need to judge process.env.node_env to do different processing, whereas in a production environment we obviously don’t need non-production processing. Here we set process.env.node_env to json.stringify (‘production’), which means that the packaging environment is set to production. The UglifyJSPlugin plug-in can then be used to remove some of the redundant code when packaging the production environment.

UglifyJSPlugin

UglifyJSPlugin is mainly used to parse and compress JS code. It is based on Uglify-ES to process JS code. It has a variety of configuration options: github.com/webpack-con… By compressing the code and removing redundancy, the volume of packaged resources is greatly reduced.


Split code/load on demand

In single-page applications such as React or Vue, the control of page routing and view is realized by the front end, and the corresponding business logic is in THE JS code. When an application is designed with a lot of pages and logic, the resulting JS file resources can be quite large.

However, when we open a url corresponding to the page, actually need not all JS code, all we need is a main runtime code and the view corresponding to the business logic of the code, before loading the next view to load that part of the code. Therefore, the best way to optimize this is to load the JS code on demand.

Lazy loading, or loading on demand, is a great way to optimize a web page or application. In this way, your code leaves at some logical breakpoint, and then references, or is about to reference, new blocks of code as soon as you have done something in those blocks. This speeds up the initial loading of the application and reduces its overall size, since some code blocks may never be loaded.

Dynamic import technology is provided in Webpack to achieve code split, first in webpack configuration need to configure the split out of each sub-module configuration:

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const PROJECT_ROOT = path.resolve(__dirname, './');
module.exports = {
  entry: {
    index: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash:4].js',
    chunkFilename: '[name].[chunkhash:4].child.js',
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: 'babel-loader',
        include: PROJECT_ROOT,
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new BundleAnalyzerPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
    }),
    new UglifyJSPlugin({
      uglifyOptions: {
        ie8: false,
        output: {
          comments: false,
          beautify: false,
        },
        mangle: {
          keep_fnames: true
        },
        compress: {
          warnings: false,
          drop_console: true
        },
      }
    }),
  ],
  resolve: {
    extensions: ['.js', '.jsx']
  },
};Copy the code

It is set to [name].[chunkhash:4].child.js, where name corresponds to module name or ID. Chunkhash is the hash of the module’s contents.

Webpack then provides two ways to dynamically import business code:

  • import('path/to/module') -> Promise.
  • require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)

Import () is mainly recommended for the latest versions of WebPack. Note: Import uses Promises, so make sure you support Promise polyfills in your code.

// src/index.js
function getComponent() {
  return import(
    /* webpackChunkName: "lodash" */
    'lodash'
  ).then(_ => {
    var element = document.createElement('div');
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    return element;
  }).catch(error => 'An error occurred while loading the component');
}
getComponent().then(component => {
  document.body.appendChild(component);
})Copy the code

You can see the package information:

Hash: d6ba79FE5995bcf9fa4d Version: webpack 3.6.0 Time: 7022 ms Asset Size Chunks Chunk Names lodash. 89 f0. Child. Js 85.4 kB 0 [emitted] lodash index. 316 e. Js 1.96 kB 1 [emitted] index [0] ./src/index.js 441 bytes {1} [built] [2] (webpack)/buildin/global.js 509 bytes {0} [built] [3] (webpack)/buildin/module.js 517 bytes {0} [built] + 1 hidden moduleCopy the code

As you can see, the packaged code generates index.316e.js and lodash.89f0.child.js, which is split by import. Import it receives a path argument, which refers to the path of the submodule pair, and also notes that it is possible to add a line comment /* webpackChunkName: “Lodash” */, this comment is not useless, it defines the name of the submodule, which corresponds to [name] in output.chunkfilename. The import function returns a Promise that will perform subsequent actions, such as updating the view, when asynchronously loaded into the submodule code.

React loads on demand

React-router can load code on demand based on the route. Use webPack to load code dynamically on React.

import React, { Component } from 'react'; export default function lazyLoader (importComponent) { class AsyncComponent extends Component { state = { Component: null } async componentDidMount () { const { default: Component } = await importComponent(); this.setState({ Component: Component }); } render () { const Component = this.state.Component; return Component ? <Component {... this.props} /> : null; } } return AsyncComponent; };Copy the code

In the Route:

<Switch>
  <Route exact path="/"
    component={lazyLoader(() => import('./Home'))}
  />
  <Route path="/about"
    component={lazyLoader(() => import('./About'))}
  />
  <Route
    component={lazyLoader(() => import('./NotFound'))}
  />
</Switch>Copy the code

Rendering in Route is the component returned by the lazyLoader function, which executes the importComponent function after mount (i.e. () => import(‘./About’)) dynamically loads its component module (split code) and renders the component after loading successfully.

Code packaged this way:

Hash: 02A053D135a5653DE985 Version: webPack 3.6.0 Time: Dilemma dilemma 9399ms Asset Size Chunks Names 0.db22.child.js 5.82 kB 0 [emitted] 1. Fcf5.child.js 4.4 kB 1 2.442 dchild.js 3 kB 2 [emitted] index.1bbc.js 339 kB 3 [emitted] [big] indexCopy the code

Extract the Common resource

Long caches for third-party libraries

First of all, for some large third-party libraries, such as React, react-dom and react-router used in React, we do not want them to be packaged repeatedly, and we do not want to change the resources in this part to cause client reloading in each version update. Here you can use webPack’s CommonsChunkPlugin to extract these common resources;

The CommonsChunkPlugin is an optional feature for creating a single file (aka chunk) that contains a common module for multiple entry chunks. By stripping out the common modules, the resultant files can be loaded once at the beginning and then stored in the cache for later use. This gives a speed boost because the browser quickly removes the common code from the cache, rather than loading a larger file every time a new page is visited.

We need to add a new entry to the entry to pack libraries that need to be removed. Here we pack ‘react’, ‘react-dom’, ‘react-router-dom’, and ‘immutable’ into vendor separately. Then define a CommonsChunkPlugin in your plugins, set its name to vendor to associate them, and set minChunks to Infinity to prevent other code from being packaged.

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const PROJECT_ROOT = path.resolve(__dirname, './');
module.exports = {
  entry: {
    index: './src0/index.js',
    vendor: ['react', 'react-dom', 'react-router-dom', 'immutable']
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash:4].js',
    chunkFilename: '[name].[chunkhash:4].child.js',
  },
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: 'babel-loader',
        include: PROJECT_ROOT,
        exclude: /node_modules/
      }
    ]
  },
  plugins: [
    new BundleAnalyzerPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
    }),
    new UglifyJSPlugin({
      uglifyOptions: {
        ie8: false,
        output: {
          comments: false,
          beautify: false,
        },
        mangle: {
          keep_fnames: true
        },
        compress: {
          warnings: false,
          drop_console: true
        },
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: Infinity,
    }),
  ],
  resolve: {
    extensions: ['.js', '.jsx']
  },
};Copy the code

Run the package and you can see:

Hash: 34a71fcfd9a24e810c21 Version: webpack 3.6.0 Time: 9618ms Asset Size Chunks 0.2d6.child.js 5.82 kB 0 [emitted] 1.6e26.child.js 4.4 kB 1 [emitted] E4bc.chil.js 3 kB 2 [emitted] index.4e2f.js 64.2 kB 3 [emitted] index vendor.5df1.js 276 kB 4 [emitted] [big] vendorCopy the code

You can see that Vendor is packaged separately.

Package again when we change the business code:

Hash: cd3f1BC16B28AC97e20a Version: webpack 3.6.0 Time: 9750 ms Asset Size Chunks Chunk Names 0.2 c65. Child. Js 5.82 kB 0 [emitted] 1.6 e26. Child. Js 4.4 kB 1 [emitted] E4bc.chil. js 3 kB 2 [emitted] index.4d45.js 64.2 kB 3 [emitted] index vendor.bc85.js 276 kB 4 [emitted] [big] vendorCopy the code

The Vendor package is also packaged, but its file hash has changed, which obviously does not meet our long cache requirements. This is because webPack uses the CommoChunkPlugin to generate runtime code (which handles the mapping of code modules), and even without changing the vendor code, This runtime is still going to change with packaging and into Verdor, so the hash will start to change. The solution is to separate the runtime code from the CommonsChunkPlugin and modify it to:

// webpack.config.js
...
new webpack.optimize.CommonsChunkPlugin({
  name: ['vendor', 'runtime'],
  minChunks: Infinity,
}),
...Copy the code

When you perform the packaging, you can see that the generated code has additional Runtime files, and that vendor’s hash value remains the same even if the business code is changed.

Of course, this runtime is actually very short, so we can inline the HTML directly. If we use the HTMl-webpack-plugin to process the HTML, The inline can be handled automatically with the HTML-webpack-inline-source-plugin plug-in.

The withdrawal of public resources

The JS resources we package include js resource packages of different entries and sub-modules, but they also load the same dependency modules or code repeatedly. Therefore, we can package some resources that they depend on together into a common JS resource through CommonsChunkPlugin.

// webpack.config.js plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity,}), the new webpack.optimize.Com monsChunkPlugin ({/ / (public chunk (commnons chunk) name) name: "Commons ", // (public chunk name) filename:" Commons.[chunkhash:4].js", // (modules must be shared by 3 entry chunks) minChunks: 3})],Copy the code

[chunkhash:4].js file.[chunkhash:4].js file.

Execute the package and see the following result:

Hash: 2577e42dC5d8b94114c8 Version: webpack 3.6.0 Time: 24009ms Asset Size Chunks 0.2 eie.child.js 90.8 kB 0 [emitted] 1. 2.557a. cached. js 88 kB 2 [emitted] vendor.66fd.js 275 kB 3 [emitted] [big] vendor index.688b.js 64.2 kB 4 [emitted] index Commons. A61e.js 1.78 kB 5 [emitted] CommonsCopy the code

It turns out that Commons.[chunkhash].js has almost no actual content, yet it’s clear that every submodule relies on some of the same dependencies. Analyze a wave with Webpack-bundle-Analyzer:

You can see that all three modules depend on LoDash, yet it is not pulled out.

This is because chunk in CommonsChunkPlugin refers to each entry in the entry, and therefore does not apply to children Chunk split out of an entry. You can package the common dependencies of the split submodule into Commons by configuring the children parameter in the CommonsChunkPlugin plugin:

// webpack.config.js plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity,}), the new webpack.optimize.Com monsChunkPlugin ({/ / (public chunk (commnons chunk) name) name: "Commons ", // (public chunk name) filename:" Commons.[chunkhash:4].js", // (modules must be shared by 3 entry chunks) minChunks: 3}), the new webpack.optimize.Com monsChunkPlugin ({/ / (select all selected chunks of a chunks) children: True, // (at least three sub-chunks are required to share this module before extracting) minChunks: 3,})],Copy the code

View the packing effect:

The common resources of its sub-modules are packed into the index, which is not ideally packed into Commons, again because Commons is for the entry module, and there are no three entry modules sharing resources. In single-entry applications you can choose to remove Commons and set async to true in the CommonsChunkPlugin configuration of the submodule:

// webpack.config.js plugins: [ new BundleAnalyzerPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production') }), new UglifyJSPlugin({ uglifyOptions: { ie8: false, output: { comments: false, beautify: false, }, mangle: { keep_fnames: true }, compress: { warnings: false, drop_console: true }, } }), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'runtime'], minChunks: Infinity,}), the new webpack.optimize.Com monsChunkPlugin ({/ / (select all selected chunks of a chunks) children: true, / / async loading (asynchronous) : True, // (at least three sub-chunks are required to share this module before extracting) minChunks: 3,})],Copy the code

View the effect:

The common resources of the submodule are packaged into 0.9c90.child.js, which is the Commons of the submodule.


tree shaking

Tree shaking is a term used to describe removing dead-code from a JavaScript context. It relies on static structural features in the ES2015 module system, such as import and export. The term and concept actually arose from the ES2015 module packaging tool rollup.

When we introduce a dependency’s output, we may only need a part of the dependency’s code, while another part of the code is unused. If this part of the code can be removed, the volume of the packaged resource can be significantly reduced. First of all, Tree Shaking is implemented in WebPack based on es2015 module mechanism supported in WebPack. Most of the time we compile JS code using Babel, which handles it through its own module loading mechanism. This causes the Tree Shaking processing in Webpack to fail. So in the configuration of Babel you need to turn off processing for module loading:

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

Then let’s take a look at how Webpack handles packaged code, with an entry file index.js and an utils.js:

// utils.js
export function square(x) {
  return x * x;
}
export function cube(x) {
  return x * x * x;
}Copy the code
// index.js
import { cube } from './utils.js';
console.log(cube(10));Copy the code

Packaged code:

// index.bundle.js
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function square(x) {
  return x * x;
}
function cube(x) {
  return x * x * x;
}Copy the code

cube
__webpack_exports__
square
unused harmony export square
square
UglifyjsWebpackPlugin