21

First Meaningful Paint 首次有效绘制时间

 4 years ago
source link: http://www.alloyteam.com/2019/12/14174/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

最近在研究网站测速相关的主题,接触到一个概念: Firsr Meaningful Paint ,简称 FMP ,中文译名: 首次有效绘制时间 。今天我们来讲讲这个概念的来龙去脉。

衡量页面打开速度是一件复杂的事情

First Meaningful Paint(以下简称 FMP),是谷歌创造的一个概念,用来 更好地 衡量页面打开的速度。你可能会说,要衡量页面打开速度,不是已经有 DOMContentLoaded EventOnLoad Event 了吗?为什么还要搞一个新概念?

答案是: 因为随着 Web 应用越来越复杂,DOMContentLoaded Event 和 OnLoad Event 已经越来越难以准确表现页面的打开速度了。

举个例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>这里是直出的内容</h1>
    <img src="https://avatars3.githubusercontent.com/u/8401872" alt="">
    <hr/>
    <h1>下面是异步加载的内容</h1>
    <div id="app"></div>
    <script>
        setTimeout(() => {
            for (let i = 0; i < 100; i++) {
                let newDiv = document.createElement("div");
                let newContent = document.createTextNode("异步回来的内容" + i);
                newDiv.appendChild(newContent);
                document.querySelector('#app').appendChild(newDiv);
            }
        }, 200);
    </script>
</body>
</html>

在这个例子中,html 中有直出的内容,也有一张图片,script 中用 setTimeout 模拟一个 Ajax 请求,数据返回后插入到页面中。像这种类型的页面非常常见,我们通过 Chrome 的 Performance 可以看到它的打开效果和速度。

NJ3AFjz.gif

7nEBfeb.png!web

观察 Timings,可以看到 在打开页面的过程中依次触发了 DOMContentLoaded -> First Paint ->First Contentful Paint -> OnLoad -> First Meaningful Paint 。我们尝试来解释一下。

  1. 浏览器首先加载解析 HTML,HTML 加载解析完成后,触发 DOMContentLoaded Event 。此时浏览器尚未开始渲染,其他静态资源,比如图片等也还没加载。
  2. 浏览器开始渲染直出的 HTML,触发 First PaintFirst Contentful Paint ,用户开始看到文字。此时图片尚未加载回来,所以还不能看到图片。
    PS:关于 First Paint 和 First Contentful Paint,网上的资料很少,我也没完全搞明白这两者的定义和区别。我大致是这样理解的:浏览器要开始渲染的时候,会先触发 First Paint,当渲染出第一个 DOM(比如文字或者图片等 DOM 元素)的时候,就会触发 First Contentful Paint。这两个概念并非本文的重点,更多的详细解析,请参考 W3C 的定义
  3. 浏览器把图片加载回来了,所有资源都加载完成,触发 OnLoad Event
  4. setTimeout 定时器触发,往页面中写入大量 DOM,触发 First Meaningful Paint

那么问题来了,这里有 5 个指标, 应该用哪个指标来衡量页面打开速度呢?

DOMContentLoaded 肯定不行,因为页面都还没出来;First Paint 和 First Contentful Paint 也不够好,因为这时候只有文字,图片和异步的数据都没出来。虽然从技术上角度看,页面是打开了。但是对于用户来说,用户想要关注的是下面的异步数据,异步数据还没出来,用户自然认为页面还没打开。OnLoad 也不够好,因为虽然图片出来了,但是异步数据还是没出来。

那哪个指标才能衡量异步数据出来的时间的呢? -> 从 Performance 中可以看出,这个指标就是 First Meaningful Paint,它最接近用户感知上的打开时间,所以它的名字叫 Meaningful,意思就是,对于用户来说,是有意义的,有效的。

综上所述,页面打开是一个非常复杂的过程,在这个过程中,有很多指标可以来表征页面打开不同阶段的情况。其中,最接近用户实际感知的,就是 First Meaningful Paint。

那么,下一个问题来了, 这个 First Meaning Paint 是怎么定义的?Chrome 又是如何找到这个时间点的呢?

猜想,验证,优化

如果这个 Demo 就是我们的实际业务,想要衡量这个业务的打开速度,由于我们知晓其中的逻辑,知道只要等到那个 Ajax 回来之后,对于用户而言,就是 First Meaningful Time 了。所以我们可以通过自己打点来算出这个时间点。

问题:如果想要做一个通用化的工具,用来测量不同网站的 First Meaningful Time 呢?就跟 Chrome 的 Performance 一样,我们不可能知晓别的网站的业务逻辑,自然无法准确定义,到底哪个时刻才是 First Meaningful Time。怎么办呢?

答案:虽然无法准确知晓,但是可以靠猜测,关键是:怎么样猜测?猜得准不准?多准算准呢?

猜想一:布局重绘变化最大

在我们这个 Demo 中,异步数据回来后,会往页面中插入大量 DOM,势必会触发布局的重绘。浏览器可以检测出,什么时候发生最大的布局重绘变化,那么这个时刻就可以认为用户看到了关键内容,也就是 First Meaningful Paint 了。最大布局重绘变化,说起来比较抽象,可以通过另一个更加直观的指标来近似表征,就是 页面中 DOM 节点的数量 。下图便是 Demo 的 DOM 元素数量变化图。可以观察到,对应的时间是近似等于 First Meaningful Paint。

FfyUJri.png!web

猜想二:考虑很长的页面

我们把 Demo 变一下,代码如下所示

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>这里是直出的内容</h1>
    <img src="https://avatars3.githubusercontent.com/u/8401872" alt="">
    <hr/>
    <h1>下面是异步加载的内容</h1>
    <h2 id="title"></h2>
    <div id="app"></div>
    <script>
        setTimeout(() => {
            for (let i = 0; i < 200; i++) {
                let newDiv = document.createElement("div");
                let newContent = document.createTextNode("异步回来的内容 " + i);
                newDiv.appendChild(newContent);
                document.querySelector('#app').appendChild(newDiv);
            }
            document.querySelector('#title').innerText = "第 1 次异步";
        }, 200);
        setTimeout(() => {
            for (let i = 0; i < 1000; i++) {
                let newDiv = document.createElement("div");
                let newContent = document.createTextNode("异步回来的更多内容 " + i);
                newDiv.appendChild(newContent);
                document.querySelector('#app').appendChild(newDiv);
            }
            document.querySelector('#title').innerText = "第 2 次异步,更多的内容";
        }, 500);
    </script>
</body>
</html>

这样的场景就相当于:我先 Ajax 加载第一页的内容,等用户先看到内容了,然后我再发起下一页的 Ajax 请求,拿回来更多的数据。 此时,假如第 2 次请求触发了更大的布局重绘,那按照猜想 1 的逻辑,那 First Meaningful Paint 岂不是要推迟到第 2 次请求回来?

但是对于用户来说,第 1 次请求回来就感知到页面打开了,后面的第 2 次请求,不过是让页面变得更长而已,用户实际上不会马上看到第 2 次请求的数据。所以,还需要在猜想一的基础上再优化。谷歌的做法就是: 引入权重的概念,不仅考虑重绘的大小,还要考虑当重绘发生时,页面高度上发生的变化 。比如第 2 次请求回来,虽然发生了很大的重绘,但是页面高度也发生了很大的变化,因此,总体加权之后的重绘大小就变小了。我们在 Performance 中测试,可以看到,Chrome 依然准确地判断到了 First Meaningful Paint 是第 1 次请求回来,而不是第 2 次。如下图所示。

IBR7jqa.png!web

当然,还有更多的其他场景需要考虑(比如字体加载),具体的可以参考 谷歌的研究资料

后话

First Meaningful Paint 的确是一个很有用的指标,让我们可以更多地从用户的角度去衡量页面打开速度。但是,想要做成一个通用化的工具, 这个指标就只能靠猜测,即便是谷歌,也不能百分百保证猜得准 。比如说,把 Demo 改成,第 1 次插入 100 条数据,第 2 次插入 500 条数据,经测试,Chrome 就会猜测错误,将 First Meaningful Paint 误判为第 2 次请求。不过值得关注的是,谷歌在严谨的实验中,也给出了猜测算法的测试样本和准确率,感兴趣的读者,强烈建议读一下 谷歌的研究资料

参考资料

  1. Time to First Meaningful Paint: a layout-based approach By 谷歌
  2. Chrome 的 First Paint By 洪闰辉
  3. First Paint && First Contentful Paint By W3C

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK