一、提纲
webpack优化构建速度——提升开发体验和效率
一、可用于生产环境:
1)优化babel-loader; (减小打包模块入手)
2)IgnorePlugin; (减小打包模块入手)
3)noParse; (减小打包模块入手)
4)happyPack;(减少打包时间入手)
5)ParalleUglifyPlugin; (减少打包时间入手)
二、不用于生产环境,用于开发环境:
1)自动刷新;(提升开发体验)
2)热更新; (提升开发体验)
3)使用 webpack.DllPlugin 来预先编译(减小打包模块入手)
二、具体介绍
1.1 优化babel-loader
{
test: /.js$/,
use: ['babel-loader?cacheDirectory'], // 开启缓存
include: path.resolve(__dirname, 'src')// 明确范围
// exclude: path.resolve(__dirname, 'node_modules') // 排除范围,
// include和excude二选一即可
}
1.2 用IgnorePlugin忽略无用文件
避免引入无用模块
IgnorePlugin 是一个 webpack 内置的插件,可以直接使用webpack.IgnorePlugin 来获取。
这个插件用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去。例如我们使用 moment.js,直接引用后,里边有大量的 i18n 的代码,导致最后打包出来的文件比较大,而实际场景并不需要这些 i18n 的代码,这时我们可以使用 IgnorePlugin 来忽略掉这些代码文件,配置如下:
module.exports = {
// ...
plugins: [
new webpack.IgnorePlugin(/^./locale$/, /moment$/)
// 配置的参数第一个是匹配引入模块路径的正则表达式,第二个是匹配模块的对应上下文即所在目录名。
]
}
例如:将第三方库的全部引入改为手动按需引入
import moment from 'moment'
moment.locale('zh-cn') // 设置语言为中文
改为:
// index.js 文件
import moment from 'moment'
import 'moment/locale/zh-cn' // 手动引入中文语言包
moment.locale('zh-cn') // 设置语言为中文
// webpack.prod.js
module.exports = smart(webpackCommonConf, {
plugins: [
// 忽略 moment 下的 /locale 目录
new webpack.IgnorePlugin(/./locale/, /moment/),
]
}
1.3 noParse避免重复打包
防止 webpack 解析那些任何与给定正则表达式相匹配的文件。对于一些不需要解析依赖(即无依赖)的第三方大型类库等,可以通过这个字段来配置,以提高整体的构建速度。module.noParse可以告诉Webpack忽略未采用模块系统文件的处理,可以有效地提高性能。比如常见的jQuery非常大,又没有采用模块系统,让Webpack解析这类型文件完全是浪费性能。
module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能提高构建性能。注:这里一定要确定被排除出去的模块代码中不能包含import、require、define等内容,以保证 webpack 的打包包含了所有的模块,不然会导致打包出来的 js 因为缺少模块而报错。
module.exports = {
module: {
noParse: [/react.min.js$/], // 完整的`react.min.js`文件就没有采用模块化
// 忽略对`react.min.js`文件的递归解析处理
},
}
noParse与 IgnorePlugin区别:
IgnorePlugin直接不引入,代码里没有;
noParse引入,但不打包(不进行编译不进行模块化分析)。
1.4 happyPack
运行在node.js之上的Webpack是单线程模型的,也就是说Webpack需要处理的任务需要一件件挨着做,无法多个时间并行,当有大量文件需要读写和处理时尤其是当文件数量变多后,Webpack构建慢的问题就会尤为严重。由于JavaScript是单线程模型,要想发挥多核CPU的能力只能通过多进程去实现,而无法通过多线程实现。HappyPack能发挥多核CPU电脑的性能优势让Webpack同一时刻处理多个任务,它把任务分解成多个子进程去并发执行,子进程处理完毕后再将结果发送给主进程。
// webpack.config.js
const os = require('os');
const HappyPack = require('happypack');
// 根据 cpu 数量创建线程池
const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: 'happypack/loader?id=jsx'
},
{
test: /.less$/,
use: 'happypack/loader?id=styles'
}
]
},
plugins: [
new HappyPack({
id: 'jsx',
// 多少个线程
threads: happyThreadPool,
loaders: ['babel-loader']
}),
new HappyPack({
id: 'styles',
// 自定义线程数量
threads: 2,
loaders: ['style-loader', 'css-loader', 'less-loader']
})
]
};
给 loader 配置使用 HappyPack 需要对应的 loader 支持才行,例如 url-loader 和 file-loader 就不支持 HappyPack,在 HappyPack 的 wiki 中有一份支持 loader 的列表。
多进程打包
项目较大,打包较慢,开启多进程能提高速度;
项目较小,打包很快,开启多进程会降低速度(进程开销)。
所以需要按实际需求选用。
1.5 ParalleUglifyPlugin
ParallelUglifyPlugin使用webpack内置Uglify工具压缩JS,用这个工具开启多进程压缩更快。本质上和happypack同理。ParralleUglifyPlugin用于生产环境。
通常开发环境代码构建时间比较快,而构建用于发布到线上的代码时会添加压缩代码这一流程则会导致计算量大耗时多。ParallelUglifyPlugin可以将多进程并行处理的思想引入到代码压缩中。当Webpack有多个js文件需要压缩处理时,原本会使用UglifyJS去一个个挨着压缩再输出,使用了ParallelUglifyPlugin则会开启多个子进程把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过UglifyJS去压缩代码但变成了并行执行,所以ParallelUglifyPlugin能更快地完成对多个文件的压缩工作。
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
module.exports = {
module: {
rules: []
},
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
// (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
uglifyJS: {
output: {
beautify: false, // 最紧凑的输出
comments: false, // 删除所有的注释
},
compress: {
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
}
})
]
}
2.1 自动刷新
要用webpack-devserver:
module.export = {
watch: true, // 开启监听,默认为false
// 注意:开启监听后,webpack-dev-server会自动开启刷新浏览器
// 监听配置:
watchOptions: {
ignored: /node_modules/, // 忽略哪些文件
// 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
// 判断文件是否发生变化是通过不停地去询问系统指定文件有没有变化实现的
poll: 1000 // 默认每隔1000毫秒询问一次
}
}
2.2 热更新
模块热替换(Hot Module Replacement)的好处是只替换更新的部分,而不是整个页面都重新加载。
模块热替换能做到在不重新加载整个网页的情况下,通过将被更新过的模块替换老的模块,再重新执行一次来实现实时预览。 模块热替换相对于默认的刷新机制能提供更快的响应和更好的开发体验。 模块热替换默认是关闭的,要开启模块热替换,你只需在启动 DevServer 时带上--hot
参数,重启 DevServer 后再去更新文件就能体验到模块热替换了。
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = smart(webpackCommonConf, {
entry: {
index: [
'webpack-dev-server/client?http://localhost:8080/',
'webpack/hot/dev-server',
path.join(srcPath, 'index.js')
],
other: path.join(srcPath, 'other.js')
},
module: {
},
plugins: [
new HotModuleReplacementPlugin()
],
devServer: {
port: 8080,
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动 gzip 压缩
hot: true
}
})
// index.js要开启
// 增加,开启热更新之后的代码逻辑
if (module.hot) {
module.hot.accept('./library.js', function() {
// 使用更新过的 library 模块执行某些操作...
})
}
2.3 DllPlugin动态链接库插件
使用 webpack.DllPlugin 来预先编译和打包不会变动存在的文件,在业务代码中直接引入,可以加快 Webpack 编译打包的速度,但是并不能减少最后生成的代码体积。
在早期Windows系统中,由于受限于当时计算机内存空间较小的问题,出现了动态链接库这样一种节省内存的方式。当一段相同的子程序被多个程序调用的时候,相当于这段代码重复出现了多次,也会成倍地占用内存空间。为了节省内存,可以将这段共享的子程序存储为一个可执行文件,被多个程序调用时只在内存中生成和使用同一个实例即可。如果将类似的思路放在打包上面来看也可以起到优化的效果。相同的模块有可能会被多个入口引用,我们可以将这部分模块预先编译好,然后在项目打包的过程中直接去调用编译好的文件即可。这就是Webpack中DllPlugin的实现思路。当然它实际生成的还是JS文件而并不是真正的动态链接库。
DLLPlugin是 webpack 官方提供的一个插件,也是用来分离代码的,和optimization.splitChunks
(3.x 版本的是 CommonsChunkPlugin)有异曲同工之妙,但配置相对繁琐,如果项目不涉及性能优化这一块,基本上使用optimization.splitChunks
即可。和optimization.splitChunks的区别是,DLLPlugin 构建出来的内容无需每次都重新构建,后续应用代码部分变更时,不用再执行配置为webpack.dll.js这一部分的构建,沿用原本的构建结果即可,所以相比optimization.splitChunks
,使用 DLLPlugin 时构建速度是会有显著提高的。
前端框架如vue React,体积大构建慢;较稳定,不常升级版本;同一个版本只构建一次即可,不用每次都重新构建。DllPlugin实现了拆分 bundles,同时还大大提升了构建的速度。
webpack已内置DllPlugin支持,不用额外npm install。使用这个插件时需要额外的一个构建配置,用来打包公共的那一部分代码。具体配置上主要分为3步骤:
1)配制动态链接库:先为动态链接库单独创建一个Webpack配制文件,如webpack.dll.js。该配制module.exports对象中的entry指定了把哪些模块打包成vendor(即dll)。
2)打包动态链接库并生成vendor清单:使用该Webpack配制文件进行打包会生成一个xx.dll.js文件以及一个资源清单,这个清单一般叫做xx.manifest.json,在内部每一个模块都会分配一个ID。
3)将vendor连接到项目中:在工程的webpack.config.js中需要配制DllReferencePlugin来获取刚才打包出来的模块清单,这相当于工程代码和vendor连接的过程。
// webpack.dll.js
const webpack = require('webpack')
module.exports = {
mode: 'development',
entry: { // JS 执行入口文件
react: ['react', 'react-dom'] // 把 React 相关模块的放到一个单独的动态链接库
},
output: { // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,
// 也就是 entry 中配置的 react 和 polyfill
filename: '[name].dll.js',
path: distPath, // 输出的文件都放到 dist 目录下
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
library: '_dll_[name]', // 之所以在前面加上 _dll_ 是为了防止全局变量冲突
},
plugins: [
// 接入 DllPlugin
new webpack.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.dll.js
文件会对第三方库打包,打包完成后就不会再打包它了,然后每次运行webpack.config.js
文件的时候,都会打包项目中本身的文件代码,当需要使用第三方依赖的时候,会使用DllReferencePlugin
插件去读取第三方依赖库,而只有我们修改第三方公共库的时候,才会执行webpack.dll.js
。本质上来说 DLL 方案就是一种缓存机制。
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
app: './src/index.js'
},
output: {
publicPath: './dist/',
filename: '[name]@[chunkhash].js',
},
plugins: [
new webpack.DllReferencePlugin({
manifest: require(path.join(__dirname, 'react.manifest.json')),
})
]
};
注意:html-webpack-plugin并不会自动处理 DLLPlugin 分离出来的那个公共代码文件,我们需要自己处理这一部分的内容,可以考虑使用add-asset-html-webpack-plugin,详细参考官方的说明文档:使用 add-asset-html-webpack-plugin。
三、代码实践
3.1 优化babel-loader
本节示例代码见《使用Babel转换JavasCript代码》第二章-深入部分
3.2 IgnorePlugin
本节示例代码见《webpack高级配置-优化产出代码》第三章-3.6 IgnorePlugin
3.3 noParse
3.3.1 建立如下结构新项目(代码见github)
├── src
│ ├── index.html
│ └── index.js
└── webpack.config.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div class="wrap">noParseDemo</div>
</body>
</html>
index.js
import $ from 'jQuery'
console.log('index.js')
$('.wrap').append("<b>你好</b>")
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
mode: 'production',
entry: {
main: './src/index.js'
},
output: {
filename: '[name].[chunkHash:8].js',
chunkFilename: '[name].[chunkHash:8].js',
path: path.resolve(__dirname, 'output')
},
module: {
noParse: /node_modules/(jquey.js)/, // 让 Webpack 忽略对用这些文件递归解析处理
rules: [
{
test: /.js$/,
loader: ['babel-loader'],
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
})
]
}
3.3.2 对当前文件夹npm init初始化package.json,并npm install下载本项目所需依赖:
npm init
npm install jquery
npm install webpack webpack-cli --save-dev
npm install babel-loader @babel/core @babel/preset-env @babel/cli --save-dev
npm install html-webpack-plugin --save-dev
package.json的scripts添加"build": "webpack --config webpack.config.js"命令并执行
3.3.3 删掉刚才生成的output文件夹,现在注释掉noParse:/node_modules/(jquey.js)/语句在观察打包结果:
可以看到module.noParse配置项可以让webpack忽略对第三方类库jQuery的递归解析处理,从而能提高构建性能(缩短了打包时间)。代码里还是引入了jQuery,但不打包(不进行编译不进行模块化分析)。
3.4 happyPack
如果项目比较简单,没有必要采用这种方式,简单的项目而使用多线程编译打包会因为多线程打包浪费更多的 CPU 资源,这样最终结果是不仅不能加快打包的速度,反而会降低打包的速度。若项目较小,打包很快,开启多进程会降低速度(进程开销),需按实际需求选用此功能。故不作实验。
3.5 ParalleUglifyPlugin
使用方法参考第二章1.5。代码略。
3.6 自动刷新
略
3.7 热更新
略
3.8 DllPlugin
3.8.1 新建如下结构项目:
├── src
│ ├── index.html
│ └── index.js
└── webpack.config.dll.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div class="dll"></div>
</body>
</html>
index.js
import React, {Component} from 'react';
import ReactDOM, {render} from 'react-dom';
console.log(React, Component, ReactDOM, render, 'hell dll');
document.write('Dll loaded——React@'+ React.version);
webpack.config.dll.js
const webpack = require('webpack');
const path = require('path')
const vendors = ['react', 'react-dom'];
module.exports = {
mode: 'production',
entry: {
// 定义程序中打包公共文件的入口文件vendor.js
vendor: vendors
},
output: {
path: path.resolve(__dirname, "dist"),
filename: '[name].[chunkhash].js',
library: '[name]_[chunkhash]'
},
plugins: [
new webpack.DllPlugin({
path: 'manifest.json',
name: '[name]_[chunkhash]',
context: __dirname
})
]
};
在根目录下npm init初始化package.json文件,并下载相关依赖
npm init
npm install webpack webpack-cli --save-dev
npm install react react-dom --save-dev
在package.json里新增npm scripts命令:
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
+ "dll": "webpack --config webpack.config.dll.js"
},
执行npm run dll,查看打包结果:
可以看到现在的目录结构变成:
├── dist
│ └── vendor.10001d3ab7dc73b260aa.dll.js # 这个是刚刚打包出来的 dll 文件
├── manifest.json # 这个是配置文件,后续要用
├── node_modules
├── package.json
├── src
│ └── index.js
├── webpack.config.dll.js # dll 配置
3.8.2 现在我们再加一个正常项目的配置文件webpack.config.js
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
module.exports = {
output: {
filename: '[name].[chunkhash].js'
},
entry: {
app: './src/index.js'
},
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./manifest.json') // 这里导入 manifest配置内容
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new AddAssetHtmlPlugin({ filepath: require.resolve('.dist/*.dll.js') }),
]
};
在实际操作中,HTML 中不会主动引入 dll 的 vendor.js 文件,这时候需要我们想办法手动或者通过插件添加进去,比如使用add-asset-html-webpack-plugin,
npm i add-asset-html-webpack-plugin -D
下载相关依赖:
npm install html-webpack-plugin --save-dev
这时候执行webpack命令("build": "webpack --config webpack.config.js")就可以生成app文件了,并且app.js并不会包含 dll 打包出来的vendor.js文件内容,打包速度也提升了不少。
很显然,DLLPlugin 的配置要麻烦得多,并且需要关心你公共部分代码的变化,当你升级 react(即你的公共部分代码的内容变更)时,要重新去执行webpack.config.dll.js这一部分的构建,不然沿用的依旧是旧的构建结果,使用上并不如optimization.splitChunks来得方便。这是一种取舍,应根据项目的实际情况采用合适的做法。
代码参见:github
本文参考引用:
webpack优化构建速度coding.imooc.com Webpack 前端工程化入门gitbook.cn