4
之前写的《webpack入门必备(一):基础配置》主要介绍了webpack基础解析所需的loader/plugin。而随着日常webpack的使用,我们会更多关注如何构建更快、构建产物更小、构建产物符合规范...希望这篇文章可以让你找到答案。

一、webpack4的构建优化

1. 加快构建速度

1.1 优化配置

这里介绍的主要的几种优化配置如下所示:

  1. 缩小构建范围

    • exclude、include范围
    • noParse
    • IgnorePlugin
  2. 多进程

    • thread-loader/happypack
  3. 缓存

    • cache-loader/cacheDirectory,把loader的处理结果缓存到本地
    • Dll缓存,把一些不常变更的模块构建产物缓存在本地

如果你有没用过的配置可以接着看下面的具体使用方法,如果你已经很熟悉了则可以跳过此节~

1. exclude、include范围
配置来确保转译尽可能少的文件(exclude 的优先级高于 include)
const rootDir = process.cwd();

{
        test: /\.(j|t)sx?$/,
        include: [path.resolve(rootDir, 'src')],
        exclude: [
          /(.|_)min\.js$/
        ],
}

PS. 相比exclude可以多用include

2. noParse
如果一些库不依赖其它库的库,不需要解析他们,可以引入来加快编译速度。
noParse: /node_modules\/(moment|chart\.js)/
3. IgnorePlugin
忽略第三方包指定目录。 (他是webpack 内置的插件)

例如: moment (2.24.0版本) 会将所有本地化内容和核心功能一起打包,我们就可以使用 IgnorePlugin 在打包时忽略本地化内容(语言包),见下图。

plugins: [
  // 表示忽略moment下的locale文件夹内容
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]

image

4.1 thread-loader
把 thread-loader 放置在其它 loader 之前,那么它之后的 loader 就会在一个单独的 worker 池中运行。
// 项目中babel-loader一般耗时比较长,所以可以配置thread-loader 
rules: [ 
    { 
        test: /\.jsx?$/, 
        use: ['thread-loader', 'cache-loader', 'babel-loader'] 
    } 
] 
4.2 happypack
运行在Node.js上的webpack是单线程,将文件解析的任务拆分由多个子进程并发进行,然后子进程处理完任务后再将结果发送给主进程,提升项目构件速度。
(但是因为进程的分配和管理也需要时间,所以使用后不一定快,需要项目接入实验一下)
const Happypack = require("happypack");
module.exports = {
  module: {
    rules: [
      {
        test: /\.js[x]?$/,
        use: "Happypack/loader?id=js",
        include: [path.resolve(__dirname, "src")],
      },
      {
        test: /\.css$/,
        use: "Happypack/loader?id=css",
        include: [
          path.resolve(__dirname, "src"),
          path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
        ],
      },
      {
        test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/,
        use: "Happypack/loader?id=file",
        include: [
          path.resolve(__dirname, "src"),
          path.resolve(__dirname, "public"),
          path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
        ],
      },
    ],
  },
  plugins: [
    new Happypack({
      id: "js", //和rule中的id=js对应
      //将之前 rule 中的 loader 在此配置
      use: ["babel-loader"], //必须是数组
    }),
    new Happypack({
      id: "css", //和rule中的id=css对应
      use: ["style-loader", "css-loader", "postcss-loader"],
    }),
    new Happypack({
      id: "file", //和rule中的id=file对应
      use: [
        {
          loader: "url-loader",
          options: {
            limit: 10240, //10K
          },
        },
      ],
    }),
  ],
};
5. cache-loader/cacheDirectory
在性能开销较大的loader处使用,将构建结果缓存中磁盘中。
(默认存在node_modueles/.cache/cache-loader目录下。 )

cacheDirectory例子:

rules: [
      {
            test: /\.(j|t)sx?$/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                },
              }
       }
 ]

cache-loader例子:

rules: [
    {
        test: /\.(css)$/,
        use: [
          { loader: 'style-loader' },
          { loader: 'cache-loader' },
          { loader: 'css-loader' },
          { loader: 'postcss-loader' }
        ]
      }
]
6. Dll缓存(动态链接库)
将复用性较高的第三方模块打包到DLL中,再次构建时直接复用,这样只需重新打包业务代码。
(注意是DLL缓存是大大缩短了首次构建时间,像之前的cache-loader优化都是缩短rebuild时间

使用相关插件:

  • DllPlugin 插件:用于打包出一个个单独的动态链接库文件。
  • DllReferencePlugin 插件:用于在主要配置文件中去引入 DllPlugin 插件打包好的动态链接库文件。

具体步骤:
(1) 新增一个webpack配置去编译DLL文件([name].dll.js[name].manifest.json

// 新增一个webpack-dll.config.js配置文件

const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
const distPath = path.resolve(__dirname, 'dll');
module.exports = {
  entry: {
    // 把 React 相关模块的放到一个单独的动态链接库
    react: ['react', 'react-dom'],
    // 把项目需要所有的 polyfill 放到一个单独的动态链接库
    polyfill: [
      'core-js/fn/object/assign',
      'core-js/fn/object/entries',
      ...
    ],
  },
  output: {
    // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称(react 和 polyfill)
    filename: '[name].dll.js',
    path: distPath,
    // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
    // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
    library: '_dll_[name]',
  },
  plugins: [
    // 接入 DllPlugin
    new DllPlugin({
      // 动态链接库的全局变量名称,需要和 output.library 中保持一致
      // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值(_dll_react)
      name: '_dll_[name]',
      context: process.cwd(),
      // 描述动态链接库的 manifest.json 文件输出时的文件名称
      path: path.join(__dirname, 'dll', '[name].manifest.json'),
    }),
  ],
};
// package.json里新增dll的构建命令
"scripts": {
    "dll": "webpack --config webpack-dll.config.js",
}

(2) dev构建时,告诉 Webpack 使用了哪些动态链接库

// webpack.config.js文件

const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

plugins: [
    // 使用的动态链接库(react和polyfill的)
    new DllReferencePlugin({
      context: process.cwd(),
      manifest: path.join(rootDir, 'dll', 'react.manifest.json'),
    }),
    new DllReferencePlugin({
      context: process.cwd(),
      manifest: path.join(rootDir, 'dll', 'polyfill.manifest.json'),
    }),
    ...
]

(3) html template里引入文件

因为我这里只是本地构建加速,所以就以dev的方式引入

<script src="./dll/polyfill.dll.js?_dev"></script>
<script src="./dll/react.dll.js?_dev"></script>

到这DLL就配好了。有些人可能比较好奇react.dll.jsreact.manifast.js到底是什么文件,做了什么事?你看看他两个文件就知道啦~

  • react.dll.js其实主要就是所引用模块的代码集合
  • react.manifast.js则写明包含哪些模块、模块路径
// react.dll.js文件部分内容如下所示。
var _dll_react = (function(modules) {
  // ... 此处省略 webpackBootstrap 函数代码
}([
  function(module, exports, __webpack_require__) {
    // 模块 ID 为 0 的模块对应的代码
  },
  function(module, exports, __webpack_require__) {
    // 模块 ID 为 1 的模块对应的代码
  },
  // ... 此处省略剩下的模块对应的代码 
]));


// react.manifast.js文件部分内容如下所示。
{
  // 描述该动态链接库文件暴露在全局的变量名称
  "name": "_dll_react",
  "content": {
    "./node_modules/process/browser.js": {
      "id": 0,
      "meta": {}
    },
    // ... 此处省略部分模块
    "./node_modules/react-dom/lib/ReactBrowserEventEmitter.js": {
      "id": 42,
      "meta": {}
    },
     ...
}

1.2 检测工具

常用工具:speed-measure-webpack-plugin
使用方法:用其来包裹 Webpack 的配置

image

2. 构建产物方面

2.1 减小构建产物大小、提高复用率

这里介绍的主要的几种优化配置如下所示:

  • optimization.splitChunks分包
  • babel配置@babel/plugin-transform-runtime
  • tree-shaking

具体使用:

1. optimization.splitChunks分包
将业务代码和第三方依赖库进行分包,减小index.js的大小;
抽离多页应用的公共模块,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。
 optimization: {
    minimize: false,
    moduleIds: 'named',
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      maxAsyncRequests: 6,
      maxInitialRequests: 6,
      automaticNameDelimiter: '~',
      name: true,
      cacheGroups: {
        polyfill: {
          test: /[\\/]node_modules[\\/](core-js|@babel|regenerator-runtime)/,
          name: 'polyfill',
          priority: 70,
          minChunks: 1,
          reuseExistingChunk: true
        },
        lib: {
            test: /[\\/]node_modules[\\/]/,
            name: 'lib',
            chunks: 'initial',
            priority: 3,
            minChunks: 1,
         },
       ...
      }
    }
 }
2. babel配置

提取所有页面所需的helper函数到一个包里,避免重复注入

"plugins": [
    "@babel/plugin-transform-runtime"
    ...
]
3. tree-shaking
如果使用ES6的import 语法,那么在生产环境下,会自动移除没有使用到的代码。

(1) 具体配置

const TerserPlugin = require('terser-webpack-plugin');

const config = {
 // 生产模式下tree-shaking才生效
 mode: 'production',
 optimization: {
  // Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记。
  usedExports: true,
  minimizer: [
   // 删除死代码的压缩器
   new TerserPlugin({...})
  ]
 }
};

(2) 哪类代码会被shake掉?以下有一些事例

// no tree-shaking
import Stuff from './stuff';
doSomething(Stuff);

// tree-shaking
import Stuff from './stuff';
doSomething();

// tree-shaking
import './stuff';
doSomething();

// no tree-shaking
import 'my-lib';
doSomething();

// 全部导入 no tree-shaking
import _ from 'lodash';

// 具名导入 tree-shaking
import { debounce } from 'lodash';

// 直接导入具体的模块  tree-shaking
import debounce from 'lodash/lib/debounce';

(3) 什么叫有副作用的代码?
`只要被引入,就会对应用程序产生重要的影响。
(一个很好的例子就是全局样式表,或者设置全局配置的js文件。)`

(4) 有副作用的代码我们不希望被shake,我们可以配置如下

// 所有文件都有副作用,全都不可 tree-shaking
{
 "sideEffects": true
}
// 没有文件有副作用,全都可以 tree-shaking
{
 "sideEffects": false
}
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件
{
 "sideEffects": [
  "./src/file1.js",
  "./src/file2.js"
 ]
}

(5) 注意,babel配置需要配modules: false,忽略import/export代码编译

const config = {
 presets: [
  [
   '@babel/preset-env',
   {
     // commonjs代码不能被tree-shaking
     // 所以babel保留我们现有的 es2015 import/export 语句,不进行编译
    modules: false
   }
  ]
 ]
};

2.2 检测工具

常用工具:webpack-bundle-analyzer
使用方法:用其来包裹 Webpack 的配置

image

3. 产物检查

ES check

生产环境构建时,会检查构建产物里是否存在es6语法。有则抛出错误并提示你去进行babel编译,这样避免了构建产物不合要求的情况。

image
image

具体使用例子:

// package.json 命令里加上es-check检查
"dist:basic": "rimraf public && cross-env NODE_ENV=production webpack --config webpack-dist.config.js && es-check es5 ./public/**/*.js"

二、webpack5的构建优化

1. 速度优化

1.1 编译缓存

编译缓存就是在首次编译后把结果缓存起来,在后续编译时复用缓存,从而达到加速编译的效果。
webpack5默认开启编译缓存,缓存默认是在内存里,你可以自定义。
module.exports = {
    cache: {
        // 将缓存类型设置为文件系统
        type: "filesystem", 
        // 缓存的位置(默认是node_modules/.cache/webpack)
        cacheDirectory: path.resolve(__dirname, '.temp_cache'), 
     
        // 指定构建过程中的代码依赖。webpack将使用这些项目以及所有依赖项的哈希值来使文件系统缓存无效。
        buildDependencies: {
     
            // 当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效。 
            config: [__filename], 

            // webpack.config、loader和所有从你的配置中require的模块都会被自动添加。如果有其他的东西被构建依赖,你可以在这里添加它们
      },
      
      // 指定缓存的版本。当需要更新配置缓存时,通过设置此版本使缓存失效。
      version: '1.0'  
    }
}

一些参数注解

  • cache: true 就是 cache: { type: 'memory' } 的别名
  • type: 'filesystem'|'memory'。

如果设置'memory'则缓存在内存且不能配置其他信息,设置成'filesystem'就可以配置更多信息。默认开发模式使用的是'memory',生产模式是false。

  • version: 当配置文件和代码都没有发生变化,但是构建的外部依赖(如环境变量)发生变化时,预期的构建产物代码也可能不同。这时就可以使用 version 配置来防止在外部依赖不同的情况下混用了相同的缓存。例如,可以传入 cache: {version: process.env.NODE_ENV},达到当不同环境切换时彼此不共用缓存的效果。

1.2 长效缓存 Long-term caching

长效缓存指的是能充分利用浏览器缓存,尽量减少由于模块变更导致的构建文件hash值的改变,从而导致文件缓存失效。
(由于moduleId和chunkId确定了,构建的文件的hash值也会确定。)
1.2.1 引子
  1. chunk、module都是什么?

    • module:每一个可被导入导出的源码js、css文件就是一个module。
    • chunk:module经webpack依赖分析、打包生成的单独文件块。如:入口entry里的文件、SplitChunks抽离的公共代码
    • bundle:chunk后面经过编译/压缩打包等处理后就变成了bundle,bundle文件直接被html文件引用。

image

  1. webpack提供了以下3种哈希值,分别是什么意思?有啥优缺点?
  • hash 所有bundle文件都是同一个hash。(【缺点】不修改文件的情况下rebuild后hash会更新)
  • chunkhash 同一个entry/及entry引用的chunk文件都是同一个hash。(【缺点】修改chunk文件内容后,这个hash不变)
  • contenthash 一个文件一个hash,修改哪个文件哪个文件的hash就改变。(【缺点】如果删除一个entry里的chunk,entry和chunk及好多个文件的hash都变了,不利于长效缓存。

比如只是jsx删除引用的一个css文件 好多bundle文件的hash就都变了。)

1.2.2 webpack4实现长效缓存

之前需要通过如下配置达到长效缓存:

plugins: [
- new webpack.NamedModulesPlugin(),
+ new webpack.HashedModuleIdsPlugin(),

或者配置

optimization.moduleIds = 'hashed’ 
optimization.chunkIds = 'named'

配置说明:

  • 在开发环境下使用 NamedModulesPlugin 来固化 module id,在生产环境下使用 HashedModuleIdsPlugin 来固化 module id(因为构建结果文件会更小)
  • 使用 NamedChunksPlugin 来固化 runtime 内以及在使用动态加载时分离出的 chunk 的 chunk id

(NamedChunksPlugin 只能对普通的 Webpack 模块起作用,异步模块(异步模块可以在 import 的时候加上 chunkName 的注释,比如这样:import(/ webpackChunkName: “lodash” / ‘lodash’).then() 这样就有 Name 了),external 模块是不会起作用的。)

1.2.3 Webpack5默认启用deterministic实现长效缓存

Webpack5采用新的算法,生产模式下默认启用如下配置不仅实现长效缓存,还减少了文件打包大小:

optimization.chunkIds: "deterministic"
optimization.moduleIds: "deterministic"
mangleExports: “deterministic"

PS.具体采用的算法还需要进一步深入研究~

2. 包构建大小优化

2.1 Node Polyfill脚本被移除

Webpack 4版本附带了大多数Node.js核心模块的polyfill,一旦前端使用了任何核心模块,这些模块就会自动应用,导致polyfill文件很大,但是其实有些polyfill是不必要的。
而现在webpack5将不会自动为Node.js模块添加Polyfills,需要开发者手动添加合适的Polyfills。

升级迁移至webpack5需要注意:

  • 尽可能尝试使用与前端兼容的模块。
  • 可以为 Node.js 核心模块手动添加 polyfill。错误消息将提示实现方法。
  • 包作者:使用 package.json 中的 browser 字段使包与前端兼容。提供浏览器的替代实现 / 依赖。

2.2 tree-shaking

1.嵌套tree-shaking
能够跟踪对export的嵌套属性的访问,分析模块的export和import的依赖关系,去掉未被使用的模块

// inner.js
export const a = 1;
export const b = 2;

// module.js
export * as inner from './inner';
// or import * as inner from './inner'; export { inner };

// user.js
import * as module from './module';
console.log(module.inner.a); // 在此示例中,可以在生产模式下移除导出 b。

2.内部模块tree-shaking(深度作用域分析)
新属性optimization.innerGraph分析模块导出和导入之间的依赖关系,在生产模式下默认启用。

import { something } from './something';
function usingSomething() {
  return something;
}
export function test() {
  return usingSomething();
}
// 在使用 test 导出时才使用 something。

可以分析以下符号:

  • 函数声明
  • 类声明
  • 带有以下内容的 export default 或变量声明:函数表达式,类表达式,序列表达式,/#_PURE_/ 表达式,局部变量,导入绑定

3.package.json 中的“sideEffects”标志允许将模块手动标记为无副作用,从而在不使用它们时将其移除。
webpack 5 还可以根据对源代码的静态分析,自动将模块标记为无副作用。

更多Webpack5的内容推荐阅读:


psychola
66 声望2 粉丝

前端/copperplate