你可能不止一次地听大家讨论性能的话题、一个速度飞快的web 应用是多么重要。
我的网站快吗?当你试图回答这个问题的时候,你会发现快是个很模糊的概念。我们在说快的时候,我们到底指的哪些方面?是在什么场景下?对谁而言?
谈论性能的时候务必要准确,不要使用错误的概念,以免开发人员一直在错误的事情上做优化——结果没有得到优化反而损害到用户体验。
看一个具体的例子,我们经常会听到有人说:我的app测过了,加载时间是XX秒。
上述的说法并不是说它是错误的,而是它歪曲了现实。加载时间因用户而异,取决于他们的设备能力和网络环境。将只关注加载时间单一数据指标会遗漏那些加载时间长的用户。
实际上,上面所说的加载时间是一个所有用户的一个平均加载时间。只有下图所示的直方图分布图才能完全反应真实的加载时间:
X轴上的数字表示加载时间,y轴直方图的高度表示某个时间内的用户数量。如图所示,虽然大多数用户的加载时间在1~2秒,但仍有不少用户的加载时间很长。
"我的页面加载时间是xx秒"不真实的另一个原因是,页面的加载不是单一的一个时间指标——它是用户的使用体验,而这种体验是没有任何一个指标能完全捕获到的。加载过程中有多个时间指标会影响用户对页面加载是否足够快的感受,如果你只盯着加载完成时间这一个指标,那么你可能会忽视发生在其他时间点的不佳用户体验。
例如,一个应用的初始渲染优化的非常快,页面内容很快就展示给用户了。如果这个应用随后加载了一个很大的js包,并且需要耗费好几秒解析和执行,页面内容在js执行完成之前,还是没法响应用户的操作。如果用户看到一个链接却无法点击,有了文本框却没法输入,他们或许不在乎你的页面渲染有多快的。
所以不能使用单一的指标来衡量加载的速度,我们应该关注整个加载过程中的任何影响用户感受的时间指标。
第二个误区是,认为性能只是在加载时需要关注的问题。
我们作为一个团队在这方面犯了错误,并且由于多数性能检测工具只检测加载性能,这个错误还被放大了。
事实上性能问题可能在任何时间发生,不只是在加载的时候。用户的点击响应速度慢,页面不能滚动,动画不流畅同样影响体验。用户关心的是整体的体验,作为开发者我们也应如此。
这些误区的共同点是,我们关注的指标跟用户体验没有关系或者说关系很小。同样,传统的
性能指标
如页面加载时间,
DOMContentLoaded
时间是非常不可靠的。因为,当他们出现时,并不等于用户认为应用已经加载完成了。
所有为确保不重复这样的错误,我们问自己几个问题:
什么指标最准确地反映用户直观体验?
如何在真实用户中检测这些指标?
如何分享得到的数据以衡量应用是否够快?
理解了什么是应用的真实用户性能,我们如何防止性能退化?未来如何进行优化?
用户为中心的性能指标
当用户访问一个页面的时候,通常会从视觉上去感知是不是页面已经如预期地加载完成可以正常使用了。
体验 指标
是否发生?
页面是否开始跳转?服务器有没有响应?
短是否有用?文本
重要的内容是否已经渲染?
是否可用?
页面可交互了吗?或者还在加载中吗?
体验好吗?
交互是否平滑自然,没有卡顿?
为了了解页面在用户侧的在这些方面的表现,我们定义了一些指标:
1、首次绘制与首次内容绘制
Paint Timing
接口定义了两个指标:首次绘制(FP)和首次内容绘制(FCP)。这些指标记录了浏览器开始在屏幕上进行绘制的时间点。这对用户很重要,因为它回答了:”发生了吗?”这个问题。
这两个指标的主要区别是FP是页面在视觉上首次出现不同于跳转前的内容的时间点。相比之下,FCP是浏览器渲染DOM中第一个内容的时候,可能是文本,图像,SVG甚至是
<canvas>
元素。
2、首次有效绘制和主角元素计时
首次有用的绘制回答了这个问题:“它有用吗?”。“有用”这个概念没有一个标准的定义。但是对于开发者来说,找出页面上的哪些部分对用户是最重要的是很容易的。
这些网页的“最重要的部分”通常称为关键元素。例如,在YouTube观看页面上,关键元素是主视频。 在Twitter上,它可能是通知徽章和第一条推文。在天气应用中,是指定城市的天气预测。 而在新闻网站上,它可能是主要故事和精选图片。
网页上几乎总是有比其他内容更重要的部分。如果网页中最重要的部分加载速度很快,用户可能甚至不会注意到页面的其他部分是否没有加载完成。
3、耗时较长的任务
浏览器通过向主线程上的队列添加任务并逐一执行来响应用户输入。这也是浏览器执行JavaScript的地方,所以在这个意义上说浏览器是单线程的。
在某些情况下,这些任务可能需要很长时间才能运行完,这样的话主线程将被阻塞,并且队列中的所有其他任务都必须等待。
对用户而言这表现为卡顿不流畅,这也是当前页面性能差的主要原因。
长任务API能识别任何长于50毫秒的任务,它认为这存在性能隐患。通过长任务API,开发者能获取到页面中存在的长任务。选择50ms是为了遵循RAIL指南以确保100ms内响应用户的输入。
4、可交互时间
可交互时间(TTI)意味着页面渲染完成并且可以正常响应用户的输入了,可能有以下几个原因导致页面不能响应用户输入:
确保页面可交互的js没有下载完成
存在长任务(上节所述)
TTI表示页面的初始JavaScript加载完成且主线程空闲(没有长任务)的点。
5、将指标对应到用户体验
回到我们以前认为对用户体验最重要的问题,本表概述了我们刚刚列出的每个指标如何映射到我们希望优化的用户体验:
体验指标
发生了吗?
首次绘制(FP) / 首次内容绘制 (FCP)
内容重要吗?
首次有用绘制 (FMP) / 关键元素渲染时间
可以使用吗?
可交互时间(TTI)
体验好吗?
下一节将详细介绍如何在真实用户的设备上测量这些指标。
以实际用户的设备衡量这些指标
过去,我们针对 Load 和 DOMContentLoaded 等指标进行优化的一个主要原因是,这些指标在浏览器中显示为事件,而且容易针对实际用户进行衡量。
相比而言,许多其他指标在过去很难加以衡量。 例如,以下代码是开发者经常用来检测耗时较长任务的黑客手段:
(function detectLongFrame() {
var lastFrameTime = Date.now();
requestAnimationFrame(function() {
var currentFrameTime = Date.now();
if (currentFrameTime - lastFrameTime > 50) {
// Report long frame here...
detectLongFrame(currentFrameTime);
}());
此代码使用
requestAnimationFrame
循环记录每次迭代的时间。如果当前时间比前一次超过50毫秒,则认为这是长任务。 虽然这些代码起作用,但它有很多缺点:
增加了每一帧的开销。
阻止空闲时间块的出现。
影响电池寿命。 性能检测代码最重要的原则是不能使性能变得更差。
Lighthouse
和
Web Page Test
虽然提供这些新的性能指标已经有一段时间了(他们是项目发布前进行性能测试的绝佳工具),但是毕竟他们不是运行在用户设备上,还是没办法衡量web项目在用户设备上的实际性能表现。
幸运的是,浏览器提供了一些新API,这些新API使得统计真实用户设备的性能指标变得很简单,不需要再使用一些影响页面性能的变通方法。
这些新的API是
PerformanceObserver
,
PerformanceEntry
和
DOMHighResTimeStamp
。接下来我们通过一个例子,来了解一下怎么通过PerformanceObserver来统计绘制相关的性能(例如,FP,FCP)以及可能出现的导致页面阻塞的js长任务。
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// `entry` is a PerformanceEntry instance.
console.log(entry.entryType);
console.log(entry.startTime); // DOMHighResTimeStamp
console.log(entry.duration); // DOMHighResTimeStamp
// Start observing the entry types you care about.
observer.observe({entryTypes: ['resource', 'paint']});
通过
PerformanceObserver
我们可以订阅性能事件,当事件发生的时候得到相应的数据。相比老的
PerformanceTiming
接口,它的好处是以异步的方式获取数据,而不是通过不断的轮询。
统计FP / FCP
获取到某个性能数据后,可以将该用户的设备的性能数据发送到任意的数据分析服务。比如我们将首次绘制的指标发送到谷歌统计。
<!-- Add the async Google Analytics snippet first. -->
<script>
window.ga
=window.ga||
function
(){(ga.q=ga.q||[]).push(arguments)};ga.l=+
new
Date;
ga(
'create', 'UA-XXXXX-Y', 'auto'
);
ga(
'send', 'pageview'
);
</script>
<script async src='https://www.google-analytics.com/analytics.js'></script>
<!-- Register the PerformanceObserver to track paint timing. -->
<script>
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);
ga(
'send', 'event'
, {
eventCategory:
'Performance Metrics'
,
eventAction: metricName,
eventValue: time,
nonInteraction:
true
,
observer.observe({entryTypes: [
'paint'
]});
</script>
<!-- Include any stylesheets after creating the PerformanceObserver. -->
<link rel="stylesheet" href="...">
</head>
基于关键关键元素统计FMP
我们还没有FMP的标准化定义(因此也没有对应的性能类型)。这部分是因为很难有一个通用的指标来表示所有页面是“有意义的”。
但是,在单页面应用的场景下,我们可以用关键元素的显示的时间点来表示FMP。
Steve Souders有一篇名为User Timing And Custom Metrics的精彩文章,详细介绍了许多使用浏览器性能API确定何时可以看到各种类型媒体的技术。
统计 TTI
从长远来看,我们希望通过
PerformanceObserver
在浏览器中对TTI指标提供标准化的支持。 与此同时,我们开发了一种可用于检测
TTI
的polyfill,并可在任何支持长任务 API的浏览器中工作。
这个polyfill暴露了一个
getFirstConsistentlyInteractive()
方法,该方法返回一个以TTI值解析的
promise
对象。 你可以使用Google Analytics统计TTI,如下所示:
import ttiPolyfill from './path/to/tti-polyfill.js';
ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => {
ga('send', 'event', {
eventCategory: 'Performance Metrics',
eventAction: 'TTI',
eventValue: tti,
nonInteraction: true,
getFirstConsistentlyInteractive()
方法接受一个可选的startTime
配置选项,用以指定一个时间表示web应用在此时间以前不能进行交互。默认情况下,polyfill使用DOMContentLoaded
作为开始时间,但使用类似于关键元素可见的时刻或当获知已添加所有事件侦听器时的时刻,通常会更准确。
完整的安装和使用说明,请参阅TTI polyfill文档。
统计长任务
我前面提到长任务会导致一些负面的用户体验(例如,缓慢的事件处理函数,丢帧)。我们最好留意一下长任务发生的频率,以将其影响最小化。
要在JavaScript中检测长任务,请创建一个PerformanceObserver
对象并观察 longtask
类型。长任务类型的一个优点是它包含一个attribution
属性,因此可以更轻松地追踪哪些代码导致了长任务:
const 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']});
attribution
属性会告诉你什么代码导致了长任务,这有助于确定第三方iframe
脚本是否导致问题。该规范未来版本正计划添加更多粒度,并提供脚本URL,行和列号,这对确定自己的脚本是否导致缓慢很有帮助。
统计输入延迟
阻塞主线程的长任务会阻止您的事件侦听器及时执行。RAIL性能模型告诉我们,为了使用户界面感觉平滑,应该在用户输入的100毫秒内做出响应,否则,应该分析是什么原因。
要检测代码中的输入延迟,可以将事件的时间戳与当前时间进行比较,如果差异大于100毫秒,则可以(也应该)上报异常。
const subscribeBtn = document.querySelector('#subscribe');
subscribeBtn.addEventListener('click', (event) => {
// Event listener logic goes here...
const lag = performance.now() - event.timeStamp;
if (lag > 100) {
ga('send', 'event', {
eventCategory: 'Performance Metric'
eventAction: 'input-latency',
eventLabel: '#subscribe:click',
eventValue: Math.round(lag),
nonInteraction: true,
由于事件延迟通常是长任务的结果,因此你可以将事件延迟检测逻辑与长任务检测逻辑相结合:如果长任务与event.timeStamp
同时阻塞主线程,则也可以上报该长任务 attribution
值。 这可以确定导致性能体验差的的代码是什么。
虽然这种技术并不完美(它在冒泡阶段不处理长事件监听器,并且它不适用于不在主线程上运行的滚动或合成动画),但却是更好地理解长时间运行的JavaScript代码会影响用户体验的第一步。
一旦开始收集真实用户的性能指标,你需要将这些数据付诸实践。真实用户性能数据之所以重要主要是由于以下几个原因:
验证你的应用是否按预期执行。
找出性能差对转化率的影响(无论转化率对你的应用而言意味着什么)。
寻求改善用户体验的措施。 你的应用在移动设备和桌面设备上的表现绝对是值得比较的一件事。下图显示了桌面(蓝色)和移动(橙色)的TTI分布。从这个例子可以看出,手机上的TTI值比桌面上的要长很多:
虽然这里的数据是特定于应用的(你应该自己测试一下自己应用的数据),下面的例子是一个基于性能指标生成的分析报告:
PercentileTTI (seconds)
通过将数据分解成移动和桌面,并将各个终端的数据采用分布图展示,可以快速洞察真实用户的体验。 例如,看上面的表格,可以很容易看到对于这个应用,10%的移动用户花费了超过12秒的时间来交互!
性能如何影响业务
加载过程用户跳出
我们知道,如果页面加载时间过长,用户通常会离开。这意味着我们所有的性能指标都存在生存偏差的问题,其中的数据并不包括那些没有等待页面完成加载的用户的指标。
虽然不能获取这些用户滞留的数据,但可以获取这种情况发生的频率以及每个用户停留的时间。
这对于使用Google Analytics
来说有点棘手,因为analytics.js
库通常是异步加载的,并且在用户决定离开时可能不可用。 不过,在向Google Analytics
发送数据之前,您无需等待analytics.js
加载。 您可以通过Measurement Protocol
直接发送它。
此代码监听一个visibilitychange
事件(如果当前页面进入后台运行或者页面关闭触发该事件),当事件触发时发送performance.now()
的值。
<script>
window.__trackAbandons = () => {
// Remove the listener so it only runs once.
document.removeEventListener('visibilitychange', window.__trackAbandons);
const ANALYTICS_URL = 'https://www.google-analytics.com/collect';
const GA_COOKIE = document.cookie.replace(
/(?:(?:^|.*;)\s*_ga\s*\=\s*(?:\w+\.\d\.)([^;]*).*$)|^.*$/, '$1');
const TRACKING_ID = 'UA-XXXXX-Y';
const CLIENT_ID = GA_COOKIE || (Math.random() * Math.pow(2, 52));
// 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.__trackAbandons);
</script>
你可以将此代码复制到文档的<head>
中,并使用你的track ID
替换UA-XXXXX-Y
占位符。
你还需要确保在页面变为可交互时删除此监听器,否则你上报TTI的时候会误将放弃加载等待业上报。
document.removeEventListener('visibilitychange', window.__trackAbandons);
性能优化和防性能退化
定义以用户为中心的指标的好处是,当针对它们进行优化时,必然会促进用户体验的提升。
提高性能的最简单方法之一就是只向客户端发送较少的JavaScript
代码,但在不能减少代码大小的情况下,关键是要考虑如何交付JavaScript
。
优化 FP/FCP
你可以通过从文档的<head>
中删除任何阻塞渲染的脚本或样式表来缩短首次绘制和首次内容绘制的时间。
花时间确定用户感知"it's happening"所需的最小样式集,并将其内联到<head>
中,(或者通过HTTP2服务推送),你将获得难以置信的快速首次绘制时间。
PWA中应用的app shell 模式就是一个应用典范。
优化 FMP/TTI
一旦确定了页面上最关键的UI元素,你应该确保加载的初始脚本仅包含使这些元素正常渲染和交互的代码。
任何与关键元素无关的代码包含在初始js模块中都会拖慢可交互时间。我们没有理由强制用户下载和解析暂时不需要的js代码。
通用的做法是,你应该尽可能的压缩FMP和TTI之间的时间间隔。如果不能压缩的话,清楚地提示用户当前用户还不能交互是很必要的。
最让用户烦躁的体验就是点击一个元素,然而什么也没发生。
防止长任务
js代码分割,优化js的加载顺序,不仅可以让页面可交互时间变快,还能减少长任务,减少由于长任务导致的输入延迟和慢帧。
除了将代码拆分为单独的文件之外,还可以将同步的大代码块拆分为异步执行的小代码块,或者推迟到下一个空闲点。通过以较小代码块的方式异步执行该逻辑,你可以在主线程上留出空间,让浏览器响应用户输入。
最后,应该确保引用的第三方代码进行了长任务相关的测试。导致大量长任务的第三方广告或者统计脚本最终会损害你的业务。
防止性能退化
本文主要关注真实用户的性能测量,虽然真实用户数据是最终关注的性能数据,但测试环境数据对于确保您的应用在发布新功能之前表现良好(并且不会退化)至关重要。测试阶段对于退化检测非常理想,因为它们在受控环境下运行,并且不易受真实用户环境的随机变异性影响。
像 Lighthouse
和 Web Page Test
这样的工具可以集成到持续集成服务器中,并且如果关键指标退化或下降到特定阈值以下,可以让构建失败。
对于已经发布的代码,可以添加自定义警报,当性能指标变差时及时通知你。例如,如果第三方发布了新代码,并且你的用户突然出现了很多的长任务,会警报通知你。
要成功防止性能退化,你需要在每个新功能版本中,都进行测试和真实用户环境下的性能测试。
总结和展望
去年,我们在浏览器上向开发人员开放以用户为中心的指标方面取得了重大进展,但还没有完成,并且还有更多已规划的事情要做。
我们非常希望将可交互时间和关键元素显示时间统计标准化,因此开发人员无需自己计算这些内容,也不需要依赖polyfills去实现。我们还希望让开发人员更容易定位导致丢帧和输入延迟的长任务和具体的代码位置。
虽然我们有更多的工作要做,但我们对取得的进展感到兴奋。有了像PerformanceObserver
这样的新API以及浏览器本身支持的长任务,开发人员可以使用js原生的API来测量真实用户的性能而不会降低用户体验。
最重要的指标是那些代表真实用户体验的指标,我们希望开发人员尽可能轻松地使用户满意并创建出色的应用程序。