你可能不止一次地听你们讨论性能的话题、一个速度飞快的web 应用是多么重要。web
个人网站快吗?当你试图回答这个问题的时候,你会发现快是个很模糊的概念。咱们在说快的时候,咱们到底指的哪些方面?是在什么场景下?对谁而言?shell
谈论性能的时候务必要准确,不要使用错误的概念,以避免开发人员一直在错误的事情上作优化——结果没有获得优化反而损害到用户体验。canvas
看一个具体的例子,咱们常常会听到有人说:个人app测过了,加载时间是XX秒。promise
上述的说法并非说它是错误的,而是它歪曲了现实。加载时间因用户而异,取决于他们的设备能力和网络环境。将只关注加载时间单一数据指标会遗漏那些加载时间长的用户。浏览器
实际上,上面所说的加载时间是一个全部用户的一个平均加载时间。只有下图所示的直方图分布图才能彻底反应真实的加载时间:性能优化
X轴上的数字表示加载时间,y轴直方图的高度表示某个时间内的用户数量。如图所示,虽然大多数用户的加载时间在1~2秒,但仍有很多用户的加载时间很长。bash
"个人页面加载时间是xx秒"不真实的另外一个缘由是,页面的加载不是单一的一个时间指标——它是用户的使用体验,而这种体验是没有任何一个指标能彻底捕获到的。加载过程当中有多个时间指标会影响用户对页面加载是否足够快的感觉,若是你只盯着加载完成时间这一个指标,那么你可能会忽视发生在其余时间点的不佳用户体验。服务器
例如,一个应用的初始渲染优化的很是快,页面内容很快就展现给用户了。若是这个应用随后加载了一个很大的js包,而且须要耗费好几秒解析和执行,页面内容在js执行完成以前,仍是无法响应用户的操做。若是用户看到一个连接却没法点击,有了文本框却无法输入,他们或许不在意你的页面渲染有多快的。cookie
因此不能使用单一的指标来衡量加载的速度,咱们应该关注整个加载过程当中的任何影响用户感觉的时间指标。网络
第二个误区是,认为性能只是在加载时须要关注的问题。
咱们做为一个团队在这方面犯了错误,而且因为多数性能检测工具只检测加载性能,这个错误还被放大了。
事实上性能问题可能在任什么时候间发生,不仅是在加载的时候。用户的点击响应速度慢,页面不能滚动,动画不流畅一样影响体验。用户关心的是总体的体验,做为开发者咱们也应如此。
这些误区的共同点是,咱们关注的指标跟用户体验没有关系或者说关系很小。一样,传统的性能指标如页面加载时间,DOMContentLoaded
时间是很是不可靠的。由于,当他们出现时,并不等于用户认为应用已经加载完成了。
全部为确保不重复这样的错误,咱们问本身几个问题:
当用户访问一个页面的时候,一般会从视觉上去感知是否是页面已经如预期地加载完成能够正常使用了。
主题 | 说明 |
---|---|
发生了吗? | 页面是否开始跳转?服务器有没有响应? |
内容重要吗? | 重要的内容是否已经渲染? |
可使用吗? | 页面可交互了吗?或者还在加载中吗? |
体验好吗? | 交互是否平滑天然,没有卡顿? |
为了了解页面在用户侧的在这些方面的表现,咱们定义了一些指标:
Paint Timing
接口定义了两个指标:首次绘制(FP)和首次内容绘制(FCP)。这些指标记录了浏览器开始在屏幕上进行绘制的时间点。这对用户很重要,由于它回答了:”发生了吗?”这个问题。
这两个指标的主要区别是FP是页面在视觉上首次出现不一样于跳转前的内容的时间点。相比之下,FCP是浏览器渲染DOM中第一个内容的时候,多是文本,图像,SVG甚至是<canvas>
元素。
首次有用的绘制回答了这个问题:“它有用吗?”。“有用”这个概念没有一个标准的定义。可是对于开发者来讲,找出页面上的哪些部分对用户是最重要的是很容易的。
网页上几乎老是有比其余内容更重要的部分。若是网页中最重要的部分加载速度很快,用户可能甚至不会注意到页面的其余部分是否没有加载完成。
浏览器经过向主线程上的队列添加任务并逐一执行来响应用户输入。这也是浏览器执行JavaScript的地方,因此在这个意义上说浏览器是单线程的。
在某些状况下,这些任务可能须要很长时间才能运行完,这样的话主线程将被阻塞,而且队列中的全部其余任务都必须等待。
长任务API能识别任何长于50毫秒的任务,它认为这存在性能隐患。经过长任务API,开发者能获取到页面中存在的长任务。选择50ms是为了遵循RAIL指南以确保100ms内响应用户的输入。
可交互时间(TTI)意味着页面渲染完成而且能够正常响应用户的输入了,可能有如下几个缘由致使页面不能响应用户输入:
TTI表示页面的初始JavaScript加载完成且主线程空闲(没有长任务)的点。
回到咱们之前认为对用户体验最重要的问题,本表概述了咱们刚刚列出的每一个指标如何映射到咱们但愿优化的用户体验:
体验 | 指标 |
---|---|
发生了吗? | 首次绘制(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
接口,它的好处是以异步的方式获取数据,而不是经过不断的轮询。
获取到某个性能数据后,能够将该用户的设备的性能数据发送到任意的数据分析服务。好比咱们将首次绘制的指标发送到谷歌统计。
<head>
<!-- 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。
Steve Souders有一篇名为User Timing And Custom Metrics的精彩文章,详细介绍了许多使用浏览器性能API肯定什么时候能够看到各类类型媒体的技术。
从长远来看,咱们但愿经过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代码会影响用户体验的第一步。
一旦开始收集真实用户的性能指标,你须要将这些数据付诸实践。真实用户性能数据之因此重要主要是因为如下几个缘由:
虽然这里的数据是特定于应用的(你应该本身测试一下本身应用的数据),下面的例子是一个基于性能指标生成的分析报告:
桌面端
Percentile | TTI (seconds) |
---|---|
50% | 2.3 |
75% | 4.7 |
90% | 8.3 |
移动端
Percentile | TTI (seconds) |
---|---|
50% | 3.9 |
75% | 8.0 |
90% | 12.6 |
经过将数据分解成移动和桌面,并将各个终端的数据采用分布图展现,能够快速洞察真实用户的体验。 例如,看上面的表格,能够很容易看到对于这个应用,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
。
你能够经过从文档的<head>
中删除任何阻塞渲染的脚本或样式表来缩短首次绘制和首次内容绘制的时间。
花时间肯定用户感知"it's happening"所需的最小样式集,并将其内联到<head>
中,(或者经过HTTP2服务推送),你将得到难以置信的快速首次绘制时间。
PWA中应用的app shell 模式就是一个应用典范。
一旦肯定了页面上最关键的UI元素,你应该确保加载的初始脚本仅包含使这些元素正常渲染和交互的代码。
任何与关键元素无关的代码包含在初始js模块中都会拖慢可交互时间。咱们没有理由强制用户下载和解析暂时不须要的js代码。
通用的作法是,你应该尽量的压缩FMP和TTI之间的时间间隔。若是不能压缩的话,清楚地提示用户当前用户还不能交互是很必要的。
最让用户烦躁的体验就是点击一个元素,然而什么也没发生。
js代码分割,优化js的加载顺序,不只可让页面可交互时间变快,还能减小长任务,减小因为长任务致使的输入延迟和慢帧。
除了将代码拆分为单独的文件以外,还能够将同步的大代码块拆分为异步执行的小代码块,或者推迟到下一个空闲点。经过以较小代码块的方式异步执行该逻辑,你能够在主线程上留出空间,让浏览器响应用户输入。
最后,应该确保引用的第三方代码进行了长任务相关的测试。致使大量长任务的第三方广告或者统计脚本最终会损害你的业务。
本文主要关注真实用户的性能测量,虽然真实用户数据是最终关注的性能数据,但测试环境数据对于确保您的应用在发布新功能以前表现良好(而且不会退化)相当重要。测试阶段对于退化检测很是理想,由于它们在受控环境下运行,而且不易受真实用户环境的随机变异性影响。
像 Lighthouse
和 Web Page Test
这样的工具能够集成到持续集成服务器中,而且若是关键指标退化或降低到特定阈值如下,可让构建失败。
对于已经发布的代码,能够添加自定义警报,当性能指标变差时及时通知你。例如,若是第三方发布了新代码,而且你的用户忽然出现了不少的长任务,会警报通知你。
要成功防止性能退化,你须要在每一个新功能版本中,都进行测试和真实用户环境下的性能测试。
去年,咱们在浏览器上向开发人员开放以用户为中心的指标方面取得了重大进展,但尚未完成,而且还有更多已规划的事情要作。
咱们很是但愿将可交互时间和关键元素显示时间统计标准化,所以开发人员无需本身计算这些内容,也不须要依赖polyfills去实现。咱们还但愿让开发人员更容易定位致使丢帧和输入延迟的长任务和具体的代码位置。
虽然咱们有更多的工做要作,但咱们对取得的进展感到兴奋。有了像PerformanceObserver
这样的新API以及浏览器自己支持的长任务,开发人员可使用js原生的API来测量真实用户的性能而不会下降用户体验。
最重要的指标是那些表明真实用户体验的指标,咱们但愿开发人员尽量轻松地使用户满意并建立出色的应用程序。