使用 Paint Timing API

发布于 大漠

本文转载自:众成翻译 译者:Yves yao 审校: huangxiaolu 链接:http://www.zcfy.cc/article/4038 原文:https://css-tricks.com/paint-timing-api

现在是成为 Web 性能爱好者的最佳时间,特别是 Chrome 60 的 Paint Timing API 的出现更加证明了这一事实。虽说 Paint Timing API 仍属于正在激增的 Performance API 之一,但是它还提供了抓取 页面资源 耗时的能力,通过这个全新的实验性 API,你可以在页面开始绘制时就去抓取你需要的度量值。

这个 API 的句法与常见的性能 API 大同小异(特别是资源计时 API)。如果你还未尝试过任何的性能 API,那么尝试使用它们中的一部分会有一定的帮助。也就是说你可以继续往下读本文并学到一些东西,如果不读也没关系。在我们深入了解之前,我们谈一谈这个 API 收集的绘制计时以及其他几种计时。

为什么我们需要一个绘制计时 API?

在读本节时,你可能已经知道“绘制”指的是什么。如果不知道,那么这是一个很好理解的概念:绘制 是指绘制像素到浏览器窗口期间的所有浏览器活动,属于页面渲染过程的关键部分。当我们在性能领域讨论绘制时,我们通常指的是浏览器加载页面过程中绘制该页面所花费的时间。确切地说,这个时间应该叫“首次绘制的时间”。

为何这个度量值很重要?因为它代表在用户请求一个页面后可能展现某些东西的最早时间。虽然页面加载时会同时跑很多任务,但是我们都知道,只有当我们越快地在页面上展现某些东西给用户,才会越快地让他们意识到某些工作正在进行。这有点像你的 LDL 胆固醇(低密度脂蛋白胆固醇),大部分与性能相关的目标都是要降低你的指数。但是如果你不知道这些指数本来是多少,那么所有想要达到这个目标的努力都是白费。

让人欣慰的是,Paint Timing API 可以帮助我们获取这些指数。通过这个 API,我们可以使用 JavaScript 了解一个用户访问页面时到底在绘制上花了多少时间。在 Lighthouse 以及 sitespeed.io 这类应用中进行模拟测试也为我们感兴趣的方面提供必要的参考来帮助我们有效提升我们的网站性能,但是他们都是在“真空”中进行的测试。他们无法告诉你用户真正使用你的网站时的性能如何。

与类似的性能 API 相比,Paint Timing API 更简洁。它只为我们提供了两种度量值:

首次绘制(first-paint):它跟你想象的差不多,即浏览器绘制了页面的第一个像素的那一刻。它可能看起来长这样:

首次渲染 大概是这个样子

首次渲染 大概是这个样子。

首次内容绘制(first-contentful-paint):这与首次绘制有一点点不同,它抓取的是第一 bit 的内容被绘制的那一刻,它可能是文字、一张图片,或者是任何不属于非内容的样式(whatever isn't some variation of non-contentful styling),这个场景可能看起来像下面这样:

首次内容渲染 大概是这个样子

首次内容渲染 大概是这个样子。

有一点需要指出,这两个时间点的区别可能并不总是那么明显。基于一个给定网站的客户端架构,首次绘制首次内容绘制 可能没什么区别。如果网站使用了更快和更轻的结构,那么这两个时间通常几乎一致(甚至完全一致)。对于客户端架构包含了大量资源的大型网站(或者连接速度比较慢),这两个度量值的出现时间可能会有差异。

不管怎么说,让我们一起看看如何使用这个 Chrome 60 带来的 API。

一个简单的用法

你可以通过很多种方式使用本 API。最简单的方式是将代码写在首次绘制之后不久就会触发的事件中。至于为什么要把它放到事件中而不是立即执行,是因为通过这样的方式可以保证当你想从 API 中提取度量值的时候,这些度量值一定是真实有效的。参考如下的例子:

if("performance" in window){
    window.addEventListener("load", ()=>{
        let paintMetrics = performance.getEntriesByType("paint");

        if(paintMetrics !== undefined && paintMetrics.length > 0){
            paintMetrics.forEach((paintMetric)=>{
                console.log(`${paintMetric.name}: ${paintMetric.startTime}`);
            });
        }
    });
}

这段代码做了以下的事情:

  • 我们用一个简单的检测去判断 window 中是否包含 performance 对象。这样可以避免在 performance 失效的情况下运行我们的代码。
  • 我们通过 addEventListenerwindow 对象的 load 事件添加代码,当页面及其资源全部加载完之后将会触发 load 事件。
  • load 的事件代码中,我们使用 performance 对象的 getEntriesByType 方法将所有 "paint" 类型的事件收集到一个叫做 paintMetrics 的变量中。
  • 由于只有 Chrome 60(及更新版本)才实现了渲染时间 API,因此我们需要检查是否返回了一些条目。做法如下:我们检查 paintMetrics 是否是 undefined 并且它的 length 是否大于 0
  • 如果这些检查都通过了,我们接下来将度量值的名称及其开始时间输出到控制台中,它看起来是这样的:

控制台中展示的绘制时间

控制台中展示的绘制时间。

如图所示,我们在控制台中看到的时间单位是毫秒。到此为止,你可以将这些度量值发送到其他地方存储以备将来分析。

这个方法很厉害,但是如果我们想在浏览器收集这些度量值的时候就可以读取它们呢?那么我们需要使用 PerformanceObserver

通过 PerformanceObserver 抓取绘制度量值

如果你确实很想在这些度量值刚刚在浏览器中生效的时候就访问它们,你可以使用 PerformanceObserver。使用 PerformanceObserver 的方式很取巧,特别是你还想保证自己没有影响到并不支持它的浏览器,或者支持它但是不支持 "paint" 事件的浏览器。我们接下来的工作与第二种情况相关,因为获取浏览器不支持的事件将会抛出 TypeError

由于 PerformanceObserver 通过一部的方式收集度量值和日志,因此最佳方式是使用 promise,它会帮助我们在处理异步的时候不会遇到昔日的回调地狱。请看下面的代码:

if("PerformanceObserver" in window){
    let observerPromise = new Promise((resolve, reject)=>{
        let observer = new PerformanceObserver((list)=>{
            resolve(list);
        });

        observer.observe({
            entryTypes: ["paint"]
        });
    }).then((list)=>{
        list.getEntries().forEach((entry)=>{
            console.log(`${entry.name}: ${entry.startTime}`);
        });
    }).catch((error)=>{
        console.warn(error);
    });
}

我们看看这段代码:

  • 我们检查 window 中是否包含 PerformanceObserver 对象。如果 PerformanceObserver 不存在,则什么都不做。
  • 创建一个 Promise。在 promise 链的第一部分,我们新建了一个 PerformanceObserver 对象并将其保存在 observer 变量中。这个观察者包含了一个回调去 resolve 这个 promise,并传入一个绘制时间的 list
  • 我们需要在某个地方获取这些绘制时间对吧?这就是 observer 方法的作用。我们可以告诉这个方法我们需要什么类型的性能条目。因为我们需要绘制事件,因此我们只需传入一个包含 "paint" 的数组即可。
  • 如果浏览器支持使用 PerformanceObserver 收集 "paint" 事件,则 promise 将会 resolve 并且链条的下一部分将会跳进来,在这部分中,我们可以通过 list 变量的 getEntries 方法访问这些条目。然后像上一个例子一样向控制台输出结果。
  • 如果当前的浏览器不支持使用 PerformanceObserver 收集 "paint" 事件,那么 catch 方法将会提供错误信息。我们可以从错误信息中获取我们想要的所有信息。

现在我们学会了一种方法去异步获取度量值而不用等待页面加载完毕。我个人倾向于使用前一种方法,因为它的代码更简短易读(个人看法)。当然,我的方法并不是最健壮的,但是它们可以有效地表达我们能够在浏览器中收集绘制时间的事实,这种方法可预见而且在老的浏览器中不会抛出错误。

我用它做什么?

这取决于你追求什么。或许你只是想知道你的网站在真实用户那里渲染得有多快。或许你想收集数据用于研究。在写这篇文章的时候,作者正在实施一个 图片质量研究项目,这个项目会研究志愿者们如何感知 JPEGs 和 WebP 图片的质量损失。作为我研究的一部分,我使用其他的计时 API 收集性能相关的信息,但是我同时也收集绘制时间。我不知道这个数据最后是否有用,但是收集并结合其他度量值去分析它或许对我的发现有所帮助。总之,你可以自己决定是否使用这部分数据。依我拙见,我很庆幸有这个 API 的存在,同时也希望更多的浏览器可以尽快实现它。

扩展阅读

通过阅读这篇文章,或许你会对其他更多的 性能接口 感兴趣。给大家提供几篇文章来检验自己的兴趣是否被充分地激发了:

  • Resource Timing API 中分享了这个 API 的使用,所以你应该刷一下这篇文章。如果你觉得文章中的代码看起来很舒服的话,你将立即获益于这个意义非凡的 API。
  • 由于这个 API 与 Navigation Timing API 不太兼容,所以你确实很需要读一下这篇文章。通过这个 API,你可以收集及时信息来检测 HTML 自身加载得有多快。
  • PerformanceObserver 本身比我这里提到的的多很多。你可以使用它获取资源时间以及用户时间。相关文章.
  • 说到用户时间,这里有一个针对它的 API。通过这个 API,你可以用很高精度的时间戳度量特定的 JavaScript 任务所花费的时间。你也可以用这个工具去测量用户与页面交互的延时。

现在你已经跳进这个 API 的坑里了,出去看看它(以及其他 API)到底能为你的探索做什么来为用户加速 Web!Levis X Jordan 6