跳到主要内容

Webpack 优化策略

· 阅读需 14 分钟

速度分析

webpack 有时候打包很慢,而我们在项目中可能用了很多的 pluginloader,想知道到底是哪个环节慢,下面这个插件可以计算 pluginloader 的耗时。

yarn add -D speed-measure-webpack-plugin

配置

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')

const smp = new SpeedMeasurePlugin()

const webpackConfig = smp.wrap({
plugins: [new MyPlugin(), new MyOtherPlugin()]
})

这个插件主要做了两件事情:

  • 计算整个打包总耗时
  • 分析每个插件和 loader 的耗时情况

体积分析

yarn add -D webpack-bundle-analyzer

配置

const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
plugins: [
new BundleAnalyzerPlugin({
// 可以是`server`,`static`或`disabled`。
// 在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。
// 在“静态”模式下,会生成带有报告的单个HTML文件。
// 在`disabled`模式下,你可以使用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。
analyzerMode: 'server',
// 将在“服务器”模式下使用的主机启动HTTP服务器。
analyzerHost: '127.0.0.1',
// 将在“服务器”模式下使用的端口启动HTTP服务器。
analyzerPort: 8866,
// 路径捆绑,将在`static`模式下生成的报告文件。
// 相对于捆绑输出目录。
reportFilename: 'report.html',
// 模块大小默认显示在报告中。
// 应该是`stat`,`parsed`或者`gzip`中的一个。
// 有关更多信息,请参见“定义”一节。
defaultSizes: 'parsed',
// 在默认浏览器中自动打开报告
openAnalyzer: true,
// 如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成
generateStatsFile: false,
// 如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。
// 相对于捆绑输出目录。
statsFilename: 'stats.json',
// stats.toJson()方法的选项。
// 例如,您可以使用`source:false`选项排除统计文件中模块的来源。
// 在这里查看更多选项:https: //github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
statsOptions: null,
logLevel: 'info'
})
]
}

多进程

webpack 是运行在 node 环境中,而 node 是单线程的。webpack 的打包过程是 io 密集和计算密集型的操作,如果能同时 fork 多个进程并行处理各个任务,将会有效的缩短构建时间。

最常用的是thread-loaderHappyPack ,其中thread-loader吧,这个也是webpack4官方所推荐的。

yarn add -D thread-loader

配置

module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
'thread-loader'
// your expensive loader (e.g babel-loader)
]
}
]
}
}

thread-loader  会将你的  loader  放置在一个  worker  池里面运行,以达到多线程构建。

thread-loaderhappypack 对于小型项目来说打包速度几乎没有影响,甚至可能会增加开销,所以建议尽量在大项目中采用。

多进程并行压缩代码

通常我们在开发环境,代码构建时间比较快,而构建用于发布到线上的代码时会添加压缩代码这一流程,则会导致计算量大耗时多。

webpack默认提供了UglifyJS插件来压缩JS代码,但是它使用的是单线程压缩代码,也就是说多个 js 文件需要被压缩,它需要一个个文件进行压缩。所以说在正式环境打包压缩代码速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,再应用各种规则分析和处理AST,导致这个过程耗时非常大)。

常用的做法就是多进程并行压缩

  • parallel-uglify-plugin
  • uglifyjs-webpack-plugin
  • terser-webpack-plugin

parallel-uglify-plugin

webpack有多个JS文件需要输出和压缩时,原来会使用UglifyJS去一个个压缩并且输出,而ParallelUglifyPlugin插件则会开启多个子进程,把对多个文件压缩的工作分给多个子进程去完成,但是每个子进程还是通过UglifyJS去压缩代码。并行压缩可以显著的提升效率。

uglifyjs-webpack-plugin

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
plugins: [
new UglifyJsPlugin({
uglifyOptions: {
warnings: false,
parse: {},
compress: {},
ie8: false
},
parallel: true
})
]
}

设置parallel: true开启多进程压缩

terser-webpack-plugin

webpack4  已经默认支持  ES6语法的压缩。

而这离不开terser-webpack-plugin

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

module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: 4
})
]
}
}

Externals

将一些JS文件存储在 CDN 上,在 index.html 中通过 <script> 标签引入

使用import方式导入且webpack不会打包

//webpack.config.js
module.exports = {
//...
externals: {
//jquery通过script引入之后,全局中即有了 jQuery 变量
jquery: 'jQuery'
}
}

预编译资源模块

在使用webpack进行打包时候,对于依赖的第三方库,比如vuevuex等这些不会修改的依赖,我们可以让它和我们自己编写的代码分开打包,这样做的好处是每次更改我本地代码的文件的时候,webpack只需要打包我项目本身的文件代码,而不会再去编译第三方库。

那么第三方库在第一次打包的时候只打包一次,以后只要我们不升级第三方包的时候,那么webpack就不会对这些库去打包,这样的可以快速的提高打包的速度。其实也就是预编译资源模块

webpack中,我们可以结合DllPlugin  和  DllReferencePlugin插件来实现。

DllPlugin

DllPlugin能把第三方库代码分离开,并且每次文件更改的时候,它只会打包该项目自身的代码。所以打包速度会更快。

DLLPlugin  插件是在一个额外独立的webpack设置中创建一个只有dllbundle,也就是说我们在项目根目录下除了有webpack.config.js,还会新建一个webpack.dll.js文件。

webpack.dll.js的作用是把所有的第三方库依赖打包到一个bundledll文件里面,还会生成一个名为  manifest.json文件。该manifest.json的作用是用来让  DllReferencePlugin  映射到相关的依赖上去的。

DllReferencePlugin

什么意思呢?就是说在webpack.dll.js中打包后比如会生成 vendor.dll.js文件和vendor-manifest.json文件,vendor.dll.js文件包含了所有的第三方库文件,vendor-manifest.json文件会包含所有库代码的一个索引,当在使用webpack.config.js文件打包DllReferencePlugin插件的时候,会使用该DllReferencePlugin插件读取vendor-manifest.json文件,看看是否有该第三方库

使用方法:

使用DllPlugin配置一个webpack_dll.config.js来构建 dll 文件:

// webpack_dll.config.js
const path = require('path')
const DllPlugin = require('webpack/lib/DllPlugin')
module.exports = {
entry: {
react: ['react', 'react-dom'],
polyfill: ['core-js/fn/promise', 'whatwg-fetch']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, 'dist'),
library: '_dll_[name]' //dll的全局变量名
},
plugins: [
new DllPlugin({
name: '_dll_[name]', //dll的全局变量名
path: path.join(__dirname, 'dist', '[name].manifest.json') //描述生成的manifest文件
})
]
}

需要注意DllPlugin的参数中name值必须和output.library值保持一致,并且生成的manifest文件中会引用output.library值。

最终构建出的文件:

|-- polyfill.dll.js
|-- polyfill.manifest.json
|-- react.dll.js
└── react.manifest.json

其中xx.dll.js包含打包的 n 多模块,这些模块存在一个数组里,并以数组索引作为 ID,通过一个变量假设为_xx_dll暴露在全局中,可以通过window._xx_dll访问这些模块。xx.manifest.json文件描述 dll 文件包含哪些模块、每个模块的路径和 ID。然后再在项目的主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件

引入xx.manifest.json文件

//webpack.config.json
const path = require('path')
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin')
module.exports = {
entry: { main: './main.js' },
//... 省略output、loader等的配置
plugins: [
new DllReferencePlugin({
manifest: require('./dist/react.manifest.json')
}),
new DllReferenctPlugin({
manifest: require('./dist/polyfill.manifest.json')
})
]
}

最终构建生成 main.js

利用缓存提升二次构建速度

webpack中利用缓存一般有以下几种思路:

  • babel-loader开启缓存
  • 使用cache-loader
  • 使用hard-source-webpack-plugin

babel-loader

babel-loader在执行的时候,可能会产生一些运行期间重复的公共文件,造成代码体积冗余,同时也会减慢编译效率。

可以加上cacheDirectory参数开启缓存

{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: "babel-loader",
options: {
cacheDirectory: true
}
}],
},

cache-loader

在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里。

hard-source-webpack-plugin

HardSourceWebpackPlugin  为模块提供了中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source

配置  hard-source-webpack-plugin后,首次构建时间并不会有太大的变化,但是从第二次开始,构建时间大约可以减少  80%左右。

安装

yarn add -D hard-source-webpack-plugin
//
// webpack.config.js
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
entry: // ...
output: // ...
plugins: [
new HardSourceWebpackPlugin()
]
}

缩小构建目标/减少文件搜索范围

// webpack.config.js
const path = require('path');
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
// include: path.resolve('src'),
use: ['babel-loader']
}
]
}

babel-loader就会排除对node_modules下对应 js 的解析,提升构建速度

// webpack.config.js
const path = require('path');
module.exports = {
...
resolve: {
alias: {
react: path.resolve(__dirname, './node_modules/react/umd/react.production.min.js')
}, //直接指定react搜索模块,不设置默认会一层层的搜寻
modules: [path.resolve(__dirname, 'node_modules')], //限定模块路径
extensions: ['.js'], //限定文件扩展名
mainFields: ['main'] //限定模块入口文件名
  • resolve.modules:告诉  webpack  解析模块时应该搜索的目录
  • resolve.mainFields:当从  npm  包中导入模块时(例如,import * as React from 'react'),此选项将决定在  package.json  中使用哪个字段导入模块。根据  webpack  配置中指定的  target  不同,默认值也会有所不同
  • resolve.mainFiles:解析目录时要使用的文件名,默认是index
  • resolve.extensions:文件扩展名

动态 Polyfill 服务

babel只负责语法转换,比如将ES6的语法转换成ES5。但如果有些对象、方法,浏览器本身不支持,比如:

  • 全局对象:PromiseWeakMap  等。
  • 全局静态函数:Array.fromObject.assign  等。
  • 实例方法:比如  Array.prototype.includes  等。

此时,需要引入babel-polyfill来模拟实现这些对象、方法。

使用

module.exports = {
entry: ['@babel/polyfill', './app/js']
}

由于是一次性全部导入整个polyfill,所以导致了一个大问题:文件很大。为了解决这个问题,引入动态polyfill

每次打开页面,浏览器都会向Polyfill Service发送请求,Polyfill Service识别 User Agent,下发不同的 Polyfill,做到按需加载Polyfill的效果

采用官方提供的服务地址 https://polyfill.io/v3/polyfill.min.js

Scope Hoisting

Scope hoisting  直译过来就是「作用域提升」。熟悉  JavaScript  都应该知道「函数提升」和「变量提升」,JavaScript  会把函数和变量声明提升到当前作用域的顶部。「作用域提升」也类似于此,webpack  会把引入的  js  文件“提升到”它的引入者顶部。

Scope Hoisting  可以让  Webpack  打包出来的代码文件更小、运行的更快。

要在 Webpack 中使用 Scope Hoisting 非常简单,因为这是 Webpack 内置的功能,只需要配置一个插件,相关代码如下:

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

module.exports = (mode) => {
if (mode === 'production') {
return {}
}

return {
devtool: 'source-map',
plugins: [new webpack.optimize.ModuleConcatenationPlugin()]
}
}