1. 初级分析:使用 webpack 内置的stats
方便但是缺点是粒度较粗,分析更多细节比较麻烦。
- 在 npm script 中把打包时输出的 stats 重定向到文件中
- 在 Node.js 中把配置传入 webpack 方法中进行调用,回调中提供了 stats
2. 速度分析:使用 speed-measure-webpack-plugin
可以看到每个 loader 和插件执行耗时,重点关注红色部分,表示耗时过长,黄色次之,绿色甚好。
3. 体积分析:使用 webpack-bundle-analyzer
构建完成后默认会在 8888 端口展示。可以看出依赖的第三方模块文件大小和业务组件代码的大小。之后有针对性地进行优化。
为了效果明显,注释之前的优化,比如:


4. 使用高版本的 webpack 和 Node.js

- V8 带来的优化(for of 替代 forEach、Map 和 Set 替代 Object、includes 替代 indexOf)
- 默认使用更快的 md4 hash 算法
- webpacks AST 可以直接从 loader 传递给 AST,减少解析时间
- 使用字符串方法替代正则表达式
webpack 5 现在虽然还是 beta 阶段,但估计快来了。所以这里不再多提 webpack 4 带来的变化,具体可以去查看 https://github.com/webpack/webpack/releases/tag/v4.0.0。
5. 多进程/多实例构建
HappyPack 作者不再维护,推荐官方的 thread-loader,安装并使用即可。thread-loader 可以将后面的 loader 放入 worker 池中运行。每个 worker 是一个单独的 node.js 进程,它的开销约为 600ms,进程间通信也有开销,所以仅在昂贵的操作中使用。
为了演示,可以将相关源码多复制几份,并且尝试不同的 workers 数量(显然不是越多越好)。
// webpack.prod.js{test: /\.js$/,use: [{loader: 'thread-loader',options: {// the number of spawned workers, defaults to (number of cpus - 1) or// fallback to 1 when require('os').cpus() is undefinedworkers: 1}},// your expensive loader (e.g babel-loader)'babel-loader','eslint-loader']}
6. 多进程并行压缩代码
使用多进程并行运行来提高构建速度,现在推荐使用 terser-webpack-plugin,这是 webpack 4 在 production 模式下默认会开启使用的 JS 压缩插件,相比以前的 uglifyjs-webpack-plugin 插件,terser-webpack-plugin 尤其支持压缩 ES6。
不过依然可以手动配置,从而覆盖默认策略。使用 terser-webpack-plugin:
module.exports = {optimization: {minimizer: [new TerserPlugin({parallel: true, // 默认是 false,开启多进程并行压缩,并且默认进程数是 os.cpus().length - 1// parallel: 4 // 也可以手动指定并行压缩时的进程数}),],},};
7. 进一步分包:预编译资源模块
使用 DllPlugin 和 DllReferencePlugin 提前拆分出 bundles 作为所谓的“动态链接库”。之后的业务构建就可以使用现成的 dll,提升了构建速度。
使用 DllPlugin 时需要创建单独的 webpack 配置文件,其中指定需要分离的包,比如框架基础包,业务基础包,只需要 entry 中继续指定 key 即可,比如 vendor,business,然后 output.library 字段中指定包最终的输出名称,最后使用 DllPlugin 指定 manifest.json 的输出路径,manifest.json 是对分离出的包的描述。
最终使用 dll 包时,使用 DllReferencePlugin 引用刚才产出的 manifest.json,会自动关联 dll 包,预编译了两个 dll 包的话,就使用两次 DllReferencePlugin。
webpack.dll.js:
const path = require('path');const webpack = require('webpack');const { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = {mode: 'production',entry: {vendor: ['react', 'react-dom']// 可以继续增加如业务公共包},output: {filename: '[name].[hash].dll.js',path: path.join(__dirname, 'dll-build/vendor'),library: '[name]_[hash]' // 与 output.library 的选项相结合可以暴露出 (也叫做放入全局域) dll 函数},plugins: [new CleanWebpackPlugin(),new webpack.DllPlugin({name: '[name]_[hash]', // 暴露出的 dll 的函数名path: path.join(__dirname, 'dll-build/vendor/[name]-manifest.json') // manifest json 文件的绝对路径 (输出文件)})]};
然后就可以进行 dll 的打包了:

webpack.prod.js 中使用 DllReferencePlugin 进行引用,并通过 AddAssetHtmlPlugin 向 html 中插入 dll 包:
// 以下省略了很多无关配置const path = require('path');const webpack = require('webpack');const HtmlWebpackPlugin = require('html-webpack-plugin');').default;const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');module.exports = {// 其他配置略plugins: [// HtmlWebpackPlugins 的配置略new webpack.DllReferencePlugin({manifest: require('./dll-build/vendor/vendor-manifest.json')}),// AddAssetHtmlPlugin 要在 HtmlWebpackPlugins 之后再使用,不然往哪插呢new AddAssetHtmlPlugin({ filepath: path.resolve('./dll-build/vendor/vendor.*.dll.js') })};
npm run build 之后可以看到成功引入了 dll 包,这里几乎不占用本次构建时间,因为是引入提前预编译的资源模块:


之前介绍的 splitChunks 也可以抽出公共包,HtmlWebpackTagsPlugin 也可以把公共资源 external 出去,由 CDN 的方式引入。那么假如三种方式意外共存时,优先级高低顺序是:external > dll > splitChunks。
最后不禁要问,dll 价值何在?更多的是工程角度:
如果项目使用了 Webpack4,确实对 dll 的依赖没那么大,使用 dll 相对来说提升也不是特别明显。而且有 hard-source-webpack-plugin 可以极大提升二次构建速度。不过从实际前端工程中来说, dll 还是很有必要掌握的。对于一个团队而言,基本是采用相同的技术栈,要么 React、要么Vue 等等。这个时候,通常的做法都是把公共框架打成一个 common bundle 文件供所有项目使用。比如我们团队会将 react、react-dom、redux、react-redux 等等打包成一个公共库。dll 可以很好的满足这种场景:将多个npm包打成一个公共包。因此团队里面的分包方案使用 dll 还是很有价值,常见的会从整个工程的角度分为基础包(react、redux等)、业务公共包(所有业务都要用到的监控上报脚本、页面初始化脚本)、某个业务的js。
