性能优化概述
构建性能
- 构建性能 是指在 开发阶段的构建性能,而不是生产环境的构建性能;
- 优化的目标:是降低从打包开始,到代码效果呈现所经过的时间;
- 构建性能会影响开发效率,构建性能越高,开发过程中时间的浪费越少;
传输性能
- 传输性能 是指打包后的 JS 代码传输到浏览器经过的时间;
- 在优化传输性能时要考虑到:
- 总传输量:所有需要传输的 JS 文件的内容加起来,就是总传输量,重复代码越少,总传输量越少;
- 文件数量:当访问页面时,需要传输的 JS 文件数量越多,http 请求越多,响应速度越慢;
- 浏览器缓存:JS 文件会被浏览器缓存,被缓存的文件不会再进行传输;
运行性能
- 运行性能是指 JS 代码在浏览器端的运行速度,它主要取决于如何书写高性能的代码;
- 永远不要过早的关注于性能,因为在开发的时候,无法完全预知最终的运行性能,过早的关注性能会极大的降低开发效率;
减少模块解析
-
什么叫做模块解析?模块解析包括:抽象语法树分析、依赖分析、模块语法替换
-
不做模块解析会怎样?
- 如果某个模块不做解析,该模块经过 loader 处理后的代码就是最终代码;
- 如果没有 loader 对该模块进行处理,该模块的源码就是最终打包结果的代码;
- 如果不对某个模块进行解析,可以缩短构建时间;
-
哪些模块不需要解析?一些已经打包好的第三方库,比如 jquery 等库
-
如何配置让某个模块不要解析?配置 webpack 的 module.noParse 它是一个正则,被正则匹配到的模块不会解析
// webpack.config.js module.exports = { mode: "development", devtool: "source-map", module: { noParse: /jquery/ } }
优化 loader 性能
限制 loader 应用范围
-
对于某些库来说,不需要使用 babel-loader,例如:有些库本身就是用 ES5 语法书写的,不需要转换,使用 babel-loader 转换反而会浪费构建时间;
-
配置 webpack 配置文件:以 lodash 为例,lodash 是在 ES5 之前出现的库,使用的是 ES3 语法:
方式一
:通过 module.rule.exclude 或 module.rule.include 排除或仅包含需要应用 loader 的场景;module.exports = { module: { rules: [ { test: /\.js$/, exclude: /lodash/, use: "babel-loader" } ] } }
方式二
: 暴力一点,可以排除掉 node_modules 目录中的模块,或仅转换 src 目录的模块,这种做法是对 loader 的范围进行进一步的限制,和 noParse 不冲突module.exports = { module: { rules: [ { test: /\.js$/, exclude: /node_modules/, //或 // include: /src/, use: "babel-loader" } ] } }
缓存 loader 结果
-
如果某个文件内容不变,经过相同的 loader 解析后,解析后的结果也不变,于是可以将 loader 的解析结果保存下来,让后续的解析直接使用保存的结果,cache-loader 可以实现这样的功能 cache-loader 还可以实现各自自定义的配置,具体方式见文档:
module.exports = { module: { rules: [ { test: /\.js$/, use: ['cache-loader', ...loaders] }, ], }, };
-
有趣的是 cache-loader 放到最前面,却能够决定后续的 loader 是否运行,实际上 loader 的运行过程中,还包含一个 pitch 过程;
为 loader 开启多线程
-
thread-loader 会开启一个线程池,线程池中包含适量的线程,它会把后续的 loader 放到线程池的线程中运行,以提高构建效率;
-
由于后续的 loader 会放到新的线程中,所以后续的 loader:
- 不能使用 webpack api 生成文件;
- 无法使用自定义的 plugin api;
- 无法访问 webpack options;
-
在实际的开发中,可以进行测试,来决定 thread-loader 放到什么位置;
-
特别注意:开启和管理线程需要消耗时间,在小型项目中使用 thread-loader 反而会增加构建时间;
热替换 HMR
-
热替换并不能降低构建时间 (可能还会稍微增加),但可以降低代码改动到效果呈现的时间;
-
当使用 webpack-dev-server 时,考虑代码改动到效果呈现的过程,而使用了热替换后,流程发生了变化
使用、原理
-
HMR 使用
:更改 webpack 配置module.exports = { devServer:{ hot: true // 开启 HMR }, plugins:[ // 可选,不写 webpack4 会内部会根据 devServer.hot 使用热更新 new webpack.HotModuleReplacementPlugin() ] }
-
HMR 原理
:- 当开启了热更新后 webpack-dev-server 会向打包结果中注入 module.hot 属性,默认情况下 webpack-dev-server 不管是否开启了热更新,当重新打包后,都会调用 location.reload 刷新页面,但如果运行了 module.hot.accept() 将改变这一行为;
- module.hot.accept() 的作用是让 webpack-dev-server 通过 socket 管道,把服务器更新的内容发送到浏览器,然后将结果交给插件 HotModuleReplacementPlugin 注入的代码执行,插件 HotModuleReplacementPlugin 会根据覆盖原始代码,然后让代码重新执行,所以热替换发生在代码运行期;
样式热替换
-
对于样式也是可以使用热替换的,但需要使用 style-loader,因为热替换发生时 HotModuleReplacementPlugin 只会简单的重新运行模块代码;
-
因此 style-loader 的代码一运行,就会重新设置 style 标签元素中的样式,而 mini-css-extract-plugin 由于它生成文件是在 构建期间,运行期间也无法改动文件,因此它对于热替换是无效的;
打包公共模块
什么是分包
:将一个整体的代码,分布到不同的打包文件中;
为什么分包?
:减少公共代码,降低总体积,充分利用浏览器缓存,特别是一些大型的第三方库;
什么时候分包
:多个 chunk 引入了相同的公共模块 (会将公共模块打包到多个 chunk 中),公共模块 体积较大 或 改动较少;
基本原理
-
先单独的打包公共模块,公共模块会被打包成为动态链接库 (dll Dynamic Link Library),并生成资源清单;
-
根据入口模块进行正常打包,由于资源清单中包含 jquery 和 lodash 两个模块,因此打包结果的大致格式是 (jquery、lodash源码并不会被打包进来,而是使用 全局变量的方式):
(function(modules){ //... })({ // index.js 文件的打包结果并没有变化 "./src/index.js": function(module, exports, __webpack_require__){ var $ = __webpack_require__("./node_modules/jquery/index.js") var _ = __webpack_require__("./node_modules/lodash/index.js") _.isArray($(".red")); }, // 由于资源清单中存在,jquery 的代码并不会出现在这里,否则 jquery 源码会被打包进来 "./node_modules/jquery/index.js": function(module, exports, __webpack_require__){ module.exports = jquery; // 全局变量 jquery }, // 由于资源清单中存在,lodash 的代码并不会出现在这里,否则 lodash 源码会被打包进来 "./node_modules/lodash/index.js": function(module, exports, __webpack_require__){ module.exports = lodash; // 全局变量 lodash } })
基本使用
-
具体流程:
- 开启 output.library 暴露公共模块;
- 利用 DllPlugin 生成资源清单 (示例:jquery.manifest.json 会和公共模块生成映射关系);
{ "name": "jquery", "content": { "./node_modules/jquery/dist/jquery.js": { "id": 1, "buildMeta": { "providedExports": true } } } }
- 用 DllReferencePlugin 使用资源清单,控制打包结果;
- 重新设置 clean-webpack-plugin,避免它把公共模块清除;
- 在页面中手动引入公共模块;
-
关键代码:
JavaScriptJavaScriptJSONhtml// webpack.dll.config.js const webpack = require("webpack"); const path = require("path"); module.exports = { mode: "production", entry: { jquery: ["jquery"], lodash: ["lodash"] }, output: { filename: "dll/[name].js", library: "[name]" // 1. 开启 output.library 暴露公共模块 }, plugins: [ new webpack.DllPlugin({ // 2. 利用 DllPlugin 生成资源清单 path: path.resolve(__dirname, "dll", "[name].manifest.json"), // 资源清单的保存位置 dll 文件夹下 name: "[name]" // 资源清单中,暴露的变量名 }) ] };
// webpack.config.js const HtmlWebpackPlugin = require("html-webpack-plugin"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const webpack = require("webpack"); module.exports = { mode: "development", devtool: "source-map", entry: { main: "./src/index.js", other: "./src/other.js" }, output: { filename: "[name].[hash:5].js" }, plugins: [ // 4. 重新设置 clean-webpack-plugin,避免它把公共模块清除,需要做出以下配置 new CleanWebpackPlugin({ // 要清除的文件或目录 // 排除掉dll目录本身和它里面的文件,目录和文件的匹配规则是 globbing patterns cleanOnceBeforeBuildPatterns: ["**/*", "!dll", "!dll/*"] }), new HtmlWebpackPlugin({ template: "./public/index.html" }), // 3. 使用 DllReferencePlugin 控制打包结果,这样 jquery、lodash 才不会被打包到 index.js 中 new webpack.DllReferencePlugin({ manifest: require("./dll/jquery.manifest.json") // 使用资源清单 }), new webpack.DllReferencePlugin({ manifest: require("./dll/lodash.manifest.json") // 使用资源清单 }) ] };
"scripts": { "dev": "webpack", "dll": "webpack --config webpack.dll.config.js" // 打包公共模块是一个 独立的 打包过程 },
<!-- 5. 在页面中手动引入公共模块 --> <script src="./dll/jquery.js"></script> <script src="./dll/lodash.js"></script>
优缺点
-
优点
- 极大提升自身模块的打包速度;
- 极大的缩小了自身文件体积;
- 有利于浏览器缓存第三方库的公共代码;
-
缺点
- 使用非常繁琐,麻烦 (不要对小型的公共 JS 库使用);
- 如果第三方库中包含了复杂的依赖关系,则效果不太理想;
手动分包、自动分包
分包后主包的体积减小,可以减小每个页面或模块的初始加载时间,因为用户只需下载当前页面或模块所需的代码,而不必加载整个应用程序的代码;
这降低了首次加载的时间,提高了用户体验;
手动分包
-
在 Webpack 中,可以手动分割代码块 (称为"手动分包");
-
手动分包通常通过 import() 函数动态导入实现,这样 Webpack 会将目标模块及其依赖作为一个新的分块进行打包;
JavaScriptJavaScript// eg1:动态导入 util 模块 import('./path/to/util').then(({ someFunction }) => { someFunction(); });
// eg2:vue-router 路由表 const routes = [ { path: '/main/:type', component: () => import(/* webpackChunkName: 'MenuWrap' */ '@/view/MenuWrap.vue'), }, { path: '/customLogin', component: () => import(/* webpackChunkName: 'MenuWrap' */ '@/view/custom/login.vue'), }, ]
自动分包
-
自动分割则是通过 Webpack 的 SplitChunksPlugin 插件实现的,它默认在 Webpack 中启用 (自动分割通常被称为"自动分包");
-
可以通过配置 Webpack 的 optimization.splitChunks 选项来自定义分割规则:
// webpack.config.js module.exports = { // ... optimization: { splitChunks: { chunks: 'async', // async:只分割异步代码块,all:分割全部 minSize: 30000, // 最小模块大小 maxSize: 50000, // 最大模块大小(分包的 基础单位是模块 ,如果一个完整的模块超过了该体积,它是无法做到再切割的,尽管使用了这个配置,完全有可能某个包还是会超过这个体积) minChunks: 1, // 最小共享模块次数 maxAsyncRequests: 5, // 最大异步请求数 maxInitialRequests: 3, // 最大初始化请求数 automaticNameDelimiter: '~', // 分割后的块名称分隔符 name: true, // 是否使用模块名作为块名 // webpack 默认提供了两个缓存组 cacheGroups: { // 属性名是缓存组名称,会影响到分包的 chunk 名 // 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置 vendors: { test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包 priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为 0 }, default: { minChunks: 2, // 覆盖全局配置,将最小 chunk 引用数改为 2 priority: -20, // 优先级 reuseExistingChunk: true // 重用已经被分离出去的 chunk,无需再次分之前分好的包 } } } } // ... };
单模块体积优化
代码压缩
-
在 Webpack 中可以使用 TerserPlugin 来压缩 JavaScript 代码,这个插件会通过 terser 库来移除死代码、最小化变量名、压缩表达式等;
-
Webpack 相关配置;
const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin({ terserOptions: { compress: { drop_console: true, // 移除所有的`console`语句 }, output: { comments: false, // 去掉注释 }, }, extractComments: false, // 不从代码中提取注释 }), ], }, };
tree shaking
-
本质上为了消除无用的 JS 代码,减少加载文件体积的方式,使其整体执行时间更短;因为 ES Module 模块化的出现,ES Module 模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析;
- “CommonJs 是动态的”:模块依赖关系的建立发生在代码运行阶段,require 路径可以是表达式;
- “ESModule 是静态的”:模块依赖关系的建立发生在代码编译阶段,ESModule 导入、导出语句都是声明式的,必须位于模块的顶层作用域不能放到 if 语句中;
-
当打包环境为 production 时,默认开启 tree-shaking 功能;
-
webpack 在使用 tree shaking 的时候有一个原则:一定要保证代码正确运行,在满足该原则的基础上,再来决定如何 tree shaking:
- 什么是副作用?一个函数会、或者可能会对函数外部变量产生影响的行为
- 副作用代码不可被删除,可能会导致报错或者 bug 的出现;
const setTitle = () => { document.title = "RICE-PUDDING"; } // 虽然 a 变量没有被其他地方使用,但由于副作用,如果将其删除,会导致 document.title 没有成功被设置可能导致出现 bug const a = setTitle();
- 所以要清楚的知道代码是否存在副作用,以下代码是针对不同情况的 tree-shaking,在 package.json 中设置 sideEffects:
JSONJSONJSON
// 所有文件都有副作用,全都不可 tree-shaking { "sideEffects": true }
// 没有文件有副作用,全都可以 tree-shaking { "sideEffects": false }
// 只有这些文件有副作用,所有其他文件都可以 tree-shaking,但会保留这些文件 { "sideEffects": [ "./src/file1.js", "./src/file2.js" ] }
多入口打包
-
webpack的多入口配置具有以下用途:
- 合理控制包的大小:通过多入口打包,可以避免一次性加载太多不需要的代码,从而优化项目的加载速度和性能;
- 代码复用:多入口配置允许在多个入口起点之间复用大量的代码或模块,提高代码的复用率和效率;
- 分割代码:对于大型项目,尤其是包含多个独立功能或模块的项目,如用户和管理员部分,多入口可以使得每个部分被单独打包,生成不同的文件,这样可以根据需要按需加载,进一步优化用户体验和性能;
-
总的来说 webpack 的多入口配置提供了一种灵活的方式来组织和管理大型项目的代码,使得项目结构更加清晰,同时也便于代码的复用和优化;
懒加载、预加载
-
懒载入 和 预载入 是 webpack 的两个优化策略,可以提高页面加载性能;
-
懒载入
:懒载入是指在需要的时候才加载某个模块,这样可以减少初始页面加载的大小;在 webpack 中,可以使用动态 import 函数实现懒载入;// 假设有一个组件叫做 'async-component.js' 想要懒加载它 // 在某个文件中,比如主文件 index.js function getComponent() { return import('./async-component.js').then(({ default: asyncComponent }) => { // 在这里你可以做任何对 asyncComponent 的操作,比如挂载到 DOM 等 // 比如:document.body.appendChild(asyncComponent); }); } // 可以在某个事件触发时调用 getComponent 函数来实现懒加载 document.getElementById('load-btn').addEventListener('click', getComponent);
-
预载入
:预载入是在页面加载的初期就加载一些将来可能会用到的资源,如 webpack 打包的某个模块;这样在将来需要使用时,资源已经加载好,可以立即使用,降低页面加载时的等待时间 (懒加载实现了 js 文件按需加载,在需要使用时才进行加载,但是如果 js 文件非常大加载速度比较慢,在使用时再加载就会使页面出现卡顿,这时就需要使用预加载);document.getElementById('load-btn').onclick = function () { // 在浏览器空闲的时候会自动去请求 async-component.js,即使没有触发点击事件的情况下 import(/*webpackChunkName:'test' ,webpackPrefetch:true*/"./async-component.js"); .then(({ test }) => { console.log('test加载成功'); }) .catch(error => { console.log('test加载失败 error:', error); }) }
-
在 webpack 配置文件中,还需要添加一些配置,这样可以确保 webpack 可以正确地将 懒加载、预加载 的代码分割出来;
module.exports = { // ... optimization: { splitChunks: { chunks: 'all', }, }, // ... };
gzip 压缩
-
BS 结构中的压缩传输:服务端获取资源需要先压缩,再发送给客户端 (服务器的压缩需要时间,客户端的解压需要时间);
-
使用 webpack 的 compression-webpack-plugin 插件对打包结果进行预压缩,可以移除服务器的压缩时间,下面是 webpack 的配置;
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const CmpressionWebpackPlugin = require("compression-webpack-plugin"); module.exports = { mode: "production", optimization: { splitChunks: { chunks: "all" } }, plugins: [ new CleanWebpackPlugin(), new CmpressionWebpackPlugin({ test: /\.js/, minRatio: 0.5 }) ] };
vue3🛫 TS 与组合式 API
上一篇