带你深度解锁Webpack系列

前两篇文章中,主要是讲解了 Webpack 的配置,但是随着项目越来越大,构建速度可能会越来越慢,构建出来的js的体积也越来越大,此时就需要对 Webpack 的配置进行优化。
本文罗列出了十多种优化方式,大家可以结合自己的项目,选择适当的方式进行优化。这些 Webpack 插件的源码我大多也没有看过,主要是结合 Webpack 官方文档以及项目实践,并且花了大量的时间验证后输出了本文,如果文中有错误的地方,欢迎在评论区指正。
鉴于前端技术变更迅速,祭出本篇文章基于 Webpack 的版本号:
├── [email protected]
└── [email protected]
复制代码本文对应的项目地址(编写本文时使用)供参考:github.com/YvetteLau/w…

量化
有时,我们以为的优化是负优化,这时,如果有一个量化的指标可以看出前后对比,那将会是再好不过的一件事。
speed-measure-webpack-plugin 插件可以测量各个插件和loader所花费的时间,使用之后,构建时,会得到类似下面这样的信息:

对比前后的信息,来确定优化的效果。
speed-measure-webpack-plugin 的使用很简单,可以直接用其来包裹 Webpack 的配置:
//webpack.config.js
const SpeedMeasurePlugin = require(“speed-measure-webpack-plugin”);
const smp = new SpeedMeasurePlugin();

const config = {
//…webpack配置
}

module.exports = smp.wrap(config);
复制代码1.exclude/include
我们可以通过 exclude、include 配置来确保转译尽可能少的文件。顾名思义,exclude 指定要排除的文件,include 指定要包含的文件。
exclude 的优先级高于 include,在 include 和 exclude 中使用绝对路径数组,尽量避免 exclude,更倾向于使用 include。
//webpack.config.js
const path = require(‘path’);
module.exports = {
//…
module: {
rules: [
{
test: /.js[x]?$/,
use: [‘babel-loader’],
include: [path.resolve(__dirname, ‘src’)]
}
]
},
}
复制代码下图是我未配置 include 和配置了 include 的构建结果对比:

  1. cache-loader
    在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。
    首先安装依赖:
    npm install cache-loader -D
    复制代码cache-loader 的配置很简单,放在其他 loader 之前即可。修改Webpack 的配置如下:
    module.exports = {
    //…

    module: {
    //我的项目中,babel-loader耗时比较长,所以我给它配置了cache-loader
    rules: [
    {
    test: /.jsx?$/,
    use: [‘cache-loader’,‘babel-loader’]
    }
    ]
    }
    }
    复制代码如果你跟我一样,只打算给 babel-loader 配置 cache 的话,也可以不使用 cache-loader,给 babel-loader 增加选项 cacheDirectory。

cacheDirectory:默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 Webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程。设置空值或者 true 的话,使用默认缓存目录:node_modules/.cache/babel-loader。开启 babel-loader的缓存和配置 cache-loader,我比对了下,构建时间很接近。
3.happypack
由于有大量文件需要解析和处理,构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack 构建慢的问题会显得严重。文件读写和计算操作是无法避免的,那能不能让 Webpack 同一时刻处理多个任务,发挥多核 CPU 电脑的威力,以提升构建速度呢?
HappyPack 就能让 Webpack 做到这点,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。
首先需要安装 happypack:
npm install happypack -D
复制代码修改配置文件:
const Happypack = require(‘happypack’);
module.exports = {
//…
module: {
rules: [
{
test: /.js[x]?KaTeX parse error: Expected group after '_' at position 90: … [path.resolve(_̲_dirname, 'src'…/,
use: ‘Happypack/loader?id=css’,
include: [
path.resolve(__dirname, ‘src’),
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’],
})
]
}
复制代码happypack 默认开启 CPU核数 - 1 个进程,当然,我们也可以传递 threads 给 Happypack。

说明:当 postcss-loader 配置在 Happypack 中,必须要在项目中创建 postcss.config.js。
//postcss.config.js
module.exports = {
plugins: [
require(‘autoprefixer’)()
]
}
复制代码否则,会抛出错误: Error: No PostCSS Config found
另外,当你的项目不是很复杂时,不需要配置 happypack,因为进程的分配和管理也需要时间,并不能有效提升构建速度,甚至会变慢。
4.thread-loader
除了使用 Happypack 外,我们也可以使用 thread-loader ,把 thread-loader 放置在其它 loader 之前,那么放置在这个 loader 之后的 loader 就会在一个单独的 worker 池中运行。
在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:

这些 loader 不能产生新的文件。
这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
这些 loader 无法获取 webpack 的选项设置。

首先安装依赖:
npm install thread-loader -D
复制代码修改配置:
module.exports = {
module: {
//我的项目中,babel-loader耗时比较长,所以我给它配置 thread-loader
rules: [
{
test: /.jsx?KaTeX parse error: Expected 'EOF', got '}' at position 87: …'] }̲ ] …/, /moment$/)
]
}
复制代码在使用的时候,如果我们需要指定语言,那么需要我们手动的去引入语言包,例如,引入中文语言包:
import moment from ‘moment’;
import ‘moment/locale/zh-cn’;// 手动引入
复制代码index.js 中只引入 moment,打包出来的 bundle.js 大小为 263KB,如果配置了 IgnorePlugin,单独引入 moment/locale/zh-cn,构建出来的包大小为 55KB。
10.externals
我们可以将一些JS文件存储在 CDN 上(减少 Webpack打包出来的 js 体积),在 index.html 中通过

Document
root
复制代码我们希望在使用时,仍然可以通过 import 的方式去引用(如 import $ from 'jquery'),并且希望 webpack 不会对其进行打包,此时就可以配置 externals。 //webpack.config.js module.exports = { //... externals: { //jquery通过script引入之后,全局中即有了 jQuery 变量 'jquery': 'jQuery' } } 复制代码11.DllPlugin 有些时候,如果所有的JS文件都打成一个JS文件,会导致最终生成的JS文件很大,这个时候,我们就要考虑拆分 bundles。 DllPlugin 和 DLLReferencePlugin 可以实现拆分 bundles,并且可以大大提升构建速度,DllPlugin 和 DLLReferencePlugin 都是 webpack 的内置模块。 我们使用 DllPlugin 将不会频繁更新的库进行编译,当这些依赖的版本没有变化时,就不需要重新编译。我们新建一个 webpack 的配置文件,来专门用于编译动态链接库,例如名为: webpack.config.dll.js,这里我们将 react 和 react-dom 单独打包成一个动态链接库。 //webpack.config.dll.js const webpack = require('webpack'); const path = require('path');

module.exports = {
entry: {
react: [‘react’, ‘react-dom’]
},
mode: ‘production’,
output: {
filename: ‘[name].dll.[hash:6].js’,
path: path.resolve(__dirname, ‘dist’, ‘dll’),
library: ‘[name]_dll’ //暴露给外部使用
//libraryTarget 指定如何暴露内容,缺省时就是 var
},
plugins: [
new webpack.DllPlugin({
//name和library一致
name: ‘[name]_dll’,
path: path.resolve(__dirname, ‘dist’, ‘dll’, ‘manifest.json’) //manifest.json的生成路径
})
]
}
复制代码在 package.json 的 scripts 中增加:
{
“scripts”: {
“dev”: “NODE_ENV=development webpack-dev-server”,
“build”: “NODE_ENV=production webpack”,
“build:dll”: “webpack --config webpack.config.dll.js”
},
}
复制代码执行 npm run build:all,可以看到 dist 目录如下,之所以将动态链接库单独放在 dll 目录下,主要是为了使用 CleanWebpackPlugin 更为方便的过滤掉动态链接库。
dist
└── dll
├── manifest.json
└── react.dll.9dcd9d.js
复制代码manifest.json 用于让 DLLReferencePlugin 映射到相关依赖上。
修改 webpack 的主配置文件: webpack.config.js 的配置:
//webpack.config.js
const webpack = require(‘webpack’);
const path = require(‘path’);
module.exports = {
//…
devServer: {
contentBase: path.resolve(__dirname, ‘dist’)
},
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, ‘dist’, ‘dll’, ‘manifest.json’)
}),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [’/*’, ‘!dll’, '!dll/’] //不删除dll目录
}),
//…
]
}
复制代码使用 npm run build 构建,可以看到 bundle.js 的体积大大减少。
修改 public/index.html 文件,在其中引入 react.dll.js

复制代码
构建速度

包体积

12.抽离公共代码
抽离公共代码是对于多页应用来说的,如果多个页面引入了一些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来了,避免了重复下载。
抽离公共代码对于单页应用和多页应该在配置上没有什么区别,都是配置在 optimization.splitChunks 中。
//webpack.config.js
module.exports = {
optimization: {
splitChunks: {//分割代码块
cacheGroups: {
vendor: {
//第三方依赖
priority: 1, //设置优先级,首先抽离第三方模块
name: ‘vendor’,
test: /node_modules/,
chunks: ‘initial’,
minSize: 0,
minChunks: 1 //最少引入了1次
},
//缓存组
common: {
//公共模块
chunks: ‘initial’,
name: ‘common’,
minSize: 100, //大小超过100个字节
minChunks: 3 //最少引入了3次
}
}
}
}
}
复制代码即使是单页应用,同样可以使用这个配置,例如,打包出来的 bundle.js 体积过大,我们可以将一些依赖打包成动态链接库,然后将剩下的第三方依赖拆出来。这样可以有效减小 bundle.js 的体积大小。当然,你还可以继续提取业务代码的公共模块,此处,因为我项目中源码较少,所以没有配置。

runtimeChunk

runtimeChunk 的作用是将包含 chunk 映射关系的列表从 main.js 中抽离出来,在配置了 splitChunk 时,记得配置 runtimeChunk.
module.exports = {
//…
optimization: {
runtimeChunk: {
name: ‘manifest’
}
}
}
复制代码最终构建出来的文件中会生成一个 manifest.js。
借助 webpack-bundle-analyzer 进一步优化
在做 webpack 构建优化的时候,vendor 打出来超过了1M,react 和 react-dom 已经打包成了DLL。
因此需要借助 webpack-bundle-analyzer 查看一下是哪些包的体积较大。
首先安装依赖:
npm install webpack-bundle-analyzer -D
复制代码使用也很简单,修改下我们的配置:
//webpack.config.prod.js
const BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPlugin;
const merge = require(‘webpack-merge’);
const baseWebpackConfig = require(’./webpack.config.base’);
module.exports = merge(baseWebpackConfig, {
//…
plugins: [
//…
new BundleAnalyzerPlugin(),
]
})
复制代码npm run build 构建,会默认打开: http://127.0.0.1:8888/,可以看到各个包的体积:

进一步对 vendor 进行拆分,将 vendor 拆分成了4个(使用 splitChunks 进行拆分即可)。
module.exports = {
optimization: {
concatenateModules: false,
splitChunks: {//分割代码块
maxInitialRequests:6, //默认是5
cacheGroups: {
vendor: {
//第三方依赖
priority: 1,
name: ‘vendor’,
test: /node_modules/,
chunks: ‘initial’,
minSize: 100,
minChunks: 1 //重复引入了几次
},
‘lottie-web’: {
name: “lottie-web”, // 单独将 react-lottie 拆包
priority: 5, // 权重需大于vendor
test: /[/]node_modules[/]lottie-web[/]/,
chunks: ‘initial’,
minSize: 100,
minChunks: 1 //重复引入了几次
},
//…
}
},
},
}
复制代码重新构建,结果如下所示:

13.webpack自身的优化
tree-shaking
如果使用ES6的import 语法,那么在生产环境下,会自动移除没有使用到的代码。
//math.js
const add = (a, b) => {
console.log(‘aaaaaa’)
return a + b;
}

const minus = (a, b) => {
console.log(‘bbbbbb’)
return a - b;
}

export {
add,
minus
}
复制代码//index.js
import {add, minus} from ‘./math’;
add(2,3);
复制代码构建的最终代码里,minus 函数不会被打包进去。
scope hosting 作用域提升
变量提升,可以减少一些变量声明。在生产环境下,默认开启。
另外,大家测试的时候注意一下,speed-measure-webpack-plugin 和 HotModuleReplacementPlugin 不能同时使用,否则会报错:

babel 配置的优化
如果你对 babel 还不太熟悉的话,那么可以阅读这篇文章:不容错过的 Babel7 知识。
在不配置 @babel/plugin-transform-runtime 时,babel 会使用很小的辅助函数来实现类似 _createClass 等公共方法。默认情况下,它将被注入(inject)到需要它的每个文件中。但是这样的结果就是导致构建出来的JS体积变大。
我们也并不需要在每个 js 中注入辅助函数,因此我们可以使用 @babel/plugin-transform-runtime,@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序,以节省代码大小的插件。
因此我们可以在 .babelrc 中增加 @babel/plugin-transform-runtime 的配置。
{
“presets”: [],
“plugins”: [
[
“@babel/plugin-transform-runtime”
]
]
}
复制代码以上就是我目前为止使用到的一些优化,如果你有更好的优化方式,欢迎在评论区留言,感谢阅读。

发布了124 篇原创文章 · 获赞 10 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/a59612/article/details/104894068