移动测试开发 WebPack 热更新 - 从 12s 到 1s 的优化之路 --- 杨洪搏
先上效果
优化前:12S
优化后:1s(理想情况下)
我们在讲解热更新优化之前先来简单了解一下 webpack 热更新原理:
● 通过 webpack-dev-server 创建两个服务器:提供静态资源的服务(express)和 Socket 服务
● express server 负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
● socket server 是一个 websocket 的长连接,双方可以通信
● 当 socket server 监听到对应的模块发生变化时,会生成两个文件.json(manifest 文件)和.js 文件(update chunk)
● 通过长连接,socket server 可以直接将这两个文件主动发送给客户端(浏览器)
● 浏览器拿到两个新的文件后,通过 HMR runtime 机制,加载这两个文件,并且针对修改的模块进行更新
热更新速度分析:speed-measure-webpack-plugin
它可以看到每个 loader 和插件的执行耗时,这样就让你知道优化你的 webpack 构建速度你的注意力该集中在哪里。
安装:
npm install --save-dev speed-measure-webpack-plugin
使用:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = {
publicPath: "./",
configureWebpack: (config) => {
return smp.serve({
...
});
},
};
效果:
从上图中我们发现 CompressionPlugin 耗费了 7.2S 的时间,但是我们只是在生产环境需要代码压缩,因此我们可以在开发环境将其屏蔽。
改进路径
1、开发环境屏蔽 CompressionPlugin 插件
plugins: [
(isProduction
? new CompressionWebpackPlugin({
filename: "[path].gz[query]",
algorithm: "gzip",
test: /\.(js|css)(\?.*)?$/i,
threshold: 2048, // 对超过10k的数据进行压缩
minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
deleteOriginalAssets: false // 删除原文件
})
: undefined)
].filter(Boolean)
如果你的项目中有只在生产环境使用的插件,可以使用该方法过滤掉,来加快热更新的速度。
效果如下:
除了删除多余的插件,我们也可以借助 sourcemap 的修改来提升热更新速度。
2、Sourcemap 与热更新
SourceMap 生成过程中,由于项目过大导致需要计算处理的映射节点(SourceNode)特别多,这也导致 SourceMap 生成过程中内存飙升频繁 GC,构建十分缓慢甚至 OOM。
因此合适的 sourcemap 有助于提升我们的热更新速度。
可用的 SourceMap 非常多,此处我们只列出一些具有代表性的:
• source-map:外部。可以查看错误代码准确信息和源代码的错误位置。
• eval:内联。每个模块会封装到 eval 里包裹起来执行,只能定位到报错模块的位置。
• inline-source-map:内联。只生成一个内联 Source Map,可以查看错误代码准确信息和源代码的错误位置
• hidden-source-map:外部。可以查看错误代码准确信息,但不能追踪源代码错误,只能提示到构建后代码的错误位置。
• eval-source-map:内联。每一个文件都生成对应的 Source Map,都在 eval 中,可以查看错误代码准确信息 和 源代码的错误位置。
• nosources-source-map:外部。可以查看错误代码错误原因,但不能查看错误代码准确信息,并且没有任何源代码信息。
• cheap-source-map:外部。生成一个没有列信息的 SourceMaps 文件,不包含 loader 的 sourcemap(譬如 babel 的 sourcemap)
• cheap-module-source-map:外部。生成一个没有列信息的 SourceMaps 文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。与 cheap-source-map 的区别为,如果加了 module 那就是 loader 处理前的源码,例如报错时会报 const 错误而不是 loader 编译后的 var。
• eval-cheap-module-source-map:内联。与 cheap-module-source-map 的区别为,它是内联的不生成多余文件,构建速度更快
内联和外部的区别:
①.外部生成了文件(.map),内联没有。
②.内联构建速度更快。
下面更直观的列出了各种 source-map 的特点对比
速度说明
• + 越多速度越快,0 介于 + -之间,-越多速度越慢
品质说明
• 原始源代码
你写的代码,未经过任何处理
• 转换过的代码
经过各种 loader 转译后的代码
• 生成后的代码
转译后并经过 webpack 处理,注入 webpack 运行时代码后的代码
• 打包后的代码
合成一个 chunk 后的、压缩过的代码
修改 sourcemap
这里一般推荐使用 vuecli5 默认的 eval-cheap-module-source-map,这个配置提供到源代码的的完整映射,方便定位问题,并且不需要通常不必要的行映射,提高了构建速度,并且由于使用了 eval,重构建速度更高。如果你想以报错的准确度来换取更快的构建速度,也可以使用其他 sourcemap 例如 eval。
configureWebpack: (config) => {
config.devtool = "eval";//eval为构建速度最快的sourcemap,但报错信息只会显示eval函数的位置
}
3、runtimeChunk
runtimeChunk 的作用:
设置 runtimeChunk 是将包含 chunks 映射关系的 list 单独从 app.js 里提取出来,因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,所以每次改动都会影响它,如果不将它提取出来的话,等于 app.js 每次都会改变。缓存就失效了。设置 runtimeChunk 之后,webpack 就会生成一个个 runtime~xxx.js 的文件。
然后每次更改所谓的运行时代码文件时,打包构建时 app.js 的 hash 值是不会改变的。如果每次项目更新都会更改 app.js 的 hash 值,那么用户端浏览器每次都需要重新加载变化的 app.js,如果项目大切优化分包没做好的话会导致第一次加载很耗时,导致用户体验变差。现在设置了 runtimeChunk,就解决了这样的问题。所以这样做的目的是避免文件的频繁变更导致浏览器缓存失效,所以其是更好的利用缓存。提升用户体验。
runtimeChunk 为何设置为 single
当 runtimeChunk 设置为"single"时,Webpack 会把所有的运行时代码都打包到一个独立的文件中。这个文件会被缓存,并且只有在运行时版本发生变化时才会重新加载。这样可以减少初始加载的文件大小,并且提高缓存命中率。
而当 runtimeChunk 设置为 true 时,Webpack 会为每个入口点生成一个运行时文件,如果你的入口过多,会导致生成的 runtime 文件很多,与 single 相比热更新效率就会下降。
因此,将 runtimeChunk 设置为"single"会将所有运行时代码打包到一个文件中,这样更加有利于热更新。
在 vuecli5 中 runtimeChunk 默认为 single,如果你使用的 vuecli 版本默认值并不是 single,可以使用如下方式更改
module.exports = {
configureWebpack: {
optimization: {
runtimeChunk: "single"
}
}
}
4、多页面导致热更新速度慢
当我们的项目中页面过多也会导致热更新速度慢,此时我们可以使用两种方式来解决该问题(此两种方式可以同时使用):
①.babel-plugin-dynamic-import-node
在开发环境下进行热更新处理动态导入时,每次修改导入模块都会导致整个模块重新解析和加载,影响了热更新的速度。
babel-plugin-dynamic-import-node 的作用就是通过静态分析代码,将动态导入 import 语句转化为静态导入 require,从而加快热更新的速度。使得模块在每次热更新时只需重新加载修改过的模块,而不需要重新解析整个模块。这种方式在开发环境中可以大大提高热更新的速度。
注意该方法仅对使用 import 懒加载路由过多导致的热更新慢有效。
安装:
npm install babel-plugin-dynamic-import-node --save-dev
使用配置:
如果你使用的是:babel.config.js
修改文件
module.exports = {
presets: [
'@vue/app'
],
env: {
development: {//仅在开发环境生效
plugins: ['dynamic-import-node']
}
}
}
②.屏蔽多余的路由
我们在日常的开发中,每次开发涉及的页面可能只有几个,如果我们只根据我们在当前开发时需要的路由来注册开发环境的路由,可以大大增加热更新速度,下面给出样例。
首先我们创建环境变量
NODE_ENV=development
VUE_APP_MY_MODEL=myMoudle
按需配置路由
const routes = process.env.VUE_APP_MY_MODEL ==="myMoudle"?
[{//本次开发需要的路由
path:"/mypath",
name:"myPathName",
component:myCom
}]
:[...]//完整的路由
总结:
通过以上一系列优化,可以将热更新速度降到 1S 内,这样修改的效果就能立刻体验啦,从而加快开发者的开发迭代速度,减少调试和错误修复时间。