Webpack packaging optimization


Webapck 4 has a lot of configuration items done for us by default and a lot of optimizations turned on internally. This out-of-the-box experience is obviously great for developers, but it also leaves us with a lot to learn, and when something goes wrong, we don’t know where to start. Let’s take a look at the main optimization options.

DefinePlugin

The DefinePlugin is used to inject global members into our code and will be turned on by default in Production mode. It will inject an environment variable, process.env.node_env, into our environment, which we can use to determine the environment we are running in and execute some logic accordingly.

const webpack = require('webpack')

module.exports = {
  mode: 'none'.entry: './src/main.js'.output: {
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.DefinePlugin({
      // The value requires a code snippet
      API_BASE_URL: JSON.stringify('https://api.example.com')]}})Copy the code

This allows us to use the API_BASE_URL variable directly in the environment

// main.js
console.log(API_BASE_URL)
Copy the code

Tree-shaking

Tree-shaking is the name for shaking a Tree. With shaking, we shake the dead branches and leaves from the Tree. In our project, tree-shaking removes parts of our code that are not referenced. Tree-shaking is not a configuration option, it is a set of features used together. We can use Optimization to turn on some features. Optimization means optimization. How do we configure it

module.exports = {
  mode: 'none'.entry: './src/index.js'.output: {
    filename: 'bundle.js'
  },
  optimization: {
    // The module exports only the members that are used
    usedExports: true.// Merge every module into a function if possible
    concatenateModules: true.// Compress the output
    // minimize: true}}Copy the code

We can think of usedExports as being for labeling “dead leaves,” and for taking care to shake down those dead leaves. ConcatenateModules combines all the code into a single function as much as possible to improve efficiency and reduce the size of the code. This feature is also known as Scope collieries, which is a feature introduced in Webpack3.

  • The Tree – shaking and Babel

Because Webpack is evolving so fast, when we look for data, we don’t necessarily find it for our current version, especially for Tree-shaking. Many of the data shows that if we use the Babel-Loader, The result is that tree-shaking fails. The premise for tree-shaking is that we have to use the ES Modules specification to organize our code, and @babel/preset-env internally translates the ES Modules code into commonJS code. So tree-shaking doesn’t work. However, if you turn on both, tree-shaking will work because the latest version of @babel/preset-env internally turns off converting ES Modules to CommonJS.

module.exports = {
  mode: 'none'.entry: './src/index.js'.output: {
    filename: 'bundle.js'
  },
  module: {
    rules: [{test: /\.js$/,
        use: {
          loader: 'babel-loader'.options: {
            presets: [
              // If Babel loads the module with the ESM converted, then Tree Shaking will fail
              // ['@babel/preset-env', { modules: 'commonjs' }]
              // ['@babel/preset-env', { modules: false }]
              // You can also use the default configuration, which is auto, so that babel-Loader will automatically disable ESM conversion
              ['@babel/preset-env', { modules: 'auto'},},},},optimization: {
    // The module exports only the members that are used
    usedExports: true.// Merge every module into a function if possible
    // concatenateModules: true,
    // Compress the output
    // minimize: true}}Copy the code

sideEffects

Webpack4 also has a new feature called sideEffects, which allows us to identify if our code has sideEffects, giving Tree shaking more space for compression. SideEffects are things that modules do besides exporting members. SideEffects are usually only used when developing an NPM module. That’s because the official website mixes sideEffects with Tree shaking. So many people mistakenly think that the two are causal, in fact, they are not related to the two. When we encapsulate components, we usually import all the components into one file and export them collectively from that file, but when other files import the file, they import all the components from that exported file

// components/index.js
export { default as Button } from './button'
export { default as Heading } from './heading'
Copy the code
// main.js
import { Button } from './components'
document.body.appendChild(Button())
Copy the code

SideEffects solves this problem when Webpack packages the Heading component into a file

module.exports = {
  mode: 'none'.entry: './src/index.js'.output: {
    filename: 'bundle.js'
  },
  optimization: {
    sideEffects: true,}}Copy the code

At the same time we import in packag.json and close the files that have no side effects so that we don’t pack useless files into the project

{
  "name": "side-effects"."version": "0.1.0 from"."main": "index.js"."author": "maoxiaoxing"."license": "MIT"."scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^ 4.41.2"."webpack-cli": "^ 3.3.9"
  },
  "sideEffects": false
}
Copy the code

The important thing to note about using sideEffects is that we really don’t have sideEffects in our code, and if there were sideEffects, we wouldn’t be able to do this configuration.

// exten.js
// Add an extension method to the Number prototype
Number.prototype.pad = function (size) {
  // Convert number to string => '8'
  let result = this + ' '
  // Add the specified number of 0 before the number => '008'
  while (result.length < size) {
    result = '0' + result
  }
  return result
}
Copy the code

For example, we add a method to the “Number” stereotype in the extend.js file. We don’t export the member, we just extend a method based on the stereotype. We import the extend.js in another file

// main.js
// Side effect module
import './extend'
console.log((8).pad(3))
Copy the code

If we also specify that all modules in the project have no side effects, then the method added to the prototype will not be packaged and will definitely report an error when running. In addition, the CSS modules we import into the code are also side effects modules, so we can specify our side effects modules in package.json

{
  "name": "side-effects"."version": "0.1.0 from"."main": "index.js"."author": "maoxiaoxing"."license": "MIT"."scripts": {
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^ 4.41.2"."webpack-cli": "^ 3.3.9"
  },
  "sideEffects": [
    "./src/extend.js"."*.css"]}Copy the code

Modules that are thus identified as having side effects are also packaged.

Webpack Code Splitting

The advantages of modularity are obvious, but there is also a downside: all the code in our projects will be packaged together, and if our projects are too large, our packaging results will be too large. The reality is that not all of the modules must be loaded when we first load them, but the modules are packaged together, so on the one hand, the browser will run slowly, and on the other hand, it will waste some traffic and bandwidth. So the logical way is to package our code into multiple JS files according to certain rules, do subcontract processing, load on demand, and we will greatly improve the response rate of our application. So one might think that Webpack is not our code scattered into a function to improve efficiency, why do subcontracting, is not a contradiction? In fact, everything is the extreme of the negative, Webpack code merge because we tend to be too granular modular development, So Webpack must merge a lot of code together, but if the overall amount of code is too large, it will lead to our single package file is too large, but affect the efficiency. So modularity too small doesn’t work, modularity too big doesn’t work, and Code Splitting is to solve our problem of modularity too large.

  • Multiple entry packing

Multi-entry packaging is to take a page as a package entry, and extract the common parts of different pages into a common file, and multi-entry packaging is easy to configure

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none'.entry: { // Multientry package, multiple entry files
    index: './src/index.js'.album: './src/album.js'
  },
  output: {
    filename: '[name].bundle.js' // For multi-entry packaging, use placeholders
  },
  optimization: {
    splitChunks: {
      // Automatically extract all common modules into a separate bundle
      chunks: 'all'}},module: {
    rules: [{test: /\.css$/,
        use: [
          'style-loader'.'css-loader']]}},plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry'.template: './src/index.html'.filename: 'index.html'.chunks: ['index'] // Configure chunk to prevent simultaneous loading
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry'.template: './src/album.html'.filename: 'album.html'.chunks: ['album']]}})Copy the code
  • Webpack loads on demand

On demand loading is a common requirement in our development, and when we deal with packaging, we can load which modules we want. Webpack supports dynamic import to support the on-demand loading of our modules, all dynamically loaded modules will be automatically subcontracted, compared to the subcontract loading method, dynamic loading method is more flexible. For example, if we have two modules, Album and posts, we can use import to implement dynamic import. Import returns a promise object.

// ./posts/posts.js
export default() = > {const posts = document.createElement('div')
    posts.className = 'posts'.return posts
}
Copy the code
// ./album/album.js
export default() = > {const album = document.createElement('div')
    album.className = 'album'.return album
}
Copy the code
// import posts from './posts/posts'
// import album from './album/album'

const render = () = > {
  const hash = window.location.hash || '#posts'

  const mainElement = document.querySelector('.main')

  mainElement.innerHTML = ' '

  if (hash === '#posts') {
    // mainElement.appendChild(posts())\
    // Magic note: rename the module
    import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) = > {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    // mainElement.appendChild(album())
    import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) = > {
      mainElement.appendChild(album())
    })
  }
}

render()

window.addEventListener('hashchange', render)
Copy the code

Modular packaging of CSS

  • MiniCssExtractPlugin is a plugin that extracts the CSS file from the package file, allowing you to load CSS modules on demand.
  • The optimize- csS-assets – webpack-Plugin is a plugin that compresses CSS files because with the MiniCssExtractPlugin, there is no need to load the CSS using the style tag. So we don’t need a style-loader
  • The terser-webpack-plugin turns optimization on because the optimize-csS-assets webpack-plugin is needed in the minimizer of Optimization, Webpack will assume that our compression code needs to be configured, so the JS file will not be compressed, so we need to install terser-webpack-plugin to compress the JS code
/ / install the mini - CSS - extract - the plugin
yarn add mini-css-extract-plugin --dev
/ / install optimize - CSS - assets - webpack - the plugin
yarn add optimize-css-assets-webpack-plugin --dev
/ / install terser webpack -- the plugin
yarn add terser-webpack-plugin --dev
Copy the code

Then we can configure them

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: 'none'.entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name].bundle.js'
  },
  optimization: {
    minimizer: [
      new TerserWebpackPlugin(), // Compress the JS code
      new OptimizeCssAssetsWebpackPlugin() // Compress the modular CSS code]},module: {
    rules: [{test: /\.css$/,
        use: [
          // 'style-loader', // inject the style through the style tag
          MiniCssExtractPlugin.loader, // Loaders using MiniCssExtractPlugin do not need a styleloader
          'css-loader']]}},plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Dynamic import'.template: './src/index.html'.filename: 'index.html'
    }),
    new MiniCssExtractPlugin()
  ]
}
Copy the code

Output hash file name

Usually we go to deploy the front-end resource file, we will enable the server cache static resources, so for the user’s browser can cache static resources, our subsequent request to the server is not required to request these static resource, the corresponding rate of our application will have a big improvement. There are also problems with enabling static resource caching on the client. If you set the cache time to be too short, the cache will be meaningless, and if you set it to be too long, you won’t be able to update the application to the client as soon as an update occurs. In order to solve this problem, we need in production mode, use hash for the file name, so that once we file resources change, our file name will change accordingly, for the client, the new file name also means that the new request, so we can set the cache time is very long, You don’t have to worry about files not being updated.

  • hash

Project-level hashes that generate new hashes as soon as any file is modified

  • chunkhash

The hash is the same as long as the package is in the same route. For example, the hash prefix of JS and CSS within a module is the same

  • contenthash

File-level hashes, depending on the output hash value of the file content, vary depending on the file content. This is the most recommended method

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: 'none'.entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name]-[contenthash:8].bundle.js'
  },
  optimization: {... },module: {... },plugins: [...new MiniCssExtractPlugin({
      filename: '[name]-[contenthash:8].bundle.css'}})]Copy the code