带你深度解锁Webpack系列(基础篇)和带你深度解锁Webpack系列(进阶篇),主要是讲解了 Webpack
的配置,可是随着项目愈来愈大,构建速度可能会愈来愈慢,构建出来的js的体积也愈来愈大,此时就须要对配置进行优化。css
文中罗列出了十多种优化方式,你们能够结合本身的项目,选择适当的方式进行优化。这些 Webpack
插件的源码我大多也没有看过,主要是结合 Webpack
官方文档以及项目实践,在验证后输出了本文,若是文中有错误的地方,欢迎在评论区指正。html
鉴于前端技术变动迅速,祭出本篇文章基于 Webpack
的版本号:前端
├── webpack@4.41.5 └── webpack-cli@3.3.10
本文对应的项目地址(编写本文时使用)供参考:https://github.com/YvetteLau/...node
有时,咱们觉得的优化是负优化,这时,若是有一个量化的指标能够看出先后对比,那将会是再好不过的一件事。react
speed-measure-webpack-plugin
插件能够测量各个插件和loader
所花费的时间,使用以后,构建时,会获得相似下面这样的信息:jquery
对比先后的信息,来肯定优化的效果。webpack
speed-measure-webpack-plugin 的使用很简单,能够直接用其来包裹 Webpack
的配置:git
//webpack.config.js const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); const smp = new SpeedMeasurePlugin(); const config = { //...webpack配置 } module.exports = smp.wrap(config);
咱们能够经过 exclude
、include
配置来确保转译尽量少的文件。顾名思义,exclude
指定要排除的文件,include
指定要包含的文件。github
exclude
的优先级高于 include
,在 include
和 exclude
中使用绝对路径数组,尽可能避免 exclude
,更倾向于使用 include
。web
//webpack.config.js const path = require('path'); module.exports = { //... module: { rules: [ { test: /\.js[x]?$/, use: ['babel-loader'], include: [path.resolve(__dirname, 'src')] } ] }, }
下图是我未配置 include
和配置了 include
的构建结果对比:
在一些性能开销较大的 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
,我比对了下,构建时间很接近。
因为有大量文件须要解析和处理,构建是文件读写和计算密集型的操做,特别是当文件数量变多后,Webpack
构建慢的问题会显得严重。文件读写和计算操做是没法避免的,那能不能让 Webpack
同一时刻处理多个任务,发挥多核 CPU 电脑的威力,以提高构建速度呢?
HappyPack
就能让 Webpack
作到这点,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。
首先须要安装 happypack
:
npm install happypack -D
修改配置文件:
const Happypack = require('happypack'); module.exports = { //... module: { rules: [ { test: /\.js[x]?$/, use: 'Happypack/loader?id=js', include: [path.resolve(__dirname, 'src')] }, { test: /\.css$/, use: 'Happypack/loader?id=css', include: [ path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules', 'bootstrap', 'dist') ] }, { test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/, use: 'Happypack/loader?id=file', include: [ path.resolve(__dirname, 'src'), path.resolve(__dirname, 'public'), 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'], }), new Happypack({ id: 'file', //和rule中的id=file对应 use: [{ loader: 'url-loader', options: { limit: 10240 //10K } }], }), ] }
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
,由于进程的分配和管理也须要时间,并不能有效提高构建速度,甚至会变慢。
除了使用 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?$/, use: ['thread-loader', 'cache-loader', 'babel-loader'] } ] } }
thread-loader
和 Happypack
我对比了一下,构建时间基本没什么差异。不过 thread-loader
配置起来为简单。
虽然不少 webpack
优化的文章上会说起多进程压缩的优化,不论是 webpack-parallel-uglify-plugin
或者是 uglifyjs-webpack-plugin
配置 parallel
。不过这里我要说一句,不必单独安装这些插件,它们并不会让你的 Webpack
更快。
由于当前 Webpack
默认使用的是 TerserWebpackPlugin
,默认开启了多进程和缓存,构建时,你的项目中能够看到 terser
的缓存文件 node_modules/.cache/terser-webpack-plugin
。
HardSourceWebpackPlugin
为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source
。
配置 hard-source-webpack-plugin
,首次构建时间没有太大变化,可是第二次开始,构建时间大约能够节约 80%。
首先安装依赖:
npm install hard-source-webpack-plugin -D
修改 webpack
的配置:
//webpack.config.js var HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); module.exports = { //... plugins: [ new HardSourceWebpackPlugin() ] }
用另一个比较大的项目测试了下,配置了 HardSourceWebpackPlugin
,构建时间从 8S 左右降到了 2S 左右。
HardSourceWebpackPlugin文档中 列出了一些你可能会遇到的问题以及如何解决,例如热更新失效,或者某些配置不生效等。
若是一些第三方模块没有AMD/CommonJS规范版本,可使用 noParse
来标识这个模块,这样 webpack
会引入这些模块,可是不进行转化和解析,从而提高 webpack
的构建性能 ,例如:jquery
、lodash
。
noParse 属性的值是一个正则表达式或者是一个 function
。
//webpack.config.js module.exports = { //... module: { noParse: /jquery|lodash/ } }
我当前的 webpack-optimize
项目中,没有使用 jquery
或者是 lodash
。
所以新建一个项目测试,只引入 jquery
和 loadsh
,而后配置 noParse
和不配置 noParse
,分别构建比对时间。
配置noParse
前,构建须要 2392ms
。配置了 noParse
以后,构建须要 1613ms
。 若是你使用到了不须要解析的第三方依赖,那么配置 noParse
很显然是必定会起到优化做用的。
resolve
配置 webpack
如何寻找模块所对应的文件。假设咱们肯定模块都从根目录下的 node_modules
中查找,咱们能够配置:
//webpack.config.js const path = require('path'); module.exports = { //... resolve: { modules: [path.resolve(__dirname, 'node_modules')], } }
须要记住的是,若是你配置了上述的 resolve.moudles
,可能会出现问题,例如,你的依赖中还存在 node_modules
目录,那么就会出现,对应的文件明明在,可是却提示找不到。所以呢,我的不推荐配置这个。若是其余同事不熟悉这个配置,遇到这个问题时,会摸不着头脑。
另外,resolve
的 extensions
配置,默认是 ['.js', '.json']
,若是你要对它进行配置,记住将频率最高的后缀放在第一位,而且控制列表的长度,以减小尝试次数。
本项目较小,所以测试时,此处优化效果不明显。
webpack
的内置插件,做用是忽略第三方包指定目录。
例如: moment
(2.24.0版本) 会将全部本地化内容和核心功能一块儿打包,咱们就可使用 IgnorePlugin
在打包时忽略本地化内容。
//webpack.config.js module.exports = { //... plugins: [ //忽略 moment 下的 ./locale 目录 new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) ] }
在使用的时候,若是咱们须要指定语言,那么须要咱们手动的去引入语言包,例如,引入中文语言包:
import moment from 'moment'; import 'moment/locale/zh-cn';// 手动引入
index.js
中只引入 moment
,打包出来的 bundle.js
大小为 263KB
,若是配置了 IgnorePlugin
,单独引入 moment/locale/zh-cn
,构建出来的包大小为 55KB
。
咱们能够将一些JS文件存储在 CDN
上(减小 Webpack
打包出来的 js
体积),在 index.html
中经过 <script>
标签引入,如:
<!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 id="root">root</div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> </body> </html>
咱们但愿在使用时,仍然能够经过 import
的方式去引用(如 import $ from 'jquery'
),而且但愿 webpack
不会对其进行打包,此时就能够配置 externals
。
//webpack.config.js module.exports = { //... externals: { //jquery经过script引入以后,全局中即有了 jQuery 变量 'jquery': 'jQuery' } }
有些时候,若是全部的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
<script src="/dll/react.dll.9dcd9d.js"></script>
构建速度
包体积
抽离公共代码是对于多页应用来讲的,若是多个页面引入了一些公共模块,那么能够把这些公共的模块抽离出来,单独打包。公共代码只须要下载一次就缓存起来了,避免了重复下载。
抽离公共代码对于单页应用和多页应该在配置上没有什么区别,都是配置在 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
构建优化的时候,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 //重复引入了几回 }, //... } }, }, }
从新构建,结果以下所示:
若是使用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
函数不会被打包进去。
变量提高,能够减小一些变量声明。在生产环境下,默认开启。
另外,你们测试的时候注意一下,speed-measure-webpack-plugin
和 HotModuleReplacementPlugin
不能同时使用,不然会报错:
webpack
的配置部分到此基本就结束了,typescript
、eslint
和 prettier
,这里没有说起,你们能够本身配置一下,也能够参考我以前练手时配置的一个项目:https://github.com/YvetteLau/...
若是你对 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" ] ] }
以上就是我目前为止使用到的一些优化,若是你有更好的优化方式,欢迎在评论区留言,感谢阅读。
若是本文对你有帮助的话,给本文点个赞吧。
参考文档: