Webpack 优化构建性能
1. 分析构建性能
分析构建体积
-
全局安装
webpack-bundle-analyzer 插件npm i -g webpack-bundle-analyzer
-
运行
webpack-bundle-analyzer webpack-bundle-analyzer
分析构建速度
// webpack.config.js const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); const smp = new SpeedMeasurePlugin(); module.exports = smp.wrap(prodWebpackConfig)
2. 分析构建过程
-
开始打包,需要获取所有的依赖模块
搜索所有的依赖项,这需要占用一定的时间,即搜索时间,那么就确定了:
需要优化的第一个时间就是搜索时间。 -
解析所有的依赖模块(解析成浏览器可运行的代码)
Webpack 根据配置的 loader 解析相应的文件。日常开发中需要使用 loader 对 JS、CSS、图片、字体等文件做转换操作,并且转换的文件数据量也是非常大。由于 JS 单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。
需要优化的第二个时间就是解析时间。 -
将所有的依赖模块打包到一个文件
将所有解析完成的代码,打包到一个文件中,为了使浏览器加载的包更新(减小白屏时间),所以 Webpack 会对代码进行优化。 JS 压缩是发布编译的最后阶段,通常 Webpack 需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时,打包就容易卡住。
需要优化的第三个时间就是压缩时间。 -
二次打包
当更改项目中一个小小的文件时,需要重新打包,所有的文件都必须要重新打包,需要花费同初次打包相同的时间,但项目中大部分文件都没有变更,尤其是第三方库。
需要优化的第四个时间就是二次打包时间。
3. 优化构建性能
升级版本
使用最新版本的 Webpack 和 Node.js,因为高版本的 Webpack、Node.js 在内置的 API、算法上都更优。
优化搜索时间
缩小文件搜索范围
Webpack 打包时,会从配置的
- 根据导入语句去寻找对应的要导入的文件
- 根据找到的要导入文件的后缀,使用配置中的 loader 去处理文件
优化 Loader 配置
可以通过
test :配置要解析的文件类型include :配置要解析的文件exclude :配置不解析的文件
// webpack.config.js module.exports = { module: { rules: [ { // 匹配 js、mjs 文件 test: /.m?js$/, // 排除 node_modules、bower_components 目录下的文件搜索 exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] } }
优化 resolve.modules 配置
- 相对路径:如果当前目录下没有
node_modules ,则递归到父目录下查找,直到找到 - 绝对路径:只查找当前目录,不递归
// webpack.config.js module.exports = { resolve: { // 将 node_modules 目录下的文件视为 module modules: [path.resolve(__dirname, 'node_modules')], }, };
优化 resolve.alias 配置
// webpack.config.js const path = require('path'); module.exports = { resolve: { alias: { // 将当前目录下的 src 目录配置别名为 @ '@': path.resolve(__dirname, 'src'), }, }, };
减少不必要的编译工作
优化 resolve.extensions 配置
在导入语句没带文件后缀时,webpack 会根据
// webpack.config.js module.exports = { resolve: { // 在解析未带后缀的文件时,会按照 .js -> .css 的顺序进行查找 extensions: ['.js', '.css'], }, };
优化 resolve.mainFields 配置
有一些第三方模块会针对不同环境提供几份代码。 Webpack 会根据
// webpack.config.js module.exports = { resolve: { // 先采用 jsnext:main 的代码,再采用 main 的代码 mainFields: ['jsnext:main', 'main'], }, };
优化 module.noParse 配置
原因是一些库,例如:JQuery 、Lodash, 它们庞大又没有采用模块化标准,让 Webpack 去解析这些文件耗时又没有意义。
// webpack.config.js module.exports = { module: { // 忽略 jquery 和 lodash 库 noParse: /jquery|lodash/, // 第二种写法 noParse: (content) => /jquery|lodash/.test(content), }, };
优化 resolve.symlinks 配置
如果不使用 symlinks(例如:
// webpack.config.js module.exports = { resolve: { symlinks: false }, };
优化 resolve.cacheWithContext 配置
如果使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置
// webpack.config.js module.exports = { resolve: { cacheWithContext: false }, };
优化解析时间
多线程并行解析 - thread-Loader
-
安装
thread-loader npm i thread-loader
-
配置
webpack.config.js // webpack.config.js module.exports = { module: { rules: [ { test: /.js$/, include: path.resolve('src'), use: [ "thread-loader", // 一定要放在其他 loader 后面 ], }, ], }, };
使用资源模块替换 Loader
使用 Webpack 资源模块(asset module)代替旧的 Assets Loader(例如:
使用 HappyPack
HappyPack 能让 Webpack 多线程解析 Loader,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。
-
安装
happypack npm i -D happypack
-
配置
webpack.config.js
// webpack.config.js const path = require('path'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const HappyPack = require('happypack'); module.exports = { module: { rules: [ { test: /.js$/, // 把对 js 文件的处理转交给 id 为 babel 的 HappyPack 实例 use: ['happypack/loader?id=babel'], // 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换 exclude: path.resolve(__dirname, 'node_modules'), }, { test: /.css$/, use: ExtractTextPlugin.extract({ // 把对 css 文件的处理转交给 id 为 css 的 HappyPack 实例 use: ['happypack/loader?id=css'], }), }, ] }, plugins: [ new HappyPack({ // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件 id: 'babel', // 如何处理 js 文件,用法和 Loader 配置中一样 loaders: ['babel-loader?cacheDirectory'], }), new HappyPack({ id: 'css', loaders: ['css-loader'], }), new ExtractTextPlugin({ filename: `[name].css`, }), ], };
优化压缩时间
多线程压缩
Webpack v3
-
安装
webpack-parallel-uglify-plugin npm i -D webpack-parallel-uglify-plugin
-
配置
webpack.config.js // webpack.config.js const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); module.exports = { plugins: [ // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码 new ParallelUglifyPlugin({ // 传递给 UglifyJS 的参数 uglifyJS: { output: { // 最紧凑的输出 beautify: false, // 删除所有的注释 comments: false, }, compress: { // 在 UglifyJs 删除没有用到的代码时不输出警告 warnings: false, // 删除所有的 console 语句,可以兼容 ie 浏览器 drop_console: true, // 内嵌定义了但是只用到一次的变量 collapse_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值 reduce_vars: true, } }, }), ], };
Webpack v4
-
安装
terser-webpack-plugin npm i terser-webpack-plugin
-
配置
webpack.config.js // webpack.config.js const TerserPlugin = require("terser-webpack-plugin"); module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin({ parallel: 4, terserOptions: { parse: { ecma: 8, }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, }, mangle: { safari10: true, }, output: { ecma: 5, comments: false, ascii_only: true, }, }, }) ], }, };
减少压缩体积
压缩 CSS - CssMinimizerWebpackPlugin
// webpack.config.js const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); module.exports = { optimization: { minimizer: [ new CssMinimizerPlugin({ parallel: 4, }), ], } }
分包 - SplitChunks
// webpack.config.js module.exports = { optimization: { /** * 把 JS 文件打包成3中类型: * 1. vendor:第三方 lib 库,基本不会改动,除非依赖版本升级 * 2. common:业务组件代码的公共部分抽取出来,改动较少 * 3. entry.{page}:不同页面 entry 里业务组件代码的差异部分,会经常改动 * 这样分的好处是尽量按改动频率来区分,利用好浏览器缓存 */ splitChunks: { chunks: 'all', maxInitialRequests: 4, // 防止切分粒度过细,请求过多,约束为4 cacheGroups: { vendor: { // 第三方 test: /[\/]node_modules[\/]/, // 打包 node_module 中的文件 name: 'vendor', // 模块名称 priority: 10, // 优先级 数字越大 优先级越高 enforce: true, // 强制执行 reuseExistingChunk: true, // 重用已有模块 }, common: { // 公共模块 name: 'common', minChunks: 2, // 被引用两处即被归纳到公共模块 minSize: 1, // 最小 size priority: 5, // 优先级 reuseExistingChunk: true, // 重用已有模块 }, }, }, // 将 webpack 运行时生成代码打包到 runtime.js runtimeChunk: true, }, }
合并公共代码 - CommonsChunkPlugin
Webpack v4 移除,通过 optimization.splitChunks 替代。
// webpack.config.js const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); module.exports = { plugins: [ new CommonsChunkPlugin({ chunks: ['common', 'base'], // 从 common 和 base 两个现成的 Chunk 中提取公共的部分 name: 'base' // 把公共的部分放到 base 中 }) ] }
Tree Shaking
Tree Shaking 是指打包时移除哪些没有使用(未引入)的代码。
按需加载
-
代码中通过
import() 按需加载 Chunk// main.js window.document.getElementById('btn').addEventListener('click', function () { // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数 import(/* webpackChunkName: "show" */ './show').then((show) => { show('Webpack'); }) });
-
在
webpack.config.js 中配置动态 Chunk 的输出
// webpack.config.js module.exports = { entry: { main: './main.js', }, output: { filename: '[name].js', // 为动态加载的 Chunk 配置输出文件的名称 chunkFilename: '[name].js', } };
开启 Scope Hoisting
Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快, 它又译作 “作用域提升”,是在 Webpack3 中新推出的功能。
// webpack.config.js? ? const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');? ? module.exports = {? resolve: {? // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件? mainFields: ['jsnext:main', 'browser', 'main']? },? plugins: [? // 开启 Scope Hoisting? new ModuleConcatenationPlugin(),? ],? };
优化二次打包时间
缓存
增加初次构建时间,缩短后续构建时间。
- 利用
cache-loader ,HardSourceWebpackPlugin 、babel-loader 的cacheDirectory 标志等。 - cache
// webpack.config.js module.exports = { cache: { type: 'filesystem', }, };
静态资源不再打包
DllPlugin
// webpack_dll.config.js - 构建出动态链接库文件 const path = require('path'); const DllPlugin = require('webpack/lib/DllPlugin'); const DllReferencePlugin = require('webpack/lib/DllReferencePlugin'); module.exports = { entry: { // 把 React 相关模块的放到一个单独的动态链接库 react: ['react', 'react-dom'], // 把项目需要所有的 polyfill 放到一个单独的动态链接库 polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch'], }, output: { filename: '[name].dll.js', path: path.resolve(__dirname, 'dist'), // 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react // 之所以在前面加上 _dll_ 是为了防止全局变量冲突 library: '_dll_[name]', }, plugins: [ // 接入 DllPluginnew DllPlugin({ // 动态链接库的全局变量名称,需要和 output.library 中保持一致 // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值 // 例如 react.manifest.json 中就有 "name": "_dll_react" name: '_dll_[name]', // 描述动态链接库的 manifest.json 文件输出时的文件名称 path: path.join(__dirname, 'dist', '[name].manifest.json'), }), ], };
// webpack.config.js const path = require('path'); const DllReferencePlugin = require('webpack/lib/DllReferencePlugin'); module.exports = { plugins: [ // 告诉 Webpack 使用了哪些动态链接库 new DllReferencePlugin({ // 描述 react 动态链接库的文件内容 manifest: require('./dist/react.manifest.json'), }), new DllReferencePlugin({ // 描述 polyfill 动态链接库的文件内容 manifest: require('./dist/polyfill.manifest.json'), }), ], };
合理配置输出文件名
// webpack.config.js module.exports = { output: { path: path.resolve(__dirname, '../dist'), // 给 js 文件加上 contenthash filename: 'js/chunk-[contenthash].js', clean: true, }, }
区分环境
- 在开发环境中,切忌在开发环境使用生产环境才会用到的工具,例如:在开发环境下,应该排除
[fullhash]/[chunkhash]/[contenthash] 等工具。 - 在生产环境中,也应该避免使用开发环境才会用到的工具,例如:
webpack-dev-server 等插件。
4. 完整优化配置
开发环境的配置文件 webpack.dev.config.js
// webpack.dev.config.js const path = require('path'); const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); const {AutoWebPlugin} = require('web-webpack-plugin'); const HappyPack = require('happypack'); // 自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用const autoWebPlugin = new AutoWebPlugin('./src/pages', { // HTML 模版文件所在的文件路径 template: './template.html', // 提取出所有页面公共的代码 commonsChunk: { // 提取出公共代码 Chunk 的名称 name: 'common', }, }); module.exports = { // AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置,// autoWebPlugin.entry 方法可以获取到生成入口配置 entry: autoWebPlugin.entry({ // 这里可以加入你额外需要的 Chunk 入口 base: './src/base.js', }), output: { filename: '[name].js', }, resolve: { // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤// 其中 __dirname 表示当前工作目录,也就是项目根目录 modules: [path.resolve(__dirname, 'node_modules')], // 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件,使用 Tree Shaking 优化// 只采用 main 字段作为入口文件描述字段,以减少搜索步骤 mainFields: ['jsnext:main', 'main'], }, module: { rules: [ { // 如果项目源码中只有 js 文件就不要写成 /.jsx?$/,提升正则表达式性能 test: /.js$/, // 使用 HappyPack 加速构建 use: ['happypack/loader?id=babel'], // 只对项目根目录下的 src 目录中的文件采用 babel-loader include: path.resolve(__dirname, 'src'), }, { test: /.js$/, use: ['happypack/loader?id=ui-component'], include: path.resolve(__dirname, 'src'), }, { // 增加对 CSS 文件的支持 test: /.css$/, use: ['happypack/loader?id=css'], }, ] }, plugins: [ autoWebPlugin, // 使用 HappyPack 加速构建new HappyPack({ id: 'babel', // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启 loaders: ['babel-loader?cacheDirectory'], }), new HappyPack({ // UI 组件加载拆分 id: 'ui-component', loaders: [{ loader: 'ui-component-loader', options: { lib: 'antd', style: 'style/index.css', camel2: '-' } }], }), new HappyPack({ id: 'css', // 如何处理 .css 文件,用法和 Loader 配置中一样 loaders: ['style-loader', 'css-loader'], }), // 提取公共代码new CommonsChunkPlugin({ // 从 common 和 base 两个现成的 Chunk 中提取公共的部分 chunks: ['common', 'base'], // 把公共的部分放到 base 中 name: 'base' }), ], watchOptions: { // 使用自动刷新:不监听的 node_modules 目录下的文件 ignored: /node_modules/, } };
生产环境的配置文件 webpack.prod.config.js
// webpack.prod.config.js const path = require('path'); const DefinePlugin = require('webpack/lib/DefinePlugin'); const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const { AutoWebPlugin } = require('web-webpack-plugin'); const HappyPack = require('happypack'); const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin'); // 自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用 const autoWebPlugin = new AutoWebPlugin('./src/pages', { // HTML 模版文件所在的文件路径 template: './template.html', // 提取出所有页面公共的代码 commonsChunk: { // 提取出公共代码 Chunk 的名称 name: 'common', }, // 指定存放 CSS 文件的 CDN 目录 URL stylePublicPath: '//css.cdn.com/id/', }); module.exports = { // AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置, // autoWebPlugin.entry 方法可以获取到生成入口配置 entry: autoWebPlugin.entry({ // 这里可以加入你额外需要的 Chunk 入口 base: './src/base.js', }), output: { // 给输出的文件名称加上 Hash 值 filename: '[name]_[chunkhash:8].js', path: path.resolve(__dirname, './dist'), // 指定存放 JavaScript 文件的 CDN 目录 URL publicPath: '//js.cdn.com/id/', }, resolve: { // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤 // 其中 __dirname 表示当前工作目录,也就是项目根目录 modules: [path.resolve(__dirname, 'node_modules')], // 只采用 main 字段作为入口文件描述字段,以减少搜索步骤 mainFields: ['jsnext:main', 'main'], }, module: { rules: [ { // 如果项目源码中只有 js 文件就不要写成 /.jsx?$/,提升正则表达式性能 test: /.js$/, // 使用 HappyPack 加速构建 use: ['happypack/loader?id=babel'], // 只对项目根目录下的 src 目录中的文件采用 babel-loader include: path.resolve(__dirname, 'src'), }, { test: /.js$/, use: ['happypack/loader?id=ui-component'], include: path.resolve(__dirname, 'src'), }, { // 增加对 CSS 文件的支持 test: /.css$/, // 提取出 Chunk 中的 CSS 代码到单独的文件中 use: ExtractTextPlugin.extract({ use: ['happypack/loader?id=css'], // 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL publicPath: '//img.cdn.com/id/' }), }, ] }, plugins: [ autoWebPlugin, // 开启ScopeHoisting new ModuleConcatenationPlugin(), // 使用HappyPack new HappyPack({ // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件 id: 'babel', // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启 loaders: ['babel-loader?cacheDirectory'], }), new HappyPack({ // UI 组件加载拆分 id: 'ui-component', loaders: [{ loader: 'ui-component-loader', options: { lib: 'antd', style: 'style/index.css', camel2: '-' } }], }), new HappyPack({ id: 'css', // 如何处理 .css 文件,用法和 Loader 配置中一样 // 通过 minimize 选项压缩 CSS 代码 loaders: ['css-loader?minimize'], }), new ExtractTextPlugin({ // 给输出的 CSS 文件名称加上 Hash 值 filename: `[name]_[contenthash:8].css`, }), // 提取公共代码 new CommonsChunkPlugin({ // 从 common 和 base 两个现成的 Chunk 中提取公共的部分 chunks: ['common', 'base'], // 把公共的部分放到 base 中 name: 'base' }), new DefinePlugin({ // 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才需要的部分 'process.env': { NODE_ENV: JSON.stringify('production') } }), // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码 new ParallelUglifyPlugin({ // 传递给 UglifyJS 的参数 uglifyJS: { output: { // 最紧凑的输出 beautify: false, // 删除所有的注释 comments: false, }, compress: { // 在 UglifyJs 删除没有用到的代码时不输出警告 warnings: false, // 删除所有的 `console` 语句,可以兼容ie浏览器 drop_console: true, // 内嵌定义了但是只用到一次的变量 collapse_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值 reduce_vars: true, } }, }), ] };