以用户为中心的前端性能指标「译」

原文地址:User-centric Performance Metrics
原作者:Philip Walton
译者:DADFAD - 简书

你可能多次听到,前端页面的性能至关重要,因为性能是决定你的网页应用是否「快」的关键因素。

但如果你尝试去回答这个问题:我的网页应用有多快?

你会发现「快」是一个模糊的概念。当我们谈及「快」时,我们在讨论的是什么?在何种环境下?为谁而快呢?

我们需要对性能有一个清晰的定义,以免在之后的讨论中产生误解,让我们的开发者们好心办坏事,最后非但没有优化用户的体验,反而进行了劣化。

举个例子,我们现在常听人说:「我刚测了一下我的页面,在X.XX秒内就加载完了」

关于这个问题,这样的阐述不在于它是错的,而是忽略了一些现实的情况,以偏概全了。我们的页面加载时间因人而异,这取决于他们的设备性能,和网络条件。将加载时间仅表述为一个数字,忽略了那些经历了更长加载时间的用户。

现实情况下,我们网页的加载时间应该是每位用户加载时间的集合,集合的分布情况大致如以下直方图所示:

load time set

图中的横轴是页面的加载时间,柱形的高度反映了处于对应区间的用户数量。如图所示,大多数的用户的页面在一到两秒内加载完成,但依然有很多的用户等待了更长的时间。

「我的页面在X.XX秒内就加载完了」产生误解的另一个原因是,页面加载的快慢体验是不能够只用一个时间指标来度量的。在页面加载的过程中有多个时间点能够让用户感受到「快」,如果我们只关注其中的一个,可能会忽略其他时间点带给用户的糟糕体验。

比如说,我们先假设有一个理想的网页,在经过快速的初始渲染后,立即将内容呈现给了用户。在这之后页面加载了一个巨大的JavaScript文件,并花费了几秒钟用来解析、执行。直到JavaScript运行前,页面的内容都将不能与用户交互。如果用户看到了一个超链接,却无法点击,抑或是他们看到了一个文本框,却不能往里输入内容,这个时候他们可能就不会在意页面渲染得有多快了。

所以,与其使用一个指标去度量我们的网页,不如将我们的关注点放在能够影响用户感知的各个时间点上。

另一个关于性能的误解是,性能只影响了页面的加载时间

我们团队也曾经犯过这样的错误,从更大的事实来看,大多数的性能测试工具也只关注了页面加载时的性能。

实际情况是,糟糕的性能不只会发生在加载时间,它会发生在任何时间。我们的页面不能响应快速的点击、不能展现平滑的动画,这些体验与页面加载缓慢一样让人难受。我们的用户关注页面的整体体验,我们开发者也当如是。

这些对性能的误解的一个共同主题是,他们将关注点置于一些对用户体验影响微乎其微,甚至没有影响的地方。同样的,传统的性能指标,像加载时间load,或者DOMContentLoaded,是极为不可靠的,因为他们发生的时间,与用户所认为的页面加载完成的时间也很可能不对应。

所以为了确保我们不犯同样的错误,我们应该首先回答一下这些问题:

  1. 什么样的性能指标最能度量人的感觉?
  2. 怎样才能从我们的真实用户中获取这些指标?
  3. 如何用我们所获取的指标来确定一个页面表现得是否「快」?
  4. 当我们得知用户所感知的真实性能表现后,我们应该如何做才能避免重蹈覆辙,并在未来提高性能表现?

以用户为中心的性能指标

当我们的用户浏览一个网页,他们通常期望一个视觉上的反馈,告知他们所有的工作正向他们所期望的方向发展。

开始了吗?
页面开始加载了吗?得到了服务端的回应吗?

有用吗?
有足够用户期望看到的内容被渲染出来了吗?

能用吗?
用户能够与我们的页面交互了吗?还是依然在加载?

好用吗?
交互是否流畅、自然、没有延迟与其他的干扰?

想要得知这些问题的答案,我们需要定义一些新的指标:

首次绘制(First Paint)和首次内容绘制(First Contentful Paint)

Paint Timing API定义了两个指标:首次绘制(FP)和首次内容绘制(FCP)。在浏览器导航并渲染出像素点后,这些指标点立即被标记。 这些点对于用户而言十分重要,因为它回答了我们的第一个问题问题:开始了吗?

FP与FCP的主要区别在于,FP标记浏览器所渲染的任何与导航前内容不同的点,而FCP所标记的是来自于DOM中的内容,可能是文本、图片、SVG,甚至是canvas元素。

首次有效绘制(First Meaningful Pain)和主要元素时间点(Hero Element Timing)

首次有效绘制(FMP)回答了我们的问题:有用吗?对于现存的所有网页而言,我们不能去清晰地界定哪些元素的加载是「有用」的(因此目前尚无规范),但是对于开发者他们自己而言,他们很容易知道页面的哪些部分对于用户而言是最为有用的。

hero elements

这些页面中「最重要的部分」通常被称为主要元素。举一些例子,在YouTube的播放页面,播放器就是主要元素。在Twitter中可能是通知的图标,或者是第一条推文。在 天气应用中,主要元素应是指定位置的预测信息。在一个新闻站点中,它可能是摘要,或者是第一张插图。

网页中总有一部分内容的重要性大于其余的部分。如果这部分的内容能够很快地被加载出来,用户甚至都不会在意其余部分的加载情况。

慢会话

我们的浏览器通过将任务添加到主线程的队列中逐个执行来响应用户的输入。但这也是浏览器用来执行我们页面Javascript的地方,从这个角度来讲,浏览器像是单线程的。

某些情况下,一些任务将可能会花费很长的时间来执行,如果这种情况发生了,主线程阻塞,剩下的任务只能在队列中等待。

image.png

用户所感知到的可能是输入的延迟,或者是哐当一下全部出现。这些是当今网页糟糕体验的主要来源。

Long Tasks API认为任何超过50毫秒的任务都可能存在潜在的问题,并将这些任务揭露给开发者。既然能够满足50毫秒内完成任务,也就能够符合RAIL在100毫秒内相应用户输入的要求。

可交互时间点

可交互时间(TTI)标记了你的页面已经呈现了画面,并且能够响应用户输入的时间点。页面不能响应用户输入有以下常见的原因:

  • 将被JavaScript所操作的元素还未被加载出来;
  • 一些慢会话阻塞了浏览器的主线程(如我们在上一部分所描述的)

TTI所记录实际上是页面的JavaScript完成了初始化,主线程处于空闲的时间点。

指标所反映的用户体验

回到我们之前提到的四个对于用户体验而言十分重要的四个问题,下表概述了我们的性能指标如何对应到我们的问题之上:

开始了吗?
首次绘制、首次内容绘制 First Paint (FP) / First Contentful Paint (FCP)

有用吗?
首次有效绘制、主要元素时间点 First Meaningful Paint (FMP) / Hero Element Timing

能用吗?
可交互时间点 Time to Interactive (TTI)

好用吗?
慢会话 Long Tasks (从技术上来讲应该是:没有慢会话)

这些基于时间线的页面加载截图能够帮助我们更加直观地看到性能指标时间点所带来的加载体验:

image.png

接下来我们将讨论如何从我们真实的用户设备中获取这些指标。

在真实的用户中获取这些指标

以前我们针对像loadDOMContentLoaded这些性能指标进行优化的一个主要原因是,他们在浏览器中作为事件发生,易于从真实的用户中获取。

相对的,许多其他的指标在以前是很难获取的。比如说,我们经常可以看到开发者们用下面这些奇技淫巧去检测慢会话:

(function detectLongFrame() {
  var lastFrameTime = Date.now();
  requestAnimationFrame(function() {
    var currentFrameTime = Date.now();

    if (currentFrameTime - lastFrameTime > 50) {
      // Report long frame here...
    }

    detectLongFrame(currentFrameTime);
  });
}());

这段代码将会无限循环调用requestAnimationFrame()并计算每次执行所花费的时间,如果超过50毫秒,则会将其当做慢会话进行上报。通常情况下,这些代码是有效的,但依然有一些不足之处:

  • 它增加了每一帧的开销;
  • 它减少了主线程的空闲区段;
  • 它很耗电;

性能监控的底线应该是不让性能变得更糟。

LighthouseWebPageTest这样的服务已经提供测算部分新指标有一段时间了(并且总得来说,通常情况下他们是发布功能之前测试性能的绝佳工具),但这些工具并不能运行在我们用户的设备上,所以他们并不能反映用户体验的真实情况。

幸运的是,随着浏览器新API的加入,在真实的设备中测算这些指标终于变得不需要那么多变通的办法,甚至是会影响性能的奇技淫巧。

这些新的API是PerformanceObserverPerformanceEntryDOMHighResTimeStamp。为了展示新API的一些特性,下面的代码创建了一个PerformanceObserver实例用以监听绘制的动作(FP和FCP)以及发生的慢会话。

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // `entry` 是一个 PerformanceEntry 实例.
    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带给我们的,是在性能时间发生时选择订阅他们,并以骚气的异步方式响应的能力。他替代了我们经常需要使用轮询才能知悉数据是否有效的老的Performance Timing接口。

追踪首次绘制和首次内容绘制(FP/FCP)

目前以下内容使用的API支持度并不高,译者暂不推荐用于非企业内部应用中,就不做翻译了(懒)。

Once you have the data for a particular performance event, you can send it to whatever analytics service you use to capture the metric for the current user. For example, using Google Analytics you might track first paint times as follows:

<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的标准定义(因此也没有性能条目类型的标准定义),这是因为以通用的标准去确定所有页面哪些元素是「有效的」是非常困难的一件事。

但是,在一个页面,或者一个应用下,最好将FMP确定为我们页面的主要元素呈现给用户的时间点。

Steve Souders有一篇很棒的文章,叫做User Timing and Custom Metrics,详细讲述了如何使用Performance API确定各种媒体的可见时间的多种技术。

追踪可交互时间点(TTI)

很长一段时间里,我们都希望在PerformanceObserver里能有一个标准化的、关于可交互时间的指标。与此同时,我们写了一个能够用来获取TTI的垫片,能够运行在支持Long Tasks API的浏览器中。

The polyfill exposes a getFirstConsistentlyInteractive() method, which returns a promise that resolves with the TTI value. You can track TTI using Google Analytics as follows:

import ttiPolyfill from './path/to/tti-polyfill.js';

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

The getFirstConsistentlyInteractive() method accepts an optional startTime configuration option, allowing you to specify a lower bound for which you know your app cannot be interactive before. By default the polyfill uses DOMContentLoaded as the start time, but it's often more accurate to use something like the moment your hero elements are visible or the point when you know all your event listeners have been added.

Refer to the TTI polyfill documentation for complete installation and usage instructions.

Note: As with FMP, it's quite hard to spec a TTI metric definition that works perfectly for all web pages. The version we've implemented in the polyfill will work for most apps, but it's possible it won't work for your particular app. It's important that you test it before relying on it. If you want more details on the specifics of the TTI definition and implementation you can read the TTI metric definition doc.

追踪慢会话

我之前提过,慢会话经常会有损用户体验(比如说事件处理的缓慢,或者丢帧)。如果我们能够得知这些事件所发生的频率,我们就能够为之努力,减少这种情况的发生。

为了能够使用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']});

归属属性能够告诉我们哪一帧的上下文导致了慢会话,这有利于确定是问题的引起是否与第三方的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,
    });
  }
});

从数据中获取信息

性能如何影响业务

放弃加载

我们知道,如果页面的加载时间过长,用户通常会选择离开。不幸的是,这意味着我们所有的性能指标都存在着幸存者偏差的问题,我们所搜集到的数据将不包括那些没能等到页面加载完成的用户(这意味着我们可能不能捕获一些性能过低的数据)。

当我们不能跟踪这些用户的过低性能数据时,我们可以跟踪这些情况发生的频率,以及用户停留在页面的时间。

This is a bit tricky to do with Google Analytics since the analytics.js library is typically loaded asynchronously, and it may not be available when the user decides to leave. However, you don't need to wait for analytics.js to load before sending data to Google Analytics. You can send it directly via the Measurement Protocol.

下面的这段代码为visibilitychange事件添加了一个监听器(如果页面正被关闭,或者进入了后台,则会触发),并在此时将performance.now()的值发送给后端。

<script>
window.__trackAbandons = () => {
  // 移除该监听器以保证只执行一遍
  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));

  // 发送数据
  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>

我们可能还希望在页面到达了可交互时间(TTI)后移除此监听器,否则我们将同时报告TTI和放弃加载的时间:

document.removeEventListener('visibilitychange', window.__trackAbandons);

优化性能、避免倒退

优化首次绘制和首次内容绘制(FP/FCP)

优化首次有效绘制和可交互时间点(FMP/TTI)

避免一些长的任务

避免倒退

总结与展望

去年,我们为了将以用户为中心的性能指标提供给开发者们,在浏览器上做了很多的工作,但我们并没有就此止步,我们依然有很多的愿景。

我们非常希望将可交互时间、主要元素指标标准化,以便开发者们不需要使用垫片的方式(polyfill)去获取他们。我们同时也希望让开发者们能够轻松地将页面的掉帧、输入的延迟归因到导致这些情况发生特定的慢会话,或者代码。

目前我们还有更多的工作需要做,但我们依然为我们所取到的进展感到兴奋。借助PerformanceObserver等新的API以及浏览器本身支持的慢会话,开发者们终于拥有了度量真实用户性能所需的基础,并且不会降低他们对于页面的体验。

能够反应用户真实体验的指标是最重要的,我们由衷地希望开发者们都能够轻松地让用户感到满意,并且创造出色的网页应用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,378评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,680评论 2 59
  • 简介 前端优化的目的是什么 ? 从用户角度而言,优化能够让页面加载得更快、对用户的操作响应得更及时,能够给用户提供...
    JuanitaLee阅读 809评论 0 5
  • 期待了好久的电影明天首映。开心的打开购票艾普,选座时突然发现愣住了,孤零零的位置显得有些突兀。原来不知不觉我已经一...
    阿司呀阅读 1,148评论 0 0
  • 时间长短的概念,在每个时间段都会有不同感觉的!小时候有个想法非常深刻,直到现在还记忆犹新。上小学的时候看见隔壁的大...
    乐阳L阅读 154评论 0 0