webpack优化前端性能(webpack之常见性能优化)
关于webpack的性能优化 ,主要体现在三个方面:
构建性能:是指在开发阶段的构建性能 。当构建性能越高 ,开发效率越高 。 传输性能:在这方面重点考虑网络中的总传输量 、JS文件数量以及浏览器缓存 。 运行性能:主要是指JS代码在浏览器端运行的速度 。构建性能
减少模块解析
模块解析包括:AST抽象语法树分析 、依赖分析 、模板语法替换
对某个模块不进行解析,可以缩短构建时间
如果某个模块不做解析 ,该模块经过loader处理侯的代码就是最终代码 。
如果没有loader对该模块进行处理 ,该模块的源码就是最终打包结果的代码 。
对于模块中没有其他依赖模块 ,则不需要解析 ,可以通过配置module.noParse进行处理:
module.exports = { mode: "development", module: { noParse: /JQuery/ } }优化loader性能
限制loader的应用范围
针对一些第三方库 ,不使用loader进行处理 。例如babel-loader ,转换一些本身就是用ES5语法书写的第三方库 ,反而会浪费构建时间 。
因此通过module.rules.exclude或module.rules.include ,排除或仅包含需要应用loader的场景 。 module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, //或 // include: /src/, use: "babel-loader" } ] } }缓存loader结果
如果某个文件内容不变 ,经过相同的loader解析后,解析后的结果也不变 ,所以我们可以把loader的解析结果保存下来 ,让后续的解析直接用缓存的结果,具体实现如下:
module.exports = { module: { rules: [ { test: /\.js$/, use: [{ loader: "cache-loader", options:{ cacheDirectory: "./cache" } }, ...loaders] }, ], }, };cache-loader的原理是 ,在执行loader之前 ,如果发现有缓存文件,则直接在loader.pitch函数里return源代码。
那么问题来了 ,loader明明不是从后往前执行的吗?那为什么cache-loader还可以拿到loader的缓存结果?
其实每个loader的运行过程中 ,还包括一个过程 ,即pitch
function loader(source){ return `new source` } loader.pitch = function(filePath){ // 可返回可不返回 // 如果返回 ,返回源代码 } module.exports = loader;第一次打包时 ,会先把filepath交给loader1.pitch执行, 检查是否有缓存结果 ,若无缓存 ,往后执行 。调用loader2.pitch ,检查是否有缓存 ,若无缓冲,往后执行 ,依次类推…直到最后结束了再调用loader ,当调用cache-loader时,就会返回loader处理的结果并缓存 。
当第二次打包时(流程同上) ,若发现有缓存 ,则直接返回缓存结果,不会继续往后走了 ,示例图如下:pitch的好处 ,可以根据是否有返回 ,来控制下一步到哪。
对于babel-loader ,使用它本身的配置也是可以缓存的 。
开启多线程打包
通过thread-loader会开启一个线程池 ,它会把后续的loader放到线程池的线程中运行 ,以提高构建效率 。
module.exports = { module: { rules: [{ test: /\.js$/, use: [ "thread-loader", "babel-loader" ] }] } };放到线程池的loader的缺点:
无法使用 webpack api 生成文件。 无法使用自定义的 plugin api 。 无法访问 webpack options 。thread-loader可以通过测试决定放置的位置 。
由于开启和管理线程需要消耗时间 ,所以在小项目使用会增加构建时间 。
热替换 (Hot Module Replacement)
其实 ,热更新是不能降低构建时间(可能还会稍微增加) ,因为它发生代码运行期间,但它可以降低代码改动到效果显现的时间 。
//配置文件 module.exports = { devServer: { open: true, hot: true //开启HMR }, plugins: [ new HTMLWebpackPlugin({ template: "./public/index.html" }) ] } //index.js if(module.hot){ // 是否开启热更新 module.hot.accept() // 接收热更新 }默认情况下 ,webpack-dev-server不管是否开启了热更新 ,当重新打包后,都会调用location.reload刷新页面
但如果运行module.hot.accept() ,将改变这一行为 ,
module.hot.accept()的作用是让webpack-dev-server通过socket管道,把服务器更新的内容发送到浏览器 ,然后 ,将结果交给插件HotModuleReplacementPlugin注入的代码执行插件HotModuleReplacementPlugin会根据覆盖原始代码 ,然后让代码重新执行 。对于样式热替换 ,可使用style-loader 。
module.exports = { devServer: { open: true, hot: true //开启HMR }, module: { rules: [{ test: /\.css$/, use: ["style-loader", "css-loader"] }] }, plugins: [ new HTMLWebpackPlugin({ template: "./public/index.html" }) ] }为什么不使用mini-css-extract-plugin插件?
因为热更新发生时 ,HotModuleReplacementPlugin只会简单的重新运行模块代码 。因此style-loader的代码一运行 ,就会重新设置style元素中的样式 ,而mini-css-extract-plugin生成文件是在构建期间 ,运行期间无法改动文件 。
整个原理流程:
当开启热更新后 ,Webpack 会轮询问有没有哪些模块发生变化,如果文件内容发生改变 ,会异步下载更新的代码 ,向服务器发送请求。下载完毕后,服务器就会主动发送信息给浏览器告知有文件内容发生改变 ,浏览器发送请求给服务器请求发送修改后的资源文件 ,服务器接收到请求后把修改的资源文件发送给浏览器,浏览器把接收到的结果交给HotModuleReplacementPlugin ,HotModuleReplacementPlugin再覆盖原始代码 ,再重新执行代码 。
传输性能
分包
webpack默认情况下是不会分包的 ,它会把所有依赖文件合并到一个bundle中 。而分包的时机是 ,当公共模块体积较大 或 有较少的变动 ,特别是在多页面打包的情况下 ,会存在多个chunk引入公共模块导致冗余代码的情况 ,占用打包体积。
分包的目的是在不影响源代码编写的情况下 ,减少公共代码 ,降低总体积(特别是一些大型的第三方库)和充分利用浏览器缓存 。并非所有的情况都适合分包,需要视具体情况而定 。
手动分包
总体思路:
先单独的打包公共模块 ,并利用DllPlugin生成资源清单。 手动引入公共模块 ,重新设置clean-webpack-plugin,然后使用DllReferencePlugin控制打包结果 。具体打包过程如下:
开启output.library暴露公共模块 用DllPlugin创建资源清单 用DllReferencePlugin使用资源清单 // webpack.dll.config.js const webpack = require(webpack); module.exports = { mode: "production", entry: { jquery: ["jquery"], lodash: ["lodash"] }, output: { filename: "dll/[name].js", library: "[name]", //libraryTarget: "var" //暴露方式 }, plugins: [ new webpack.DllPlugin({ path: path.resolve(__dirname, "dll", "[name].manifest.json"), //资源清单的保存位置 name: "[name]"//资源清单中 ,暴露的变量名 }) ] }; //webpack.config.js module.exports = { plugins:[ //指定资源清单 ,在打包时对照资源清单,当发现该模块是资源清单里的资源时不进行打包处理 。 new webpack.DllReferencePlugin({ manifest: require("./dll/jquery.manifest.json") }), new webpack.DllReferencePlugin({ manifest: require("./dll/lodash.manifest.json") }) ] };引用:https://webpack.docschina.org/plugins/dll-plugin#dllplugin
自动分包
原理:
检查每个chunk编译的结果 根据分包策略 ,找到那些满足策略的模块 根据分包策略 ,生成新的chunk打包这些模块(代码有所变化) 把打包出去的模块从原始包中移除 ,并修正原始包代码在代码层面 ,有以下变动
分包的代码中 ,加入一个全局变量 ,类型为数组 ,其中包含公共模块的代码 原始包的代码中 ,使用数组中的公共代码webpack提供了optimization配置项 ,其中splitChunks是分包策略的配置 。
实际上,webpack在内部是使用SplitChunksPlugin进行分包的 ,分包时 ,webpack开启了一个新的chunk,对分离的模块进行打包 。打包结果中 ,公共的部分被提取出来形成了一个单独的文件 ,它是新chunk的产物
过去有一个库CommonsChunkPlugin也可以实现分包,不过由于该库某些地方并不完善 ,到了webpack4之后 ,已被SplitChunksPlugin取代 。
一般分包是在生产环境下进行的 。
分包策略有其默认的配置 ,只需小小改动即可应用大部分分包场景 。
chunks:用于配置需要应用分包策略的chunk 。一般只需要配置为 all 即可
all: 对于所有的chunk都要应用分包策略 async:【默认】仅针对异步chunk应用分包策略 initial:仅针对普通chunk应用分包策略maxSize:可以控制包的最大字节数 。
如果某个包(包括分出来的包)超过了该值 ,则webpack会尽可能的将其分离成多个包。
分包的基础单位是模块 ,如果一个完整的模块超过了该体积 ,它是无法做到再切割的 ,因此 ,尽管使用了这个配置 ,完全有可能某个包还是会超过这个体积全局策略:
module.exports = { mode: "production", entry: {}, output: {}, optimization: { splitChunks: { // 分包策略 chunks: "all", maxSize: 60000, // 分包策略的其他配置 automaticNameDelimiter: ".", // 新chunk名称的分隔符,默认值~ minChunks: 1, // 一个模块被多少个chunk使用时 ,才会进行分包 ,默认值1 minSize: 30000, // 当分包达到多少字节后才允许被真正的拆分,默认值30000 }, }, plugins: [], }缓存组策略: 每个缓存组提供一套独有的策略 ,webpack按照缓存组的优先级依次处理每个缓存组 ,被缓存组处理过的分包不需要再次分包 。
默认情况下,webpack提供了两个缓存组:
module.exports = { optimization:{ splitChunks: { //全局配置 cacheGroups: { // 属性名是缓存组名称 ,会影响到分包的chunk名 // 属性值是缓存组的配置 ,缓存组继承所有的全局配置 ,也有自己特殊的配置 vendors: { test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时 ,将这些模块进行单独打包 priority: -10 // 缓存组优先级 ,优先级越高 ,该策略越先进行处理 ,默认值为0 }, default: { minChunks: 2, // 覆盖全局配置 ,将最小chunk引用数改为2 priority: -20, // 优先级 reuseExistingChunk: true // 重用已经被分离出去的chunk } } } } }单模块体积压缩
代码压缩
代码压缩除了减少代码体积 ,还可以破坏代码的可读性,提升破解成本 。
Terser是一个新起的代码压缩工具 ,支持ES6+语法。webpack会内置Terser ,当启用生产环境后即可用其进行代码压缩 。
Terser官网:https://terser.org/
如果想更改 、添加压缩工具,又或者是想对Terser进行配置 ,使用下面的webpack配置即可:
module.exports = { optimization: { minimize: true, // 是否要启用压缩 ,默认情况下,生产环境会自动开启 minimizer: [ // 压缩时使用的插件 ,可以有多个 new TerserPlugin(), // js压缩插件 new OptimizeCSSAssetsPlugin() // css压缩插件 ], } }terser 、webpack 、rollup.js都能够识别/*#__PURE__ */注释标记 ,/*#__PURE__* /的作用就是告诉打包工具该函数的调用不会产生副作用 。
tree shaking
tree shaking可以移除模块之间的无效代码
如果运行环境是生产环境 ,tree shaking自动开启。
在编写代码时 ,由于tree shaking需要满足一定的代码规范 ,所以应该尽量注意规范 。例如:
//使用 export xxx; //导出 import {xxx} from "xxx"; // 导入 //不使用 export default {xxx}; //导出 import xxx from "xxx"; // 导入当webpack依赖分析完毕后 ,webpack会根据每个模块每个导出是否被使用 ,标记其他导出为dead code ,然后交给代码压缩工具处理 ,代码压缩工具最终移除掉那些dead code代码 。
commonjs很难做到tree shaking,所以主流的库为了做tree shaking ,都会发布其es6版本 ,比如lodash-es 。
webpack在tree shaking的使用,有一个原则:一定要保证代码正确运行
在满足该原则的基础上 ,再来决定如何tree shaking
因此 ,当webpack无法确定某个模块是否有副作用时,它往往将其视为有副作用
因此 ,某些情况可能并不是我们所想要的
//common.js var n = Math.random(); //index.js import "./common.js"虽然我们根本没用有common.js的导出 ,但webpack担心common.js有副作用 ,如果去掉会影响某些功能
如果要解决该问题 ,就需要标记该文件是没有副作用的
在package.json中加入sideEffects
{ "sideEffects": false }有两种配置方式:
false:当前工程中 ,所有模块都没有副作用 。注意 ,这种写法会影响到某些css文件的导入 数组:设置哪些文件拥有副作用 ,例如:["!src/common.js"] ,表示只要不是src/common.js的文件 ,都有副作用这种方式我们一般不处理,通常是一些第三方库在它们自己的package.json中标注
由于webpack无法对css完成tree shaking ,所以可以通过正则匹配页面样式有没有引用进行移除样式代码 。
可以通过purgecss-webpack-plugin进行处理 ,该插件对css module无法处理 。
懒加载
通过动态导入模块,例如在判断里使用导入语句 。
导入语句不能使用commonjs ,虽然require支持动态导入 ,但是它在打包环节也会进入依赖分析 。
动态加载可以使用import(),import作为es6的草案 ,webpack打包发现使用import()的调用 ,会对其单独打包 ,打包结果该代码时 ,浏览器会使用JSOP的方式远程去读取一个js模块 ,import()返回的是一个promise 。 async function run(){ if(判断条件){ const { chunk } = await import(/* webpackChunkName:自定义chunkName */xxx.js) } } run();请求的异步的模块会加入webpackJsonp数组里。
值得注意的是 ,这样的异步导入是不可以做到tree shaking的 ,不过可以使用取巧的方法 ,通过一个媒介引入 ,打包分析过程既能tree shaking又能异步加载 。
//媒介文件 export { xxx } from 目标文件 //主文件 async function run(){ if(判断条件){ const { chunk } = await import( 媒介文件) } } run()gzip
gzip是一种压缩文件的算法
gizp工作原理:
浏览器发送请求时,会在请求头中设置Accept-Encoding:gzip,deflate,br 。表明浏览器支持gzip。服务器收到浏览器发送的请求之后 ,判断浏览器是否支持gizp ,如果支持gzip,则向浏览器传送压缩过的内容 ,不支持则向浏览器发送未经压缩的内容 。一般情况下 ,浏览器和服务器都支持gzip,响应头返回包含content-encoding:gzip 。浏览器接收到服务器的响应之后判断内容是否被压缩 ,如果被压缩则解压缩显示页面内容。
对哪些文件压缩 ,采用哪种压缩算法 ,这个需要测试权衡 ,毕竟压缩文件和解压文件都是需要时间的 ,对于相对大点的文件一般会有收益 。
webpack压缩参与的步骤在于将文件预压缩 ,当请求到来时直接响应已经压缩的文件 ,而不需要先压缩再响应 。
使用compression-webpack-plugin插件对打包结果进行预压缩 ,可以移除服务器的压缩时间 。
const CmpressionWebpackPlugin = require("compression-webpack-plugin") module.exports = { plugins: [ new CmpressionWebpackPlugin({ // filename: "[file].gzip" test: /\.js/, //针对需要预压缩的文件 minRatio: 0.5 //压缩比率 }) ] };以gzip为例 ,打包之后的文件包含了.js和.js.gz文件 。
辅助工具
ESlint:检查JS代码规范官网:https://eslint.org/
民间中文网:https://eslint.bootcss.com/
bundle-analyzer:生成代码分析报告,帮助提升代码质量和网站性能运行性能
运行性能是指 ,JS代码在浏览器端的运行速度 ,它主要取决于我们书写代码质量的高低 。
关于高性能的代码,可以参考常见的设计模式 、代码规范 、最佳实践等 。
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!