背景
性能是用户体验的重要组成部分,加载时间长、响应卡顿的页面几乎没有用户想用,因此保持“不错”的页面性能是开发中非常重要的部分,本文的目标是介绍如何量化网页的性能,以及介绍一些通用的性能优化思路
统计哪些指标
简单的观感判断页面是否卡顿过于主观了,我们需要用具体的数据来体现一个页面的性能,下面是一些常用的衡量页面性能的指标。
FCP (First Contentful Paint)
首次内容绘制 (FCP) 用于衡量从用户首次导航到网页到网页内容的任何部分在屏幕上呈现的时间。对于此指标,选择“内容”是指文本、图片(包括背景图片)、<svg> 元素或非白色 <canvas> 元素。FCP反映了页面的初步加载性能,FCP短意味着白屏时间短,减少用户的等待时间。
1.8s之内为性能良好,超过3.0s表现为性能不佳
FCP示意图,FCP发生在第二帧
LCP (Largest Contentful Paint)
最大内容绘制,LCP 会报告视口中可见的最大图片、文本块或视频的呈现时间(相对于用户首次导航到相应网页的时间)。LCP表示用户需要花多久能看到页面的主要内容,是衡量页面性能的重要指标。
2.5s以内为性能良好,超过4s为性能不佳
LCP示意图
CLS(Cumulative Layout Shift)
每当可见元素在渲染帧中发生位置改变时,就会发生布局偏移,CLS 衡量的是页面整个生命周期内发生的布局偏移的累计得分。CLS高表示这个页面不够稳定,例如一个列表下放有一个新增按钮,每点一下,列表新增一项,如果按钮也向下移动一项的高度,这就会增加CLS
cls小于0.1为良好,大于0.25为不良
INP (Interaction to Next Paint)
INP 是一项指标,通过观察用户访问网页期间发生的所有点击、点按和键盘互动的延迟时间,评估网页对用户互动的总体响应情况。最终 INP 值是观测到的最长互动时间,离群值会被忽略。INP衡量的是网页的响应时间,越小,响应越快。
INP小于200ms为网页响应速度良好,大于500ms为响应速度很慢
为什么不用load和domContentLoaded
load 和 DOMContentLoaded 这些较旧的指标效果不佳的原因主要有以下几点:
load事件:
-
- 该事件在页面及其所有资源(包括图片、样式表、脚本等)完全加载后触发。这意味着即使用户的关键内容已经渲染,
load事件可能仍然延迟到所有资源加载完成后才触发。这导致用户在视觉上可能已经看到内容,但仍在等待其他资源加载。
- 该事件在页面及其所有资源(包括图片、样式表、脚本等)完全加载后触发。这意味着即使用户的关键内容已经渲染,
DOMContentLoaded事件:
-
- 该事件在 DOM 完全加载和解析后触发,但不等待样式表、图片和子框架等资源的加载。这意味着即使 DOM 结构已经准备好,用户仍然可能看到空白或未样式化的内容,影响用户体验。
- 与用户体验的关联性差:
-
- 这两个事件并不能准确反映用户看到的内容何时可用。例如,用户可能在
DOMContentLoaded事件触发时看到一个空白页面,直到样式和内容完全加载。因此,它们无法真实反映用户体验的质量。
- 这两个事件并不能准确反映用户看到的内容何时可用。例如,用户可能在
- 新指标的出现:
-
- 现代的性能指标如 FCP(首个内容绘制)、LCP(最大内容绘制)和 CLS(累积布局偏移)等更关注用户实际看到的内容和交互体验。这些指标能够更好地反映用户在使用页面时的感知性能,从而提供更有效的性能优化方向。
如何统计这些指标
使用Performance手动统计
统计FCP
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
console.log('FCP candidate:', entry.startTime, entry);
}
}).observe({type: 'paint', buffered: true});
统计LCP
// 记录 LCP 时间
const lcpObserver = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('LCP 时间:', entry.startTime.toFixed(2), '毫秒');
}
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
使用web-vitals自动统计
安装web-vitals
npm install web-vitals
使用
import {onLCP, onINP, onCLS} from 'web-vitals';
onCLS(console.log);
onINP(console.log);
onLCP(console.log);
怎么进行优化
通用的优化方式
提高资源请求速度
- 提高网络加载速度
-
- 使用cdn
- 使用http2
- 使用http缓存,强缓存/协商缓存
- 重要的资源使用preload,声明预先加载
<link rel="preload" href="ui.js" as="script" />
-
- 请求资源可以通过fetch priority划分优先级
- 减小请求资源的大小
-
- 移除无用的JavaSript、css或其他资源
- 压缩JavaScript,例如开启gzip压缩
- 应用按照路由拆分代码
提高渲染速度
- 避免dom过大
-
- 从需求上进行更改,或者使用v-if
- 避免阻塞主线程
-
- 加缓存,computed/useMemo
- web worker
- 需要移动元素,使用transform,而不是改变top/left等
使用lighthouse针对性优化
网页性能评分
可以优化的点
一次简单的实践
目前有一个正在开发的博客应用,开发过程比较自由,没有刻意的进行优化,使用lighthouse查看它目前的性能表现,目前性能表现比较糟糕
可以优化的点:
可以看到主要问题是文件没有压缩,存在未使用的Js(应该是没有路由懒加载,导致当前页面存在未使用的js),目前这个打包没有做任何处理,所有js都打包成一个文件,可以从network中看出,这个包加载花费了很长时间,因此从减小包的体积入手进行优化
路由懒加载
按需加载,提高首屏幕速度
const Home = () => import('../pages/home/index.vue');
const About = () => import('../pages/about/index.vue');
const Log = () => import('../pages/log/index.vue');
const routes = [
{ path: '/', redirect: '/home' },
{ path: '/home', name: 'path', component: Home },
{ path: '/about', name: 'about', component: About },
{ path: '/log', name: 'log', component: Log },
];
拆包
把一些大的包拆出来,充分利用浏览器并行加载的能力,使用vite-bundle-analyzer的分析功能,看看哪些较大的包能拆出来单独加载:
安装:
pnpm install vite-bundle-analyzer -D
配置:
export default defineConfig(({ mode }) => {
const isDevelopment = mode === 'development';
return {
plugins: [
// 只在开发环境使用
isDevelopment && analyzer(),
]
}
});
执行pnpm build之后,就能进行查看了
element-plus和pdf-vue3这两个包比较大,因此把它俩单独拆出来
export default defineConfig(({ mode }) => {
const isDevelopment = mode === 'development';
return {
// 省略其他
build: {
rollupOptions: {
output: {
manualChunks(id) {
// 例如,将 'lodash' 和 'axios' 拆分为独立的包
if (id.includes('node_modules/element-plus')) {
return 'element-plus'; // 生成 lodash.js
}
if (id.includes('node_modules/pdf-vue3')) {
return 'pdf-vue3'; // 生成 axios.js
}
},
},
},
},
};
});
gzip压缩
使用gzip压缩能明显降低文件的体积,进而提高加载速度,使用vite-plugin-compression开启gzip压缩:
pnpm install vite-plugin-compression -D
配置:
export default defineConfig(({ mode }) => {
const isDevelopment = mode === 'development';
return {
plugins: [
// 省略其他
compression({
// 压缩算法,支持 'gzip' 或 'brotli'
algorithm: 'gzip',
// 是否在构建时删除原文件
deleteOriginFile: false,
// 其他可选配置
threshold: 10240, // 只对大于10KB的文件进行压缩
ext: '.gz', // 生成的文件后缀
}),
],
};
});
nginx服务器进行相应的配置,以便浏览器能正确接收gzip文件
http {
include mime.types;
default_type application/octet-stream;
# 启用 Gzip 压缩
gzip on;
gzip_vary on; # 在响应头中添加 Vary: Accept-Encoding
gzip_comp_level 6; # 压缩级别(1-9,越高压缩率越好,但消耗 CPU 更多)
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000; # 只有大于这个长度的响应才会被压缩
}
结果
打包后的文件变多,http1.1支持不了同时并行那么多请求,这是后续可以继续优化的点,鉴于目前性能指标“还不错”,本次性能优化就告一段落了。