阅读 8643

前端性能优化指南[6]--Web 性能标准

本篇是此系列第 6 篇,下一篇介绍 Web 页面的性能指标, 内容如下:

很多人关注的、包括网上发布的一些文章大多都是关于性能优化手段,也有关于性能指标的以及如何做性能监控的,这些很多都基于 Web 性能标准以及浏览器对 Web 性能标准的实现。

如果我们全面的了解了 Web 性能标准,就能知道性能指标是如何定义的、性能指标的计算方式以及有哪些浏览器支持的优化策略。基于这些了解,我们才能说如何去排查性能问题、如何去优化性能以及如何去采集这些性能数据来做性能监控。

从 Web 性能工作组的主页可以看到全部的性能标准,我们也可以在 ALL STANDARDS AND DRAFTS 搜索到这些标准。

本篇文章将与大家一起,系统的学习这些 Web 性能标准。我将 Web 性能标准大体分为两类:性能度量相关和优化策略相关。在介绍每种标准时,我将按这样的顺序介绍:

  • 标准的用处
  • 标准的版本
  • 标准包含的内容
  • 与其他标准的关系

性能度量


我们关注的页面的性能主要包括页面呈现时间以及交互操作的流畅度。当页面呈现时间越短、交互越流畅时,我们认为页面性能越好。

用户可以在感官上判断性能的好坏,但是作为开发者,我们需要把性能量化,用性能指标来度量性能的好坏。现在主流浏览器已经支持通过特定的 API 来获取页面的性能数据。

在 HTML 规范中定义了 Window 对象,我们可以通过 window 来获取一个 Window 对象, 在 window 上挂载了很多我们熟悉的 API, 例如:

  • window.document
  • window.history
  • window.localStorage
  • window.location
  • window.navigator
  • window.postMessage
  • ...


这些 API 都由不同的 W3C 标准定义,而 Web 性能标准则是在 window 上添加了 performance 属性,通过 window.performance 返回一个 Performance 对象。

image.png

对象中包含了很多用于衡量性能的属性和方法,而这些属性和方法由多种 Web 性能标准定义。下面来一一介绍这些与性能度量相关的规范。

High Resolution Time


该规范定义了一个 JavaScript 接口,该接口以毫秒级的分辨率提供当前时间,并且不受系统时钟偏差或调整的影响。

此标准有两个版本 Level 1 和  Level 2,Level 2 已经是正式发布的标准,所以 Level 1 已经过时了,官方不建议再使用,我们只需要知道 Level 2 定义了哪些规范就好。

Level 2 包含了这些内容:

  • 1、定义了测量性能数据的初始时间(Time Origin)

我们获取到的性能数据都是时间戳,需要一个初始时间来计算时间差,即某一阶段的耗时。

  • 2、定义了高精度时间戳 DOMHighResTimeStamp

用于存储以毫秒为单位的时间值,Web 性能规范定义的 API 获取到的时间都为高精度的时间戳。

  • 3、定义了 Performance 对象,以及 Performance 对象的几个属性和方法。

    • now()
    • timeOrigin
    • toJSON()


所有的性能数据都是通过 window.performance 返回的 Performance 对象获得的,下面的 Time 相关的性能标准所定义的属性和方法都是在 window.performance 和 Performance 对象中。

Performance 对象最初是在 Navigation Timing Level 1 中定义的

好了,了解了规范定义的内容,大家也清楚了为什么此规范名为 High Resolution Time 了吧?因为该规范的主要内容是与高精度时间相关的。

Performance Timeline


此标准对上述的 HR-TIME-2 规范中定义的 Performance 对象进行了扩展,提供了基于特定筛选条件(Name、EntryType)检索性能度量的接口,通过这些接口我们可以获得多种维度(页面、资源等)的性能度量时间线。

此标准目前有两个版本 Level 1 和 Level 2。Level 1 目前是 REC 状态,Level 2 规范还在草案阶段,当 Level 2 规范正式发布时, Level 1 也将被废弃。

Level 2 规范包含了这些内容:

  • 1、给 Performance 对象添加了三个方法:

    • getEntries()
    • getEntriesByType()
    • getEntriesByName()


我们可以在浏览器控制台输入这段代码,看看 Entry 是什么。

window.performance.getEntries();
复制代码

得到的结果是:

image.png

返回的是一个数组,数组中包含这几种对象,每种对象都继承自 PerformanceEntry 对象,即都包含 PerformanceEntry 的两个字段: name 和 entryType。

  • PerformanceResourceTiming
  • PerformanceNavigationTiming
  • PerformancePaintTiming
  • PerformanceMark
  • PerformanceMeasure
  • PerformanceEventTiming
  • PerformanceServerTiming
  • ...


这些对象包含 Web 应用程序整个生命周期的各种性能数据度量。

这些对象会在其他 Time 相关的规范中定义,因此这些与 Time 相关的规范都会使用到 Performance Timeline 规范定义的 PerformanceEntry。

所以,我们可以根据 getEntriesByType() 和 getEntriesByName() 来获取特定的 name 和 特定的 entryType 的性能数据。例如:

performance.getEntriesByType('paint');
performance.getEntriesByName('https://www.google.com/images/nav_logo299.webp');
复制代码

它们的具体信息如下:

Interface EntryType Name InitiatorType (发起请求的类型)
PerformanceResourceTiming resource 资源 URL link、img、script、css、fetch、beacon、iframe、other
PerformanceNavigationTiming navigation 页面 URL navigation
PerformancePaintTiming paint first-paint、first-contentful-paint /
PerformanceEventTiming first-input keydown /
PerformanceMark mark mark 被创建时给出的 name /
PerformanceMeasure measure measure 被创建时给出的 name /
PerformanceLongTaskTiming longtask 看这里 /


Entry Type 表示  PerformanceEntry 对象的类型,W3C 性能工作组制定了一个 Entry Type 名称的注册规范:Timing Entry Names Registry, 该规范目前在 Working Draft 阶段。

目前已经注册的 Type 有这些:

image.png


PerformanceEntry 对象具有 name、entryType、startTime、duration、 toJSON() 这些属性,是 PerformanceResourceTiming、PerformanceNavigationTiming 等对象的公共属性,所以将被后面介绍的其他规范定义的对象(如 PerformanceResourceTiming、PerformanceNavigationTiming 等)继承。


用于观察性能时间线,以便在记录新的性能指标时发出通知, 这在采集性能数据时经常用到。例如下面的例子,观察 resource 类型的性能数据并打印。

<!doctype html>
<html>
<head></head>
<body>
<img id="image0" src="https://www.w3.org/Icons/w3c_main.png" />
<script>
const resourceObserver = new PerformanceObserver(list => {
  list
    .getEntries()
    // Get the values we are interested in
    .map(({ name, entryType, startTime, fetchStart, responseStart, responseEnd }) => {
      const obj = {
        "Name": name,
        "Entry Type": entryType,
        "Start Time": startTime,
        "Fetch Start": fetchStart,
        "Response Start": responseStart,
        "Response End": responseEnd,
      };
      return JSON.stringify(obj, null, 2);
    })
    // Display them to the console.
    .forEach(console.log);
  // Disconnect after processing the events.
  resourceObserver.disconnect();
});
// Subscribe to new events for Resource Timing.
resourceObserver.observe({type: "resource"});
// 多个 Entry Type
// userTimingObserver.observe({entryTypes: ["mark", "measure"]});
</script>
</body>
</html>
复制代码

好了,了解了此规范定义的内容,大家也清楚了为什么此规范名为 Performance Timeline 了吧?因为该规范的提供了获取各种类型(navigation、resource、paint 等)的性能时间线的 API。

Resource Timing


该规范定义了用于访问文档中资源的完整计时信息的 API, 例如请求资源的 DNS、TCP、Request 等的开始时间和结束时间,可以帮助我们收集文档中静态资源的加载时间线、资源大小和资源类型。

此规范有两个版本 Level 1 和  Level 2, Level 1 在 2017 年就成为了候选推荐版本,到今天也没有正式发布。Level 2 当前还在工作草案阶段,在 2020.02.18 还在更新,但并没有在文档中说明与 Level 1 的关系。


从两个版本的内容看,内容差别不大,但 Level 2 新增了 IANA Considerations 用于将 Timing-Allow-Origin 设置为临时消息头,并更新了资源时序处理模型。

image.png

https://w3c.github.io/resource-timing/timestamp-diagram.svg
资源时序处理模型

Level 2 规范包含了这些内容:

此对象描述了资源请求的性能时间线。


  • 2、给 Performance 对象添加了如下方法

    • clearResourceTimings()
    • setResourceTimingBufferSize()

对于跨域请求的资源,获取到的 PerformanceResourceTiming 对象中的属性值(时间),由于跨域限制,浏览器不会将资源的性能数据提供给用户,这些时间值都会被设置为 0 。

如果服务端在请求资源的响应头中添加 Timing-Allow-Origin,则浏览器就会将此资源的性能时间值暴露给用户。

我们可以通过以下语句获取文档中所有资源的性能数据:

performance.getEntriesByType('resource');
复制代码

或者通过以下语句获取特定资源的性能数据:

performance.getEntriesByName('https://www.google.com/images/nav_logo299.webp');
复制代码

结果是:

image.png

好了,了解了此规范定义的内容,大家也清楚了为什么此规范名为 Resource Timing 了吧?因为该规范描述了资源请求时序的性能度量。

Navigation Timing


此标准定义了文档导航过程中完整的性能度量,即一个文档从发起请求到加载完毕各阶段的性能耗时。

此标准目前有两个版本,Level 1 已经是 2012 年的版本了, Level 2 最新更新在 2020 年 1 月,将来 Level 2 会替代 Level 1 版本。

目前很多浏览器已经实现了 Level 2,建议使用 Level 2 规范定义的 API。因为 Level 1 和 Level 2 的差别比较大,但  Level 1 定义的 API 仍有很多人在用,所以下面都详细介绍下。


Navigation Timing Level 1

前面提到了 High Resolution Time API 定义了 Performance 对象,可通过 window.performance 获取。而 Navigation Timing Level 1 则是在此基础上增加了两个属性:timing 和 navigation。

规范包含了以下内容:

  • 1、定义了 PerformanceTiming 对象

用来衡量页面性能,我们可以通过通过 window.performance.timing 获取页面的性能数据,返回的对象中每个字段的含义可以在 PerformanceTiming | MDN 上查阅。

image.png

按照事件发生的先后顺序,这些性能数据的 TimeLine 如下:

image.png

https://www.w3.org/TR/navigation-timing/timing-overview.png

  • 2、定义了 PerformanceNavigation 对象

用来描述加载相关的操作,通过 window.performance.navigation 获得,返回的 PerformanceNavigation 对象存储了两个属性,它们表示触发页面加载的原因。这些原因可能是页面重定向、前进后退按钮或者普通的 URL 加载

image.png

返回的对象中每个字段的含义可以在 PerformanceNavigation | MDN 上查阅。

  • 3、定义了 window.performance 属性


为 Window 对象添加了 performance 属性:timing 和 navigation

Navigation Timing Level 2

2019 年 Web 性能工作组带来了 Navigation Timing Level 2 ,将会替代 Navigation Timing Level 1。在 Navigation Timing Level 1 中定义的两个属性 performance.timing 和 performance.navigation 被废弃了。

Level 2 新增了以下这些内容,我们可以先不用理解这段英文讲的什么,在阅读完这一小节后就能理解 Level 2 做了哪些更新了。


Level 2 规范主要包含以下内容:


此对象用于度量文档的性能,我们可以通过以下方式获取文档的性能数据,所有时间值都是以 Origin Time 为起点测量的。

window.performance.getEntriesByType("navigation");
复制代码


这将返回以下这些数据:

image.png

这些属性并不都是 PerformanceNavigationTiming | MDN 对象自有的,有一部分是从原型对象中继承的。我们可以在控制台输入以下代码来查看此对象的原型链。

const [entry] = performance.getEntriesByType("navigation");
let proto = Object.getPrototypeOf(entry);

const protos = {};

while(proto.toString() != '[object Object]') {
	protos[proto.toString()] = Object.keys(proto);
	proto = Object.getPrototypeOf(proto);
}

console.log(protos);
复制代码


得到的结果是:

{
    "[object PerformanceNavigationTiming]":[
        "unloadEventStart",
        "unloadEventEnd",
        "domInteractive",
        "domContentLoadedEventStart",
        "domContentLoadedEventEnd",
        "domComplete",
        "loadEventStart",
        "loadEventEnd",
        "type",
        "redirectCount",
        "toJSON"
    ],
    "[object PerformanceResourceTiming]":
        "initiatorType",
        "nextHopProtocol",
        "workerStart",
        "redirectStart",
        "redirectEnd",
        "fetchStart",
        "domainLookupStart",
        "domainLookupEnd",
        "connectStart",
        "connectEnd",
        "secureConnectionStart",
        "requestStart",
        "responseStart",
        "responseEnd",
        "transferSize", 
        "encodedBodySize",
        "decodedBodySize",
        "serverTiming",
        "toJSON"
    ],
    "[object PerformanceEntry]":[
        "name",
        "entryType",
        "startTime",
        "duration",
        "toJSON"
    ]
}
复制代码


可以看到 Navigation Timing Level 2 规范只定义了这些字段:

"unloadEventStart",
"unloadEventEnd",
"domInteractive",
"domContentLoadedEventStart",
"domContentLoadedEventEnd",
"domComplete",
"loadEventStart",
"loadEventEnd",
"type",
"redirectCount",
"toJSON"
复制代码


其中的 type 和 redirectCount 就是被废弃的 Navigation Timing Level 1 标准的 window.performance.navigation 返回的 PerformanceNavigation 对象所包含的内容。

而 Navigation Timing Level 1 中的通过 window.performance.timing 获取的 PerformanceTiming 对象所包含的描述文档性能的数据则是由以下对象共同描述的:

我们可以通过 Resource Timing Level 2 标准中定义的 API window.performance.getEntriesByType("resource") 获取特定资源所需的时间长度。


由此,Navigation Timing Level 2 给出了新的时间线,与 Navigation Timing Level 1 不同的是将描述资源加载的时间用 PerformanceResourceTiming 对象封装了起来。

image.png

https://www.w3.org/TR/navigation-timing-2/timestamp-diagram.svg

2、定义了 NavigationType 的枚举值

NavigationType 是一个枚举类型,包含四种值:navigate、reload 、 back_forward 和 prerender

好了,现在让我们来理解下 Level 2 标准中说的这些改变具体表现在哪里吧。

the definition of Performance interface was moved to [PERFORMANCE-TIMELINE-2];

将对 Performance 对象的定义放在了 PERFORMANCE-TIMELINE-2 标准中

builds on top of [RESOURCE-TIMING-2];

基于 RESOURCE-TIMING-2 标准,从上文可以看到使用了在 RESOURCE-TIMING-2 中 定义的 PerformanceResourceTiming


support for [PERFORMANCE-TIMELINE-2];

使用了 Performance Timeline Level 2 定义的 PerformanceEntry 对象


support for [HR-TIME-2];

所有的时间值都是以 High Resolution Time Level 2 中定义的 timeOrigin 为时间起点计算的。


support for prerender navigations [RESOURCE-HINTS];

Level 1 中定义的导航类型 有三种值:0, 1,2;对应了 Level 2 中 NavigationType 的 navigate、reload 和 back_forward。 但 Level 2 新增了 prerender 这种类型,而导航可被初始化为 prerender 类型是在 RESOURCE-HINTS 标准中定义的。


exposes number of redirects since the last non-redirect navigation;

PerformanceNavigationTiming 对象中的 redirectCount 字段


exposes next hop network protocol;

PerformanceResourceTiming 对象中的 nextHopProtocol 字段


exposes transfer, encoded body and decoded body size information;

PerformanceResourceTiming 对象中的 transferSize、encodedBodySize、decodedBodySize


secureConnectionStart attribute is now mandatory.

secureConnectionStart 属性在 Level 1 中是可选的,但在 Level 2 中浏览器必需设置这个属性


好了,了解了此规范定义的内容,大家也清楚了为什么此规范名为 Navigation Timeline 了吧?因为该规范定义了描述文档在整个导航过程中的性能度量 PerformanceNavigationTiming 对象。

Paint Timing


此规范定义了一个 API 用来记录在页面加载期间的一些关键时间点的性能度量,比如 First Paint 、First Contentful Paint。

此规范只有一个版本,这是第一个也是最新的版本,目前还在 Working Draft 阶段。


此规范包含以下内容:

  • 1、定义了 PerformancePaintTiming 对象


用于描述在页面加载期间的一些关键时间点的性能度量,我们可以在控制台通过以下语句查看:

performance.getEntriesByType('paint');
复制代码

结果返回一个每一项都为 PerformancePaintTiming 类型的数组,一项为 first-paint ,另一项为 first-contentful-paint。

image.png

  • 2、提出了一些关键时间点的定义,例如  First Paint 、First Contentful Paint

First Paint,是从导航到浏览器将第一个像素呈现到屏幕的时间,这不包括默认的背景绘制,但包括非默认的背景绘制。这是开发人员关心页面加载的第一个关键时刻——当浏览器开始呈现页面时。

First Contentful Paint (FCP),是当浏览器呈现来自 DOM 的第一位内容时,向用户提供页面实际加载的第一个反馈。这是用户第一次开始使用页面内容。

好了,了解了此规范定义的内容,大家也清楚了为什么此规范名为 Paint Timing 了吧?因为该规范定义了获取Paint 关键时间点性能数据的 API。

User Timing

此规范定义了一个可以让 Web 开发者测量性能的 API 。目前有两个版本, Level 2 已正式发布,Level 3 还在草案阶段,将来也会替代 Level 2。

Level 3 在 Level 2 的基础上做了改动,下面介绍 Level 3 规范包含的内容。

  • 1、给 Performance 对象添加了几个方法

    • mark()
    • clearMarks()
    • measure()
    • clearMeasures()

使用方法如下,通过 mark() 方法可以在指定位置添加一个时间戳,记录执行到此位置时的时间。Mark 就是标记的意思,并且会给这个标记一个名称。

measure() 方法则可以测量两个时间点之间的间隔,并给它一个名称。

但是 Level 2 和 Level 3 中对 mark() 和 measure() 的用法是有区别的。

在 Level 2 中的用法如下,mark() 方法返回的 startTime 是执行到此标记语句时的时间。

// Level 2 中的用法
performance.mark("mySetTimeout-start");

performance.measure(
  "mySetTimeout",
  "mySetTimeout-start",
  "mySetTimeout-end"
);
复制代码

在 Level 3 中,mark() 支持传入的第二个参数为一个对象,此对象的 startTime 字段可以支持用户自定义标记的开始时间,还提供了一个 detail 字段给用户描述其他信息;measure() 方法也接收第二个参数为一个对象,除了在 Level 2 中第二个参数和第三个参数表示的测量的开始时间和结束时间外,也提供了一个 detail 字段给用户描述其他信息。

// Level 3 中的用法
performance.mark("mySetTimeout-start", {
  detail: {component: 'component_name'},
  // 在 Level 2 中 startTime 不用传入,后台默认是 mark 标记的时间,Level 3 中可以支持用户自己定义了
  startTime: 123  
});

performance.measure("click_to_update_component", {
    detail: {component: 'component_name'},
    start: startMark.startTime,
    end: performance.now(),
 });
复制代码
  • 2、定义了 PerformanceMark 对象


描述 mark() 方法返回的数据, 可以通过 performance.getEntriesByType('mark') 获得,字段的具体含义可以在这里查看:Performance.mark() | MDN, 其中 detail 字段是在 Level 3 规范中定义的。

image.png

  • 3、定义了 PerformanceMeasure 对象


描述 measure() 方法返回的数据,performance.getEntriesByType('measure'),字段的具体含义可以在Performance.measure() | MDN 查看,其中 detail 字段是在 Level 3 规范中定义的。

image.png

好了,现在你知道为什么此规范叫 User Timing 了吧? 因为它提供了可以让用户自定义时间节点来测量性能的接口,而不是像 Resource Timing、Navigation Timing 和 Paint Timing 一样由浏览器决定测量的时间节点(例如:redirectStart、domainLookupStart、connectEnd 等等)。

Server Timing


我们知道前面已经介绍的 Resource Timing、Navigation Timing、Paint Timing 和 User Timing 规范描述的都是 Web 应用程序的性能特征,比如请求启动的时间、协商连接、接收响应等时间节点,这些性能度量浏览器都可以采集到,但请求是如何路由的、在服务器上花费的时间在哪里等无法获知。

此规范描述了如何将服务器端在请求-响应周期内的性能度量传递给用户代理,并定义了一个 API 使应用程序能够收集、处理和执行这些指标。

此规范只有一个版本,目前在 Working Draft 阶段。


此规范包含以下内容:

  • 1、定义了与服务端的通信协议:Server-Timing 响应头

响应头信息如下, 可以在 www.w3.org/TR/server-t… 查看响应头的具体含义

> GET /resource HTTP/1.1
> Host: example.com

< HTTP/1.1 200 OK
< Server-Timing: miss, db;dur=53, app;dur=47.2
< Server-Timing: customView, dc;desc=atl
< Server-Timing: cache;desc="Cache Read";dur=23.2
< Trailer: Server-Timing
< (... snip response body ...)
< Server-Timing: total;dur=123.4
复制代码
  • 2、定义了描述服务端性能度量的接口 PerformanceServerTiming 对象

浏览器会通过 Server Timing Header 的解析算法 将解析后的每一个性能度量用 PerformanceServerTiming 对象来表示。

每个 PerformanceServerTiming 描述服务端的一个性能度量信息,这些度量服务端性能的所有PerformanceServerTiming 对象放在一个数组中,挂在  PerformanceResourceTiming 对象的 serverTiming 属性上,我们可以通过以下语句获取:

performance.getEntriesByType('navigation');
performance.getEntriesByType('resource');
复制代码

好了,了解了此规范定义的内容,大家也清楚了为什么此规范名为 Server Timing 了吧?因为该规范定义了如何获取服务端各节点性能的 API。

Long Tasks API


当用户与页面交互时,应用程序和浏览器都会将浏览器随后执行的各种事件排队,例如,用户代理根据用户的活动安排输入事件,应用程序为 requestAnimationFrame 和其他回调等安排回调。一旦进入队列,这些事件就会被浏览器安排逐个出列并执行。

但是,有些任务可能需要很长时间,如果发生这种情况,UI 线程将被锁定,所有其他任务也将被阻止。对于用户来说,这是一个卡死的页面,浏览器无法响应用户的输入,这是目前 Web 上不良用户体验的主要来源。

此规范定义了一个 API,可以使用它来检测这些“长任务(Long Task)”的存在,“长任务”在很长一段时间内独占 UI 线程,并阻止执行其他关键任务,例如响应用户输入。

此规范目前只有一个版本,在 Working Draft 阶段。


此规范包含以下内容:

  • 1、定义了 PerformanceLongTaskTiming 对象


用于描述 Long Task 信息,对象中各字段的含义可在 Long Tasks API | MDN 查阅。

  • 2、定义了什么是 Long Task

Long Task 是指超过 50ms 的事件循环任务。

50 毫秒这个阈值标准来源于 RAIL ModelResponse: process events in under 50ms(切换到英文版) 。


如何检测是否有 Long Task?我们可以使用 PerformanceObserver 这个 API。

var observer = new PerformanceObserver(function(list) {
    var perfEntries = list.getEntries();
    for (var i = 0; i < perfEntries.length; i++) {
        // Process long task notifications:
        // report back for analytics and monitoring
        // ...
    }
});
// register observer for long task notifications
observer.observe({entryTypes: ["longtask"]});
// Long script execution after this will result in queueing
// and receiving "longtask" entries in the observer.
复制代码

好了,了解了此规范定义的内容,大家也清楚了为什么此规范名为 Long Task API 了吧?因为该规范定义了如何**检测长任务(Long Task)**的 API。

优化策略

除了以上关于性能度量的标准,Web 性能工作组还制定了一些可以让浏览器来实现的性能优化标准,并给我们提供了使用这些优化措施的 API。

Resource Hints - 加载性能

许多 Web 应用程序已经利用了各种预取技术来提高加载性能,这包括但不限于在需要之前使用 XMLHttpRequest 来获取和缓存资源。但是,这些实现不能提供与浏览器支持的相同的性能级别。更糟糕的是,这些实现有时与浏览器逻辑冲突,导致延迟或不必要的资源获取,从而降低整个页面性能。

此规范定义了 HTML 的 <Link> 元素的 rel 属性值,包括 dns-prefetch、preconnect、prefetch 和 prerender。我们可以使用这些资源提示让用用户代理帮助我们预解析 DNS、预链接、预加载资源以及预处理资源以提高页面性能。

此规范目前只有一个版本,还在工作草案阶段。


下面介绍此规范定义的 4 个资源提示:

1、资源提示: dns-prefetch(Resource Hints: dns-prefetch)

给浏览器提示,在后台执行 DNS 查找以提高性能。

<link rel="dns-prefetch" href="//example.com">
复制代码

2、资源提示:预连接(Resource Hints: preconnect)

给浏览器提示在后台开始连接握手(DNS,TCP,TLS)以提高性能。

<link rel="preconnect" href="//example.com">
<link rel="preconnect" href="//cdn.example.com" crossorigin>
复制代码

3、资源提示:预取(Resource Hints: prefetch)

<link rel=prefetch /> 告诉浏览器获取下一次导航可能需要的资源。大多数情况下,这意味着将以极低的优先级来获取资源(因为浏览器知道当前页面中需要的所有内容比我们认为在下一页中需要的资源更重要)。这意味着 prefetch 的主要用处是加快下一个导航的速度,而不是当前的导航。

<link rel="prefetch" href="//example.com/next-page.html" as="document" crossorigin="use-credentials">
<link rel="prefetch" href="/library.js" as="script">
复制代码

prefetch 有 3 条规则:

  • 用户代理在获取资源后不会做预处理,也不会在当前页面使用这个资源。
  • as 属性是一个可选属性,符合 [PRELOAD] 中的定义。
  • crossorigin CORS 设置属性是一个可选属性,指示指定资源的 CORS 策略。

4、资源提示:预渲染(Resource Hints: prerender)


给浏览器提供提示,以便在后台呈现指定的页面,如果用户导航到页面,则会加快页面加载速度。用于获取下一个可能的 HTML 导航,并通过获取必要的子资源并执行它们来预处理 HTML 响应(即预呈现页面)。

<link rel="prerender" href="//example.com/next-page.html">
复制代码

如果 HTML 不需要预处理,则可以使用 prefetch 资源提示。

Preload - 加载性能

许多应用程序需要细粒度地控制何时获取、处理和应用资源到文档。例如,应用程序可能会延迟某些资源的加载和处理,以减少资源争用并提高初始加载的性能。此行为通常通过将资源获取移动到应用程序的自定义资源加载逻辑中来实现,即当满足特定条件时,资源获取通过注入的元素或通过 XMLHttpRequest 启动。

但是,也有一些情况需要尽早获取一些资源,但它们的处理和执行逻辑取决于特定的要求,例如依赖关系管理、条件加载、排序保证等。

此规范定义了可与 link 元素一起使用的 preload 资源提示,可以告诉用户代理预读取资源而不必执行它们,允许将资源加载与执行分离,细粒度控制资源何时和如何加载。

  • Preload  Candidate Recommendation


例如,应用程序可以使用 preload 关键字启动 CSS 资源的早期、高优先级和非呈现阻塞获取,然后应用程序可以在适当的时间应用这些获取:

Using markup
<!-- preload stylesheet resource via declarative markup -->
<link rel="preload" href="/styles/other.css" as="style">

<!-- or, preload stylesheet resource via JavaScript -->
<script>
var res = document.createElement("link");
res.rel = "preload";
res.as = "style";
res.href = "styles/other.css";
document.head.appendChild(res);
</script>

Using HTTP Header
Link: <https://example.com/other/styles.css>; rel=preload; as=style
复制代码


如上例所示,可以通过声明性标记、Link HTTP头([RFC5988])或通过 JavaScript 调度资源。有关如何和在何处使用 preload 的更多实际例子,请参见用例部分。

注意:preload 与 prefetch 的关系

prefetch 和 preload 都可以声明一个资源及其获取属性,但在用户代理获取资源的方式和时间上有所不同:prefetch 是可选的,通常是用于后续导航可能使用的资源的低优先级获取;preload 是当前导航所必需的资源的强制获取。开发人员应该合理使用它们来最小化资源争用和优化加载性能。

另外,在 preload 中 as 属性对于保证正确的优先级、请求匹配、请求对应正确的内容安全策略(Content-Security-Policy )指令以及基于资源类型发送正确的 Accept 首部是必需的。

更多可以参考: Preload: What Is It Good For? by Yoav Weiss

Page Visibility - 节省资源

此规范提供了观察页面可见性状态的 API ,例如当用户最小化窗口或切换到另一个选项卡时,API 会发送visibilitychange 事件,让监听者知道页面状态已更改,我们可以检测事件并执行某些操作。

例如网站有图片轮播效果,只有在用户观看轮播的时候,才会自动展示下一张幻灯片;显示信息仪表盘的应用程序不希望在页面不可见时轮询服务器进行更新。

因此,页面可见性 API 对于节省资源提高性能特别有用,它使页面在文档不可见时避免执行不必要的任务。

此规范目前有两个版本,Level 2 在 Proposed Recommendation 阶段将会替代  Second Edition 这个版本。


Level 2 规范包含以下内容:

  • 1、定义了页面状态的枚举:VisibilityState

这个枚举对象我们是用不到的,而是浏览器用的。

  • 2、给 Document 对象添加了三个属性

    • hidden
    • visibilityState
    • onvisibilitychange 事件


我们可以通过 document.hidden 和 document.visibilityState 访问页面可见状态。

出于历史原因,保留对 hidden 属性的支持。开发人员应尽可能使用 visibilityState,document.visibilityState 返回的值在 VisibilityState 枚举中定义。

image.png

可通过 visibilitychange 事件监听页面可见状态是否有改变,如果有改变通过 document.visibilityState 获取页面改变后的状态。

var videoElement = document.getElementById("videoElement");

// Autoplay the video if application is visible
if (document.visibilityState == "visible") {
  videoElement.play();
}

// Handle page visibility change events
function handleVisibilityChange() {
  if (document.visibilityState == "hidden") {
    videoElement.pause();
  } else {
    videoElement.play();
  }
}

document.addEventListener('visibilitychange', handleVisibilityChange, false);
复制代码

但是这个规范各大浏览器厂商的实现方式不一样,存在兼容性问题,在实现时需要做兼容,具体兼容方式可在 Page Visibility API | MDN 查看。

requestIdleCallback API - 充分利用资源

此规范定义了后台任务协同调度 API,提供了由用户代理决定在空闲时间自动执行队列任务的能力,在 Background Tasks API | MDN 中有详细介绍。


规范名称为 Cooperative Scheduling of Background Tasks, 而不是 requestIdleCallback。

此规范包含以下内容:

  • 1、给 Window 接口增加了新的方法

    • requestIdleCallback()
    • cancelIdleCallback()


我们可以使用 requestIdleCallback() 在浏览器空闲时运行高耗时、低优先级的任务。

  • 2、定义了 IdleDeadline 对象

可以通过规范中的这个示例来解释,requestIdleCallback 方法接收一个函数(需要在空闲时间执行的任务)refinePi, 该函数的参数 deadline 就是 IdleDeadline 类型,这个对象提供一个 timeRemaining() 函数,用于获取任务可利用的空闲时间。

function refinePi(deadline) {
  while (deadline.timeRemaining() > 0) {
    if (piStep())
      pointsInside++;
    pointsTotal++;
  }
  currentEstimate = (4 * pointsInside / pointsTotal);
  textElement = document.getElementById("piEstimate");
  textElement.innerHTML="Pi Estimate: " + currentEstimate;
  requestId = window.requestIdleCallback(refinePi);
}
function start() {
  requestId = window.requestIdleCallback(refinePi);
}
复制代码

空闲回调应尽可能不超支分配到的时间,关于如何充分利用空闲回调,这里有几点建议,不过更建议您去 Background Tasks API | MDN 查看详细内容。

  • 对非高优先级的任务使用空闲回调。
  • 空闲回调应尽可能不超支分配到的时间。
  • 避免在空闲回调中改变 DOM。
  • 避免运行时间无法预测的任务。
  • 在你需要的时候要用 timeout,但记得只在需要的时候才用。

Beacon - 数据上报

我们经常需要尝试在卸载(unload)文档之前向 Web 服务上报性能数据。过早的发送数据可能导致错过收集数据的机会。但对于开发者来说保证在文档卸载期间发送数据一直是一个困难,因为用户代理通常会忽略在 unload 事件处理器中产生的异步 XMLHttpRequest

为了解决这个问题,通常要在 unload 或者 beforeunload 事件处理器中发起一个同步 XMLHttpRequest** **来发送数据。同步的 XMLHttpRequest 迫使用户代理延迟卸载文档,使得下一个导航出现的更晚,而下一个页面对于这种较差的载入表现无能为力。

此规范给 navigator 添加了一种方法,使用 navigator.sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。


用法如下:

window.addEventListener('unload', logData, false);

function logData() {
    navigator.sendBeacon("/log", analyticsData);
}
复制代码

考虑到此方法的兼容性,应该在浏览器不支持 navigator.sendBeacon 时使用其他方法发送数,例如同步的 XMLHttpRequest。

小结


本文介绍了 Web Performance 工作组制定的性能标准,包括性能度量和优化策略两个方面,下面用一张思维导图总结一下。

Timing 相关对象继承关系:


关于 Web 性能 标准的更多细节建议查阅 W3C 文档。在下一节,我将介绍各种性能指标的出处、定义、区别以及测量方式。

思考


今天留两个问题:

  • 1、为什么 Frame Timing | W3C 被迁移到了  Frame Timing | WICG ,是浏览器厂商无法实现吗?如果让你来计算页面的 FPS,该如何计算呢?

  • 2、Resource Hints 与 Preload 的关系, 为什么 Preload 不在 Resource Hints 标准中?它们有什么故事吗?

欢迎大家在评论区讨论。


系列篇