web performance optimization

性能指标

User-centric Performance Metrics 一文中,共提到了 4 个页面渲染的关键指标。

指标 用户体验 描述
FP 它在发生吗? 网页浏览顺利开始了吗?服务端有响应吗?
FCP 它是否有用? 用户是否能看到足够的内容?
FMP 它是否可用? 用户是否可以和页面交互,还是页面仍在忙于加载?
TTI 它是否令人愉快的? 交互是否流程和自然,没有卡段或闪烁?

此外,性能优化的时候经常也会讨论到以下指标:

  1. First View(第一个视图):浏览器中的缓存和Cookie全部清空时,第一次访问该页面
  2. Repeat View(重复试图):首次视图测试结果完成后,不清除任何数据,再次访问此页面的测试结果
  3. Load Time(整页加载时间):从初始化请求,到加载所有静态内容(图片、CSS、JavaScript等)完成
  4. First Byte(首字节时间):从用户开始导航到页面直到服务器响应的第一位到达的时间。大部分时间通常称为“后端时间”,并且是服务器为用户构建页面所花费的时间量
  5. Start Render(开始渲染):页面上显示内容的第一个时间点,在这之前显示一个空白页
  6. Speed Index(加载速度指数):可见页面加载的视觉进度,并计算内容绘制的总速度,以毫秒为单位
  7. DOM Elements(DOM元素数量):测试结束后,页面上的DOM元素个数
  8. Document Complete(文档加载完成):从初始化请求,到加载所有静态内容(图片、CSS、JavaScript等)完成,可以理解为开始执行window.onload
  9. Full Loaded(所有元素加载完成):从初始化请求,到文档加载完成,2秒内没有网络请求的时间,包括在主网页加载后由JavaScript触发的任何活动
  10. Requests(HTTP请求数):整个页面的请求数
  11. Bytes in(传输的字节量):加载页面下载的数据量,一般指页面大小

性能测量

本地开发

LighthouseWeb Page Test 为我们本地开发提供了非常好的性能测试工具,而且对于我们前面提到的各项测量标准都有较好的支持。

用户设备

在用户设备上,我们可以通过性能 API (PerformanceObserver, PerformanceEntry, 以及 DOMHighResTimeStamp),进行测量。

1.测量 FP/FCP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 性能度量结果对象数组
const metrics = [];

if ('PerformanceLongTaskTiming' in window) {
const observer = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
const metricName = entry.name;
const time = Math.round(entry.startTime + entry.duration);
metrics.push({
eventCategory: 'Performance Metrics',
eventAction: metricName,
eventValue: time,
nonInteraction: true
});
}
});
observer.observe({ entryTypes: ['paint'] });
}

2.测量 FMP
由于页面主视觉元素没有固定规范,需要开发者自行约定,因此无法直接通过调用某个接口来获取 FMP,我们可以通过 performance.mark 来标记关键节点的方式来大致计算 FMP 时间。

1
2
3
4
5
<link rel="stylesheet" href="/sheet1.css">
<link rel="stylesheet" href="/sheet4.css">
<script>
performance.mark("stylesheets done blocking");
</script>
1
2
3
4
5
<img src="hero.jpg" onload="performance.clearMarks('img displayed'); performance.mark('img displayed');">
<script>
performance.clearMarks("img displayed");
performance.mark("img displayed");
</script>
1
2
3
4
<p>This is the call to action text element.</p>
<script>
performance.mark("text displayed");
</script>
1
2
3
4
5
6
7
8
9
function measurePerf() {
var perfEntries = performance.getEntriesByType("mark");
for (var i = 0; i < perfEntries.length; i++) {
console.log("Name: " + perfEntries[i].name +
" Entry Type: " + perfEntries[i].entryType +
" Start Time: " + perfEntries[i].startTime +
" Duration: " + perfEntries[i].duration + "\n");
}
}

3.测量 TTI
可以采用谷歌提供的 tti-polyfill。

1
2
3
4
5
6
7
8
9
10
import ttiPolyfill from './path/to/tti-polyfill.js';

ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
ga('send', 'event', {
eventCategory: 'Performance Metrics',
eventAction: 'TTI',
eventValue: tti,
nonInteraction: true,
});
});

4.测量 Long Tasks

1
2
3
4
5
6
7
8
9
10
11
12
onst observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
ga('send', 'event', {
eventCategory: 'Performance Metrics',
eventAction: 'longtask',
eventValue: Math.round(entry.startTime + entry.duration),
eventLabel: JSON.stringify(entry.attribution),
});
}
});

observer.observe({entryTypes: ['longtask']});

秒开

在我们的业务中,我们希望 Web 页面能够像小程序一样,能够给人秒开的体验,因此我们会更关注秒开率。

秒开标准:1s之内完成首屏页面框架和主要元素的布局,主要元素个数/位置/大小处于稳定状态;同步加载的图片可见,异步加载的图片不保证可见。主要元素指除浮层、弹窗、骨架屏/骨架模块之外的元素,不关心是否进入可交互状态。

秒开的计算方式为从触发导航到页面首次有效绘制 (FMP) 的时长。

timing

根据 performance API,我们可以使用 performance.timing.navigationStart 来代表页面加载开始时间,因此秒开计算方式:FMP - performance.timing.navigationStart < 1s

FMP 时间在不同页面实现中计算方式会有差别:

  1. 符合首屏内容直出、不经二次请求获取首屏数据的页面。近似等同 FCP 时间,具体实现要调整精度。
  2. 完全前后端分离,首屏内容需经由二次请求获取的页面。首屏DOM最后一次变化的时间。

为了达到秒开,关键在于如何优化 FMP 。

性能优化

最简单的优化性能的方式是减少需要传输给客户端的 js 代码和简化页面的 HTML 结构。但是如果我们已经无法缩小 js 代码体积,那就需要思考如何传输我们的 js 代码。

优化 FP/FCP

  • 移除影响 FP/FCP 的 CSS 和 JS 代码
  • 将影响首屏渲染的关键 CSS 代码直接通过 inline 的方式写入 中;
  • 对 React/Vue/Angular 这种客户端渲染框架,做 SSR 或者 CSR;
  • 本地缓存
  • 预加载

优化 FMP/TTI

  • 首先需要确定页面中的最关键元素,例如专题中的视频组件,然后需要保证关键组件相关的代码最先加载并且使得关键组件在第一时间被渲染且可交互;
  • 图片懒加载,组件懒加载;
  • 其他一些对渲染关键组件无用的代码可以延缓加载
  • 减少 html dom 个数和层数
  • 尽量缩减 FMP 和 TTI 的时间间隔,最好让用户知道当前页面并未完全可交互。如果用户想要交互但是页面没有响应,那么用户会感到不爽

防止 long tasks

  • 将代码分割,在需要调用的时候才进行加载。不仅能加快页面交互时间,而且可以减少 long tasks
  • 对于执行时间特别长的代码,可以尝试让他们分为几个异步执行的代码块

Webpack提供一种动态组件的功能,如果项目是通过Webpack进行打包,可以直接通过以下方式进行代码拆分:

1.动态注册 Component

1
2
3
4
5
6
7
8
9
10
11
Vue.component('dynamic-component', function (resolve) {
// 通过 `require` 语法引进的文件
// webpack会自动将其封装到单独的文件
// 然后通过 JSONP 的方式,动态添加到文档中
require(['./dynamic-component'], resolve)
})

或者
components: {
'dynamic-component': () => import('./dynamic-component')
}

2.动态注册 Store

1
2
3
created() {
this.$store.registerModule('dynamicStore', DynamicStore, { preserveState: true });
},

参考文章

  1. 前端黑科技:美团网页首帧优化实践
  2. 前端性能量化标准