Webpack优化实践
缩小文件的搜索范围
优化 Loader 配置
由于 Loader 对文件的转换操作很耗时,所以需要让尽可能少的文件被 Loader 处理。我们可以通过以下 3 方面优化 Loader 配置:
- 优化正则匹配
- 通过 cacheDirectory 选项开启缓存
- 通过 include、exclude 来减少被处理的文件。实践如下:
项目原配置:
1 | { |
优化后配置:
1 | { |
优化 resolve.modules 配置
resolve.modules
用于配置 Webpack 去哪些目录下寻找第三方模块。resolve.modules 的默认值是[node modules]
,含义是先去当前目录的/node modules 目录下去找我们想找的模块,如果没找到,就去上一级目录../node modules 中找,再没有就去../ .. /node modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。当安装的第三方模块都放在项目根目录的./node modules 目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找。
1 | module.exports = { |
优化 resolve.alias 配置
resolve.alias
配置项通过别名来将原导入路径映射成一个新的导入路径。
如项目中的配置使用:
1 | module.exports = { |
优化 resolve.extensions 配置
在导入语句没带文件后缀时,Webpack 会在自动带上后缀后去尝试询问文件是否存在。默认是:extensions:['. js ','. json ']
。也就是说,当遇到 require(‘. /data’)这样的导入语句时,Webpack 会先去寻找./data .js 文件,如果该文件不存在,就去寻找./data.json 文件,如果还是找不到就报错。如果这个列表越长,或者正确的后缀越往后,就会造成尝试的次数越多,所以 resolve.extensions
的配置也会影响到构建的性能。
优化措施:
- 后缀尝试列表要尽可能小,不要将项目中不可能存在的情况写到后缀尝试列表中。
- 频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程。
- 在源码中写导入语句时,要尽可能带上后缀,从而可以避免寻找过程。例如在确定的情况下将 require(’. /data ’)写成 require(’. /data.json ’),可以结合 enforceExtension 和 enforceModuleExtension 开启使用来强制开发者遵守这条优化**
优化 resolve.noParse 配置
noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析和处理,这 样做的好处是能提高构建性能。原因是一些库如 jQuery、ChartJS 庞大又没有采用模块化标准,让 Webpack 去解析这些文件既耗时又没有意义。noParse 是可选的配置项,类型需要是 RegExp 、[RegExp]、function 中的一种。例如,若想要忽略 jQuery 、ChartJS ,则优化配置如下:
1 | module.exports = { |
打包时 gzip 压缩
webpack 打包生成 gz 文件
- 安装插件(compression-webpack-plugin):
1 | npm install compression-webpack-plugin --save-dev |
- vue.config.js 文件进行 webpack 配置
1 | const CompressionWebpackPlugin = require("compression-webpack-plugin"); |
- nginx 开启 gzip
浏览器请求 xx.js/css 等文件时,服务器返回对应的 xxx.js.gz 文件,所以还需要在服务器配置一个属性,以期望它能正常返回我们需要的 gz 文件
1 | http { |
服务器在线 gzip 压缩
前端不用进行任何配置,也不用 webpack 生成 gz 文件,服务器进行处理
响应头中会携带 gzip 压缩配置,每次请求 xx.js 文件,服务器都会进行实时压缩。
1 | server |
两种方案的优缺点
- webpack 打包,然后直接使用静态的 gz,缺点就是打包后文件体积太大,但是不耗服务器性能;
- 使用 nginx 在线 gzip,缺点就是耗性能,需要实时压缩,但是 vue 打包后的文件体积小。
如果想有 gz 文件的时候进行静态压缩,不存在 gz 文件的时候进行在线压缩而不是加载源文件,nginx 配置时两种配置都写上即可
1 | gzip on; |
gzip_static 的优先级高,会先加载静态 gz 文件,当同目录下不存在此文件的时候,会执行在线压缩的命令。
如何区分使用了静态加载还是在线压缩?
响应头的 Content-Edcoding:gzip 表示 gzip 压缩已经生效,而 Etag 中只有简单字符表示静态资源加载,而前面带 W/ 表示启动了在线压缩
减少冗余代码
babel-plugin-transform-runtime
是 Babel
官方提供的一个插件,作用是减少冗余的代码 。 Babel 在将 ES6 代码转换成 ES5 代码时,通常需要一些由 ES5 编写的辅助函数来完成新语法的实现,例如在转换 class extent 语法时会在转换后的 ES5 代码里注入 extent 辅助函数用于实现继承。babel-plugin-transform-runtime 会将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小。
使用 HappyPack 多进程解析和处理文件
由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack 构建慢的问题会显得更为严重。运行在 Node.之上的 Webpack 是单线程模型的,也就是说 Webpack 需要一个一个地处理任务,不能同时处理多个任务。Happy Pack ( https://github.com/amireh/hap… ) 就能让 Webpack 做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程。
项目中 HappyPack 使用配置:
HappyPack 插件安装:
1 | npm i -D happypack |
webpack.base.conf.js 文件对 module.rules 进行配置
1 | module.exports = { |
webpack.prod.conf.js 文件进行配置
1 | const HappyPack = require("happypack"); |
使用 ParallelUglifyPlugin
多进程压缩代码文件
由于压缩 JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理 AST ,所以导致这个过程的计算量巨大,耗时非常多。当 Webpack 有多个 JavaScript 文件需要输出和压缩时,原本会使用 UglifyJS 去一个一个压缩再输出,但是 ParallelUglifyPlugin 会开启多个子进程,将对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。所以 ParallelUglify Plugin 能更快地完成对多个文件的压缩工作。
ParallelUglifyPlugin 插件安装:
1 | npm i -D webpack-parallel-uglify-plugin |
webpack.prod.conf.js 文件进行配置
1 | const ParallelUglifyPlugin = require("webpack-parallel-uglify-plugin"); |
需要注意的是 webpack 升级之后 UglifyJsPlugin 模块找不到,解决方法如下:
原:
1 | { |
1 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); |
替换为:
1 | config.plugins = [...config.plugins, ...plugins]; |
使用自动刷新
借助自动化的手段,在监听到本地源码文件发生变化时,自动重新构建出可运行的代码后再控制浏览器刷新。Webpack 将这些功能都内置了,并且提供了多种方案供我们选择。
项目中自动刷新的配置:
1 | module.exports = { |
相关优化措施:
- 配置忽略一些不监听的一些文件,如:node_modules。
- watchOptions.aggregateTirneout 的值越大性能越好,因为这能降低重新构建的频率。
- watchOptions.poll 的值越小越好,因为这能降低检查的频率。
开启模块热替换
DevServer 还支持一种叫做模块热替换( Hot Module Replacement )的技术可在不刷新整个网页的情况下做到超灵敏实时预览。原理是在一个源码发生变化时,只需重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块 。模块热替换技术在很大程度上提升了开发效率和体验 。
项目中模块热替换的配置:
1 | module.exports = { |
CDN 加速
将引用的静态资源修改为 CDN 上对应的路径,可以利用 webpack 对 于 output 参数和 loader 的 publicpath 参数来修改资源路径
删除死代码(tree shaking)
css 需要使用 Purify-CSS
提取公共代码
如果每个页面的代码都将这些公共的部分包含进去,则会造成以下问题 :
- 相同的资源被重复加载,浪费用户的流量和服务器的成本。
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
如果将多个页面的公共代码抽离成单独的文件,就能优化以上问题 。Webpack 内置了专门用于提取多个 Chunk 中的公共部分的插件 CommonsChunkPlugin。
项目中 CommonsChunkPlugin
的配置:
1 | module.exports = { |
按需加载代码
通过 vue 写的单页应用时,可能会有很多的路由引入。当打包构建的时候,javascript 包会变得非常大,影响加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。
项目中路由按需加载(懒加载)的配置:
1 | const Foo = () => import("./Foo.vue"); |
优化 SourceMap
source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map
我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩,去掉多余的空格,且 babel 编译化后,最终会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug 的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发不好调式,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的。
SourceMap 的可选值如下
开发环境推荐:cheap-module-eval-source-map
生产环境推荐:cheap-module-source-map
原因如下:
源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加 cheap 的基本类型来忽略打包前后的列信息。
不管是开发环境还是正式环境,我们都希望能定位到 bug 的源代码具体的位置,比如说某个 vue 文件报错了,我们希望能定位到具体的 vue 文件,因此我们也需要 module 配置。
我们需要生成 map 文件的形式,因此我们需要增加 source-map 属性。
我们介绍了 eval 打包代码的时候,知道 eval 打包后的速度非常快,因为它不生成 map 文件,但是可以对 eval 组合使用 eval-source-map 使用会将 map 文件以 DataURL 的形式存在打包后的 js 文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快
具体使用
map 文件只要不打开开发者工具,浏览器是不会加载的,线上环境一般有三种处理方案
hidden-source-map
:借助第三方错误监控平台 Sentry 使用nosources-source-map
:只会显示具体行数以及查看源代码的错误栈,安全性 比 sourcemap 要高一些sourcemap
:通过 nginx 设置将.map
文件只对白名单开放(公司内网)
注意:避免在生产中使用 inline-
和 eval-
,因为它们会增加 bundle 体积大小,并降 低整体性能
构建结果输出分析
Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解 vue 项目中用到的分析工具:webpack-bundle-analyzer
。
项目中在 webpack.prod.conf.js
进行配置:
1 | if (config.build.bundleAnalyzerReport) { |
执行 npm run build --report
后生成分析报告如下:
如何提高 webpack 的构建速度
在多入口情况下,使用 CommonsChunkPlugin 来提取公共代码
- 通过 externals 配置来提取常用库
- 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引用但是绝对不会修改的 npm 包来进行预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
- 使用 Happypack 实现多线程加速编译
- 使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度
- 使用 Tree-shaking 和 Scope Hoisting 来剔除多余代码