背景:

性能优化很重要,但是我们怎么知道自己的网页性能如何呢?如何测算?不少人会说,我打开很快呀,不到1秒,但是别人呢?

要正确地分析我们网页应用的性能指标,这种方法比较靠谱的呢!

横轴代表加载时间,纵轴代表用户数(其实还可以加上地区等指标的筛选,It’s up to you!).只有这样我们才能避免坐井观天,看到我们的应用在所有用户中的加载情况。

性能指标

我们知道,用GA可以采集很多信息,包括页面停留时间,DOM加载时间,以及页面加载时间等等,但是,评判一个网页的性能,是有优先级之分的。

Any performance metric that values all the content the same is not a good metric.

有哪些指标是我们需要关注的呢?当用户导航到网页时,通常会寻找视觉反馈,以确信一切符合预期。
| 体验 | 指标 | 具体情况 |
| ——– | —– | —- |
| 是否发生? | 首次绘制 (FP)/首次内容绘制 (FCP) | 导航是否成功启动?服务器是否有响应?
| 是否有用? | 首次有效绘制 (FMP)/主角元素计时 | 是否已渲染可以与用户互动的足够内容?|
| 是否可用? | 可交互时间 (TTI) | 用户可以与页面交互,还是页面仍在忙于加载?|
| 是否令人愉快? | 耗时较长的任务(Long Task) | 交互是否顺畅而自然,没有滞后和卡顿?|

下列加载时间线屏幕截图有助于您更直观地了解加载指标对应的加载体验:(lighthouse检查结果截图)

从左到右可以看到,直到第三张图页面才不是空白,所以

  1. FP => 第三张图
  2. FCP => 第四张图
  3. FMP => 第五张图(假设转盘为主角,转盘代码开源)
  4. TTI => 也是第五张图(不过不一定的呢,这个可能等所有sync js加载完才知道)

我们可以用LighthouseWeb Page Test 等这些工具用于在功能发布前测试其性能,但这些工具并未在用户设备上运行,因此未反映出用户的实际性能体验。

这里我们用几个新增的API,他们是PerformanceObserverPerformanceEntryDOMHighResTimeStamp 以及 performance

以下是PerformanceEntry的可选项:

跟踪FP/FCP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// `name` will be either 'first-paint' or 'first-contentful-paint'.
const metricName = entry.name;
const time = Math.round(entry.startTime + entry.duration);
//发送数据到后台
request.post({
eventCategory:'Performance Metrics',
eventAction: metricName,
eventValue: time,
nonInteraction: true,
})
}
});
observer.observe({entryTypes: ['paint']});

可以看到用PerformanceObserver观察entryTypes为paint,也就是FP/FCP的timing.每一个entry是一个PerformanceEntry对象。

跟踪FMP

确定页面上的主角元素之后,您可以跟踪为用户呈现这些元素的时间点。
记住!无论用什么方法,你应该尽快让主角元素呈现出来

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>

假如我们的主角元素是一张图片,这时候我们在图片加载出来前先mark一下,然后在onload时间在mark一下,两者再相减,就可以知道我们主角元素出来的用时了

跟踪TTI

这里我们使用tti-polyfill

1
2
3
4
5
6
7
8
9
10
 import ttiPolyfill from 'tti-polyfill';

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

getFirstConsistentlyInteractive() 方法接受可选的 startTime 配置选项,让您可以指定下限值(您知道您的应用在此之前无法进行交互)。 默认情况下,该 polyfill 使用 DOMContentLoaded 作为开始时间,但通常情况下,使用主角元素呈现的时刻或您知道所有事件侦听器都已添加的时间点这类时间会更准确。

跟踪Long tasks

要在 JavaScript 中检测耗时较长的任务,请创建新的 PerformanceObserver,并观察类型为 longtask 的条目。 耗时较长的任务条目的一个优点是包含提供方属性,有助于您更轻松地追查导致出现耗时较长任务的代码:

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

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

跟踪输入延迟

若要在代码中检测输入延迟,您可将事件时间戳与当前时间作比较,如果两者相差超过 100 毫秒,您可以并应该进行报告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const testBtn = document.querySelector('#testBtn');

testBtn.addEventListener('click', (event) => {
// Event listener logic goes here...

const lag = performance.now() - event.timeStamp;
if (lag > 100) {
request.post({
eventCategory:'Performance Metric'
eventAction: 'input-latency',
eventLabel: '#subscribe:click',
eventValue:Math.round(lag),
nonInteraction: true,
});
}
});

记录丢失的用户

有些时候,用户会因为页面加载时间过长而选择离开,但是他到底忍受了多久呢?
我们可以用visibilitychange(该事件在页面卸载或进入后台时触发)记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
window.trackLeaver = () => {
// Remove the listener so it only runs once.
document.removeEventListener('visibilitychange', window.trackLeaver);

// Send the data to Google Analytics via the Measurement Protocol.
navigator.sendBeacon && navigator.sendBeacon(ANALYTICS_URL, [
'v=1', 't=event', 'ec=Load', 'ea=abandon', 'ni=1',
'dl=' + encodeURIComponent(location.href),
'dt=' + encodeURIComponent(document.title),
'tid=' + TRACKING_ID,
'cid=' + CLIENT_ID,
'ev=' + Math.round(performance.now()),
].join('&'));
};
document.addEventListener('visibilitychange', window.trackLeaver);
</script>

其他指标

1.采集css/js的加载时间

直接在所有css文件后加一下performace.mark就可以了, 不过内联的css或者async js都不行哦。

2.采集字体的加载时间

使用 fontfaceobserver,

1
2
3
4
5
6
7
performance.clearMarks("img displayed");
performance.mark("img displayed");
new w.FontFaceObserver( "lato" )
.check()
.then( function(){ console.log( “Loaded!” );
performance.clearMarks("img displayed");
performance.mark("img displayed");});

方法类似于采集主角元素的加载时间。

3.采集用户Dom渲染的时间

参考 React16.9 Profiler,

1
2
3
4
5
6
7
8
9
10
11
function onRenderCallback(
id, // the "id" prop of the Profiler tree that has just committed
phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
actualDuration, // time spent rendering the committed update
baseDuration, // estimated time to render the entire subtree without memoization
startTime, // when React began rendering this update
commitTime, // when React committed this update
interactions // the Set of interactions belonging to this update
) {
// Aggregate or log render timings...
}

在RenderCallback里面上报就可以了。

总结

我们做优化一定是为了有效果,如果我们做出来最后的效果还不如以前就坏了,所以我们要哈哈自己先测试,然后先灰度。

这里方法只是为了有机会帮助我们制定优化策略,要取决于我们的人力,时间,技术等成本。

最后就是可能后面会做一个统一这些方法的小工具,敬请期待吧~~

参考文章

  1. https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics
  2. https://speedcurve.com/blog/user-timing-and-custom-metrics/
  3. https://w3c.github.io/performance-timeline/#performanceobserverinit-dictionary
  4. https://www.filamentgroup.com/lab/font-events.html
  5. https://w3c.github.io/longtasks/
  6. http://www.stevesouders.com/blog/2015/05/12/hero-image-custom-metrics/
  7. https://github.com/GoogleChromeLabs/tti-polyfill