预获取/预加载模块(prefetch/preload)

在使用 import() 动态导入模块时,可以使用下面指令来告知浏览器:

  1. - prefetch(预加载):加载将来默写导航下可能用到的资源
  2. - preload (预加载):加载当前导航下可能用到的资源

这两个指令通过 魔法字符串 的方式添加:

  1. import(/* webpackPrefetch: true */, '模块地址'); // 预获取
  2. import(/* webpackPreload: true */, '模块地址'); // 预加载

预加载和预获取有一下几点区别:

  - preload 会在父 chunk 加载的时候,以并行的方式加载,prefetch 则是在父 chunk 加载完成后浏览器空闲时间内加载。
  - preload chunk 具有中等优先级,并立即下载,下载完成后放在内存中,但不会执行其中的JS语句。prefetch chunk 在浏览器闲置时下载,下载完成后放在缓存中,当有页面使用的时候在缓存中读取。
  - preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  - 浏览器支持程度不同。

注:如果 prefetch 还没下载完之前,浏览器发现当前页面也引用了同样的资源,浏览器会再次发起请求,这样会严重影响性能的,加载了两次,所以不要在当前页面马上就要用的资源上用 prefetch,要用 preload。

CDN 引入

在项目中使用的第三方包可能会使用 CDN 的方式引入,如果使用了 CDN 引入后那么就没必要再将这些第三方包进行打包了,这时可以通过 externals 配置将其排除:

// webpack.config.js
module.exports = {
    externals: {
    // key 为忽略的包名称, value 为当前包暴露出来的全局对象
      lodash: '_',
  },
}

然后手动在 index.html 文件中添加对应的 CDN script 标签,因为只有在生产环境才需要配置 CDN,所有下面代码中判断了开发环境:

<html>
  <body>
    <div id='root'></div>
    <% if (process.env.NODE_ENV === 'production') {%>
      <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.core.js"></script>
    <%}%>
  </body>
<html>

如果所有的文件都放在了 CDN 上面的话,可以修改 output.publicPath 让其都指向 CDN 的地址:

// webpack.config.js
module.exports = {
     //...
  output: {
      publicPath: 'https://cdn.bootcdn.net',
  },
}

Shimming

shimming(垫片) 是一个概念,是某一类功能的统称,意思是给我们的代码填充一些垫片来处理一些问题。

比如依赖的一个第三方包 A 中需要依赖另外一个第三方包 B,但是 A 包中并没有导入 B 包(A 包认为全局存在 B 包),这时我们可以通过 shimming 方式来解决这个问题。

webpack 中使用了一个插件 ProviderPlugin 实现垫片的效果,这个插件不需要下载直接在 webpack 中引入即可。

const { ProvidePlugin } = require('webpack');

module.exports = {
    plugins: {
      new ProvidePlugin({
        // key 是文件中使用的变量名, value 是第三包
          axios: 'axios',
        // 配置为数组表示获取 axios.get 这个属性
        get: ['axios', 'get'],
      })
  },
}

CSS 文件抽离

style-loader 会将 css 插入到 head 标签中,如果想要把 css 代码抽离成一个单独的文件时,需要下载一个插件来实现 mini-css-extract-plugin :

安装插件:

npm i mini-css-extract-plugin -D

修改配置:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
      rules: {
      {
            test: /\.css$/,
          use: [
              MiniCssExtractPlugin.loader, // 去掉 style-loader 使用这个 loader 
              'css-loader',
          ],
        },
    },
  },
  plugins: [
      new MiniCssExtractPlugin({
            filename: 'css/[name]-[hash:6].css', // 配置 css 文件输入文件名称及目录
        }),
  ]
}

一个 chunk 会生成一个 css 文件,也就是说异步加载的 js 文件中引入的 css 会生成一个新的 css 文件。
例如:

// index.js 入口文件
import './style.css';
import date from './date.js'
// date.js
import './date.css';

这些会成一个 css 文件,因为是在一个 chunk 里面,通过 import() 方式导入的话就会生成两个 css 文件:

// index.js 入口文件
import './style.css';
import('./date.js').then(xx);

hash、chunkhash、contenthash

在给打包的文件命名时会使用 placeholder,placeholder 中有三个 hash 可以选择:

  - hash: 跟整个项目有关,只要文件的内容发生变化这个 hash 就会发生变化。
  - chunkhash:跟 chunk 有关系,只有 chunk 中的内容发生了变化,chunkhash 才会发生变化。
  - contenthash:跟文件内容有关系,只有文件内容发生变化时 contenthash 才会发生变化。

如果现在又两个入口 entry,如果使用 hash 命名文件的话,只要一个入口文件中的内容发生了改变,这两个入口打包出来的文件名称都会发生改变,而使用 chunkhash 的话只有对应的那个入口文件打包出来的文件名称才会变化,但是如果我们将 css 单独抽离为文件时,当依赖这个 css 文件的 js 文件发生了变化,这时整个 chunkhash 就会发生变化,从而导致 css 文件的名称也变了,所以这个时候应该使用 contenthash,这样就不会影响到 css 文件了,从而更好的利用浏览器的缓存功能。

注:hash本身是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制);

DLL 动态链接库

DLL(Dynamic Link Library)指的是我们可以将共享的,并且不经常改变的代码,抽取成一个共享的库,比如开发 React 项目,React、ReactDOM 这两个包就是可以共享并且不经常改动的代码,这时就可以将这个两个包单独打包成一个 DLL 库,使用时直接引用这个库就可以,就不在需要每次打包时都重复的打包这两个文件。

DLL 的使用分为三步:

  - 第一步:配置 DLL 的 webpack.config 专属配置文件,利用这个文件打包成 DLL 库
  - 第二部:使用 DllReferencePlugin 插件
  - 第二部:引入 DLL 库

打包 DLL 库

首先先创建一个 dll 的配置文件:

// webpack.dll.config.js
const path = require('path');
const { DllPlugin } = require('webpack');

module.exports = {
    entry: {
      react: ['react', 'react-dom'], // 打包 react、react-dom 两个包
  },
  output: {
      filename: 'dll_[name].js',
    path: path.resolve(__dirname, './dll'), // 当前目录的 dll 文件夹
    library: 'dll_[name]', // 指定打包后的 dll 库导出的变量名, 这里没有文件后缀
  },
  plugins: {
    // 使用 DllPlugin
      new DllPlugin({
          name: 'dll_[name]', // 和上面的 library 命名相同
        path: path.resolve(__dirname, './dll/[name]_manifest.json'), // 指定 manifest.json 文件输出地址
      }),
  },
}
再加上一个 scripts 脚本然后运行
{
    "scripts": {
      "dll": "webpack --config ./webpack.dll.config.js"
  }
}

使用 DllReferencePlugin 插件

运行上面脚本后会生成 dll 目录和使用 react、react-dom 生成的 dll 库,同时还会生成一个 `manifest.json` 文件,接下来我们需要在 webpack 主配置文件中配置 DllReferencePlugin 插件:
// webpack.config.js
const path = require('path');
const { DllReferencePlugin } = require('webpack');

module.exports = {
  plugins: [
      new DllReferencePlugin({
      // 指向 dll 生成的 manifest.js 文件
      manifest: path.resolve(__dirname, './dll/react_manifest.json')
    }),
  ],
}

引入 DLL 文件

运行 npm run build 后就会发现打包出来的文件中已经没有 react、react-dom 这两个第三方包了,所以这时需要我们手动的在 index.js 中添加 script 标签 src 指向 dll 库的文件,但是也可以同时一个插件自动完成这件事。

安装:add-asset-html-webpack-plugin

npm i add-asset-html-webpack-plugin -D
修改 webpack.config 配置
// webpack.config.js
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

module.exports = {
    plugins: [
      // 添加
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, './dll/dll_react.js'), // 指定要添加的 js 文件
      outputPath: 'js', // 这个文件被输出到那个文件夹
      publicPath: 'js', // html script 标签的 src 的前缀, src='js/dll_react.js'
    }),
  ],
}

Terser 工具

Terser 是一个 JavaScript 的Parser(解析)、Mangle(绞肉机)、Compressor(压缩机)的工具集,它可以将代码压缩、丑化让代码体积变得更小。

安装 terser:

npm i terser
// or
npm i terset -g

在命令行使用 Terser

在这里使用的局部的安装方式,所以下面使用 npx 来运行 terser 命令。

npx terser 入口文件 -o 输出文件 [options]

例如入口文件如下:
webpack 的性能优化(一) - 图1
运行:npx terser ./index.js -o ./index.min.js 命令,由于没有指定配置项,所以生成的新文件只去掉了代码中的空格:
webpack 的性能优化(一) - 图2

Compress、Mangle

Terser 中两个常用的配置就是 Compress 和 Mangle,他们的一些配置属性如下:

Compress:

  • arrows:将 class 或 object 中的函数转为箭头函数
  • arguments:将函数中的 arguments 转为对应的形参名称
  • dead_code:去除不可到达的代码(比如 if (false) {…} 这种)

Mangle:

  • toplevel:顶层的全局作用域变量名称进行丑化(默认 false)
  • keep_classnames: 类名称不进行丑化(默认 false)
  • keep_fnames:函数名称不进行丑化(默认 false)

在命令行中通过 -c / -m 指定他们两个的配置,多个配置属性使用逗号隔开:

npx terser ./index.js -o ./index.min.js -c arrows -m toplevel=true,keep_classnames=true,keep_fnames=true

运行后的结果:
webpack 的性能优化(一) - 图3

在 webpack 中使用 Terser

webpack 已经集成了 Terser,将 mode 设置为 production 后就可以对代码进行压缩、丑化,当然我们也可以自己进行配置。

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin'); // 安装 webpack 时已经安装了这个插件

module.exports = {
  optimization: {
        minimize: true, // 打开 minimize
    minimizer: [
        new TerserPlugin({
          extractComments: false, // 不生成 LICENSE 文件
        parallel: true, // 指定多进程打包, true 为 (os.cpus().length - 1) 个进程
        terserOptions: {
          // terser 配置
            compress: true,
          //...
        }
      }),
    ],
  },
}

CSS 代码的压缩

CSS 代码的打包默认是没有进行压缩的,我们可以通过一个插件来实现 CSS 代码的压缩,但也只是去掉 CSS 文件中的空格而已,因为很难去修改选择器、属性名和值。

安装:css-minimizer-webpack-plugin 插件:

npm i css-minimizer-webpack-plugin -D
修改 webpack 配置:
// webpack.config.js
const CSSMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
    plugins: [
      new CSSMinimizerWebpackPlugin({
        parallel: true,
    }),
  ]
}

作用域提升 Scope Hoisting

默认情况下 webpack 打包会生成很多的函数作用域,包括一些自执行函数(IIFE),无论是最开始的代码运行还是加载一个模块都需要执行一系列的函数,Scope Hositing 可以将代码的作用域提升来减少函数作用域,提升效率。

比如基于下面的代码结构:
webpack 的性能优化(一) - 图4
如果没有使用 Scope Hoisting 时打包后的如下,存在很多函数作用域:
webpack 的性能优化(一) - 图5
接下来使用 webpack.optimize.ModuleConcatenationPlugin 插件开启 Scope Hoisting :

// webpack.config.js
const webpack = require('webpack');

module.exports = {
    //...
  plugins: [
      new webpack.optimize.ModuleConcatenationPlugin()
  ]
}

开启 Scope Hoisting 后打包后少了很多函数且 sort 和 index 在同一个作用域下面了。
webpack 的性能优化(一) - 图6

注1:在 mode: production 时默认使用了这个插件,不需要手动配置。

注2:这个插件依赖于 ES Module 的静态分析(在静态分析阶段判断能不能进行作用域提升),所以如果要实现 Scope Hoisting 需要使用 ES Module 而不是 CommonJS Module。