为什么做性能优化?
"从 2018 年 Google I/O 大会上提供的数据发现,页面加载时长 是影响用户体验的最大因素,页面加载时长不仅对用户体验有影响,还对业务数据有影响,当页面加载时长超过 5s 就会造成 90% 的用户流失 " --- 《从零开始建立性能优化知识体系》
再不了解性能优化就out了🙀
一位资深hr说道:"性能优化 在面试中经常拿来提问,拿区分度来说:如果罗里吧嗦列出一堆优化方式,可能就是背的八股或者缺乏自己的理解;若有自己的理解并归类总结讲出本质,或有实际项目列出各种优化收益指标,就能达到良好 ;而如果能定制行业标准、自研优化方案,那就能拿到优秀了。"
(ps:作者也是为了在面试之中,能够顶住拷打而开始了对项目的优化💪)
性能诊断💉
FP(First Paint)
我们可以使用 performance 来录制分析我们详细的加载请求资源过程,其中我们可以看到 FP
,它在 FCP
的前面,即表示渲染出第一个像素点。也就是说它是我们 白屏时间 的一个结束位置。
首先看看未优化前的 FP
:
优化 FP
有很多方法,比如骨架屏、进度条等等,这里我们采用进度条
css
pnpm i nprogress -S
pnpm i @types/nprogress -D
主要是配合路由守卫来显示其效果,在我们进行鉴权逻辑判断时开始进度条,允许路由跳转后进度条结束,以下是代码示例片段:
javascript
import router from "@/router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
router.beforeEach(async (to, from, next) => {
NProgress.start();
const hasToken = localStorage.getItem("accessToken");
if (hasToken) {
next({ path: "/" });
NProgress.done();
}
});
router.afterEach(() => {
NProgress.done();
});
优化后的 FP
:2175.9ms ->802.8ms
FCP(First Contentful Paint)
FCP
表示渲染出 第一个内容,这里的"内容"可以是文本、图片、canvas。
我们还可以在浏览器开发者工具中使用 Lighthouse 为项目体检一下,其中较为重要的就是 FCP
,即当用户访问网页 到 浏览器开始将页面内容呈现给用户的时间,FCP
的值越小,表示页面内容首次呈现给用户的速度越快,用户感知的加载速度也会更好。因此,对于网页性能优化来说,降低 FCP
时间是一个重要的目标
"下面我们将进行一系列优化方案来提高我们的性能!"
依赖分析工具🔍
rollup-plugin-visualizer
是一个用于 Rollup 的插件,用于生成可视化的依赖包分析报告stats.html
,可以通过该图表来查看和定位各个模块的体积,找出可能存在的冗余代码或者体积过大的模块
bash
npm i rollup-plugin-visualizer -D
typescript
import { visualizer } from 'rollup-plugin-visualizer'
plugins: [
visualizer(),
]
我们可以看到echarts
这个包占据了我们项目依赖将近一半 的空间!很可能是开发时偷懒全局引入了!!那么我们可以把它改为按需引入
优化后:
Vite优化⚡
打包优化 --- 工具🔨
- esbuild 是一个快速且功能丰富的 JavaScript 打包工具,可以将多个模块打包成一个最终的构建文件,并提供了代码转换和优化的功能。你可以使用 esbuild 来处理模块的转换、压缩以及其他优化。
- **terser **是一个专门用于 JavaScript 代码压缩和优化的工具。它可以去除代码中的空格、注释、不必要的字符,并进行变量名缩短等优化操作,使最终的代码更加精简和高效。
- rollup 是一个 JavaScript 模块打包器,它可以将多个模块打包成一个或多个最终的构建文件。它提供了处理模块之间依赖关系、引入外部库和插件等功能。
我们使用 Terser
、Rollup
和 Esbuild
(默认) 进行配合打包,相互配合取长补短 。
在 build 配置中,minify 选项设置为 "terser",表明使用 Terser 进行代码压缩。(Vite 版本在 2.6.x 以上需要配置 minify: "terser"
和terserOptions
才能生效)。
此外,还可以根据需求在 rollupOptions 中进行更高级的构建配置。Rollup 是一个灵活而强大的打包工具,可以帮助你配置文件入口、输出路径、自定义代码拆分策略等等。
总结:Terser 进行代码压缩,Rollup 进行更高级的构建配置,而 Esbuild 则作为 Vite 默认的打包工具来处理模块打包过程。
打包优化 --- 方法🔑
chunk 分割
hunkFileNames
选项指定了生成的引入文件的名称格式entryFileNames
选项指定了包的入口文件的名称格式assetFileNames
选项指定了资源文件(如字体、图片等)的名称格式。manualChunks
对象进行配置,自定义代码分割规则。
manualChunks
是一个函数,用于手动指定哪些代码块需要被单独打包。在这个示例中,如果某个模块的路径中包含"node_modules"关键字,它将被作为 vendor(供应商)代码块打包。
typescript
rollupOptions: {
output: {
chunkFileNames: "js/[name]-[hash].js", // 产生的 chunk 自定义命名
entryFileNames: "js/[name]-[hash].js", // 指定 chunks 的入口文件匹配模式
assetFileNames: "[ext]/[name]-[hash].[ext]", // 自定义构建结果中的静态资源名称,资源文件像 字体,图片等
manualChunks(id) {
if (id.includes("node_modules")) {
return "vendor";
}
},
}
}
具体来说,自定义文件命名可以优化缓存策略 。当文件名中的内容发生变化时,浏览器会认为资源已更新,从而重新下载。通过使用带有 hash
的文件名,可以确保每次文件内容更改时都会生成不同的文件名,从而强制浏览器重新下载更新后的文件,防止缓存旧版本的文件。
另外,手动分割 Chunk 可以帮助优化资源的加载方式 。例如,在将第三方库单独提取为一个 vendor Chunk
后,这个 Chunk 在应用更新时一般情况下不会改变,可以设置长期缓存,减少重复下载,从而减少了网络请求并提升加载性能。根据需求合理设置 Chunk 的大小和数量,可以使浏览器可以更好地并行下载资源,并提高页面渲染的速度和用户体验。
总而言之,通过自定义文件命名和手动分割 Chunk,可以优化浏览器缓存策略和资源加载方式,从而达到更快的资源请求和加载速度,提升网页性能和用户体验。
gzip压缩
vite-plugin-compression
是一个用于 Vite 的插件,用于在构建时对静态资源进行压缩和编码。
该插件主要用于优化生产环境下的资源加载速度,通过使用压缩算法(如 Gzip、Brotli)对静态资源进行压缩,以减少文件大小,从而加快资源的传输速度。
注意浏览器是否支持gzip,在请求头中的 Accept-Encoding
判断是否支持gzip压缩。
bash
npm i vite-plugin-compression -D
typescript
import viteCompression from "vite-plugin-compression";
viteCompression({
algorithm: "gzip", // 压缩算法,默认为'gzip'
ext: ".gz", // 压缩文件扩展名,默认为'.gz'
threshold: 10240, // 文件大小超过threshold时才会进行压缩,默认为10KB (10240 bytes)
// 为了查看dist文件夹能减少多少体积可先设置为true
deleteOriginFile: false, // 是否删除原始文件,默认为false
verbose: true, // 是否在控制台显示压缩信息,默认为true
}),
高于threshold: 10240
(10kb) 的包都进行了gzip压缩
剔除调试语句
typescript
return defineConfig({
build: {
target: "es2020",
minify: "terser", // Vite 2.6.x 以上需要配置 minify: "terser", terserOptions 才能生效
terserOptions: {
compress: {
// 生产环境时移除console
drop_console: true,
drop_debugger: true,
},
},
}
})
打包优化 --- 效果✨
deleteOriginFile: true
的情况下我们可以清晰看到文件体积缩减了大半,优化前后, dist
对比:
3.06M -> 1.20M
浏览器在支持gzip压缩的情况下,会自动加载gzip压缩过的文件而不是源文件,所以为了项目的正常运行我们一般将
deleteOriginFile
设为false,即不删除源文件。
看看 Lighthouse 的结果:
性能:74分 -> 89分
FCP:1.9s -> 1.3s
LCP:2.4 -> 1.6s
SI:2.8s -> 1.7s
总结
- 对依赖包的分析:找出可能存在的冗余代码或者体积过大的模块;
- 选择合适的打包工具:Terser 进行代码压缩,Rollup 进行更高级的构建配置,而 Esbuild 则作为 Vite 默认的打包工具来处理模块打包过程。相互配合、取长补短
- 尝试优化方案:chunk分割、gzip压缩、剔除调试语句 。。。
当然还有许多优化的方案,在此就不一一列举,图片懒加载(intersectionObserver) 、路由懒加载 、
组件懒加载、CDN引入静态资源 、小图片使用精灵图或base64压缩 等等。
这是我的第一次对项目进行打包优化的一次记录,作者水平有限,如有错误欢迎在评论区指正,一起讨论!😺
相关配置
typescript
import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import { visualizer } from "rollup-plugin-visualizer";
import viteCompression from "vite-plugin-compression";
// https://vitejs.dev/config/
export default ({ mode }: any) => {
return defineConfig({
build: {
target: "es2020",
minify: "terser", // Vite 2.6.x 以上需要配置 minify: "terser", terserOptions 才能生效
terserOptions: {
compress: {
// 生产环境时移除console
drop_console: true,
drop_debugger: true,
},
},
// rollup 配置
rollupOptions: {
output: {
chunkFileNames: "js/[name]-[hash].js", // 产生的 chunk 自定义命名
entryFileNames: "js/[name]-[hash].js", // 指定 chunks 的入口文件匹配模式
assetFileNames: "[ext]/[name]-[hash].[ext]", // 自定义构建结果中的静态资源名称,资源文件像 字体,图片等
manualChunks(id) {
if (id.includes("node_modules")) {
return "vendor";
}
},
},
plugins: [
viteCompression({
algorithm: "gzip", // 压缩算法,默认为'gzip'
ext: ".gz", // 压缩文件扩展名,默认为'.gz'
threshold: 10240, // 文件大小超过threshold时才会进行压缩,默认为10KB (10240 bytes)
deleteOriginFile: false, // 是否删除原始文件,默认为false
verbose: true, // 是否在控制台显示压缩信息,默认为true
}),
],
},
},
// 配置路径别名
plugins: [
vue(),
visualizer(),
],
// 优化依赖项(即第三方库)的加载和打包,使它们在预构建阶段单独处理并缓存,
// 加快开发时的启动速度
optimizeDeps: {
// 用于指定需要优化的依赖包的名称或路径
include: [
"vue",
"vue-router",
"pinia",
"axios",
"element-plus/es/components/form/style/css",
"element-plus/es/components/form-item/style/css",
"element-plus/es/components/button/style/css",
"element-plus/es/components/input/style/css",
"@vueuse/core",
"path-to-regexp",
"echarts",
"@wangeditor/editor",
"@wangeditor/editor-for-vue",
"vue-i18n",
"codemirror",
],
},
});
};