一些废话

总所周知,写文章需要一个标题。虽然我们搞代码的人一般都喜欢单刀直入,但是受制于文体的约束和发表载体的要求,有时不得不想一个标题。而起一个标题,不亚于起一个函数名或者变量名。单就这篇文章,我就有好几个草稿标题,例如:《页面加载指标演进之路》,《Element Timing:一种全新的页面速度指标》,《如何最准确地测量网页加载速度》,《新前端下的页面加载速度》,甚至《Element Timing In Action》,《三分钟学会测量页面速度》。最后综合考虑了读者的承受能力,编辑的意见,以及最最重要的:本人的孱弱写作实力,就取了个这样的一个非常大众化,既不会一眼就被当成垃圾,也不会被人挑出来仔细找茬的标题。

好了,废话不能多说,直接进入正题。

DOMReady

上古时期(指距今 10+ 年前的 jQuery 纪元),我们开发网页还停留在编写静态页面结构,用 JS 脚本对 DOM 进行直接操作,添加删除一些额外页面元素上。此时,DOMReady 基本就可以满足计算页面加载完成时间的需求,DOMReady (在 DOM 事件中是 DOMContentLoaded)代表页面文档完全加载并解析完毕, 一般包含HTML文档分析以及DOM树的创建、外链脚本的加载、外链脚本的执行以及内联脚本的执行,而不会等待样式文件,图片文件,子框架页面的加载。一般在页面 header 中打个时间戳,再在 jQuery 的 domReady 事件中打个时间戳,我们就可以计算到大致的 DOMReady 耗时了。

Navigation Timing

中古时期(指距今 10-5 年左右的 Ajax 纪元),网页的交互形式更加丰富多样,Gmail 为首的富网页应用在用户体验大幅增强的同时,也给细粒度的网页加载时间记录带来了需求。因此,从2010年开始Web 性能工作组就已经为 Web 引入了大量时间信息记录,可以通过 window 对象的 performance 属性去获取。这就是后来我们所熟知的 Navigation Timing。

Navigation Timing 接口所提供的数据大致如图:

基本上囊括了从网页开始网络请求到页面完整加载并执行完资源并完成初始化 DOM 节点的时间。我们直接使用 performance.timing,就可以轻松获得这些时间来帮助分析页面的加载时间。

FP, FCP 和 LCP

近古时期(指距今 5-2 年左右的 React 纪元),由于各种前端框架(React,Vue 等)雨后春笋似的涌出,加上 Webpack 这种前端构建神器的出现,导致 Web 页面的开发难度迅速下降,复杂度也直线上升。重前端的应用大行其道,页面加载脚本的时间也迅速变长,很多网站为了体验采取了渐进式加载的策略,以解决等待脚本执行时白屏时间过长的问题。因此,渐进式网页渲染指标也应运而生。

渐进式网页指标一般有这几个:

  • 首次绘制(FP): 全称 First Paint,标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点
  • 首次内容绘制(FCP):全称 First Contentful Paint,标记的是浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 <canvas> 元素。
  • 首次有效绘制(FMP):全称 First Meaningful Paint,标记的是页面主要内容绘制的时间点,例如视频应用的视频组件、天气应用的天气信息、新闻应用中的新闻条目。
  • 最大内容绘制(LCP):全称 Largest Contentful Paint,标记在可视区“内容”最大的可见元素开始绘制在屏幕上的时间点。

其中 FMP 因为依赖算法猜测有效元素,所以目前已经基本被弃用。这几个指标的可视化意义可以参考以下两张图:

由于复杂页面的元素往往很多,FCP 所观测的元素可能只是 无足轻重 的一个 Loading 标记或者一个边栏,因此对于真实的用户来说,并不能代表页面的“首屏时间”。反而,在某些逻辑复杂的页面中,由于 JS 代码的执行时间长,或者依赖很多后端接口来渲染页面,经常会导致页面最重要的数据展示的时间远远长于页面 OnLoadEvent 触发的时间,此时,对于用户来说最直观感觉的到的“首屏时间”,往往就是 LCP 的时间。

这就是现在很多前端页面性能工具都会把 LCP 列入一个重要的参考指标的原因。

Element Timing

现代时期(指距今 1-0 年左右的微前端纪元),LCP 的计算逻辑是浏览器给定的,在不同页面中,浏览器所认为的 最大的可见元素 也未必是我们业务中 真正重要的 内容。并且在微前端流行的现代,不仅仅是同一应用的不同页面采用单页模式,甚至不同子应用的加载也可能通过 hash 路由来驱动。对于这种单页应用来说,以上的各个指标其实都无法满足在主体框架加载完成后切换不同页面时的重新计算。那么我们是不是只能够完全依赖业务开发本身去在代码里主动打点和上报加载时间呢?那也未必。

让我们来看看 W3C 的一个新草案,元素计时 API :https://wicg.github.io/element-timing/ 。尽管这个 API 还处于草案阶段,但是 ChromeEdge 两个浏览器其实早已在新版本给予了支持:兼容性

Element Timing API 的目的是让 Web 开发人员或分析工具能够测量页面上关键元素的渲染时间,比起 LCP ,我们能够自己来定义关键元素,这正是Element Timing 的最大魅力。
​Element Timing 支持的元素有:

  • <img> 元素
  • <svg> 中的 <img> 元素
  • <video> 中的 poster image
  • 拥有 background-image 的元素
  • 一组文本节点

举个例子:

<img src="image.jpg" elementtiming="big-image">
<p elementtiming="text" id="text-id">text here</p>
const observer = new PerformanceObserver((list) => {
  let entries = list.getEntries().forEach(function (entry) {
      console.log(entry);
  });
});
observer.observe({ entryTypes: ["element"] });


// 输出 entry 内容:
// {
//   duration: 0
//   element: p.aimake-site-name
//   entryType: "element"
//   id: ""
//   identifier: "text-id"
//   intersectionRect: DOMRectReadOnly {x: 236, y: 130, width: 144, height: 28, top: 130, …}
//   loadTime: 0
//   name: "text-paint"
//   naturalHeight: 0
//   naturalWidth: 0
//   renderTime: 10850.899999976158
//   startTime: 10850.899999976158
//   url: ""
// }

但是需要注意的是,并不是所有文本节点都可以通过添加 elementtiming="" 让 Element Timing API 识别,WICG 的解释中有一段注意事项:

读起来比较难懂,但是实际上它想说明的是,只有满足以下条件的文本节点才能够被记录:

  • 必须是块级元素:如 <p><h1><div><section> 等,也就是说,单独的 <span> 元素等行内元素,即时添加了 elementtiming="" 属性也并不会被记录。
  • 直接子节点必须包含一个或多个文本节点:例如 纯文本,<span><i><b> 等,<p> 等块级元素则不算,<img> 这种图像也不算。

举一些例子就懂了:

<html>
  <p elementtiming = "p1"><p>1</p></p> <!-- 无效 -->
  <p elementtiming = "p2">2</p> <!-- 有效 -->
  <span elementtiming = "span1">span1</span> <!-- 无效 -->
  <div elementtiming = "div1"><image elementtiming = "img1" src="https://img.alicdn.com/tfs/xxx.png"></image></div> <!-- div1 无效,其中的 img1 有效 -->
  <div elementtiming = "div2"><span>1</span></div> <!-- 有效 -->
  <div elementtiming = "div3"><p>2</p></div> <!-- 无效 -->
  <div elementtiming = "div4"><h1>3</h1></div> <!-- 无效 -->
  <div elementtiming = "div5"><b>4</b></div> <!-- 有效 -->
  <b elementtiming = "b1">b1</b> <!-- 无效 -->
  <i elementtiming = "i1">i1</i> <!-- 无效 -->
  <h1 elementtiming = "h1">h1</h1> <!-- 有效 -->
  <section elementtiming = "section1">section1</section> <!-- 有效 -->
</html>

在添加了自定义 elementtiming 属性后,当所标记的图像或者文本节点被 真正渲染 时,浏览器就会记录下时间。因此,我们可以在不同应用中让开发同学直接给能够标志 首屏 的元素添加该属性,即可由采集脚本通过监听 PerformanceObserver 来统一采集到元素绘制的时间点(renderTime)了。

通过使用 Element Timing API ,我们能够更精确记录到每个应用,页面,甚至功能模块的加载时长。这才是最现代,最前沿的页面加载时间方案,其余方案最终都将被埋葬在历史的尘埃中!

另一些废话

一般来说,结尾并没有标题那么重要,因此我也不需要费脑子去想 N 个结尾了,直接简单总结一下:无论前端发展到什么程度,Timing 的标准总会追上你!

希望如果有读者大神如果受到了一点点启发,能够给写个包含 Element Timing 指标的性能库造福我们。毕竟我就是懒,以上都是我自己 YY 的用法。在此先提前感谢了。

03-05 23:26