万字长文,解读前端性能监控原理

本文正在参加「金石计划 . 瓜分6万现金大奖」

最近公司打算做一个前端监控,其中性能监控接触的比较少,本人进行了一番研究,把所学的内容整理下,欢迎阅读。

在讲解性能监控之前,我们需要做一些知识储备,只有搞懂了前置知识才能很好的理解性能监控的原理。

浏览器的多进程架构

前端有一道常考的面试题: chrome 浏览器仅仅打开了1个页面,为什么有4个进程?

我们点击 Chrome 浏览器右上角的”选项”菜单,选择”更多工具”菜单,点击”任务管理器”,这将打开 Chrome 的任务管理器的窗口:

image.png

从图中可以看到,Chrome 启动了 4 个进程。分别是:浏览器主进程,网络进程(network),GPU进程及渲染进程。如果安装了浏览器插件,那么还加上一个插件进程。

image.png

  • 浏览器进程: 主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程: 核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程: Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是后来的网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程: 主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程: 主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

早在 2007 年之前,市面上浏览器都是单进程的。单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript 运行环境、渲染引擎和页面等。如此多的功能模块运行在一个进程里,是导致单进程浏览器不稳定、不流畅和不安全的一个主要因素。

你可以想象一下这样的场景:当你正在用浏览器打开多个页面时,突然某个页面崩溃了或者失去响应,随之而来的是整个浏览器的崩溃或者无响应,然后你发现你给老板写的邮件页面也随之消失了,这时你的心情会不会和页面一样崩溃呢?

从输入 URL 到页面展示完整流程

下面是一张从输入 URL 到页面展示完整流程示意图:

image.png

从图中可以看出,整个过程需要各个进程之间的配合。

  • 浏览器进程主要负责用户交互、子进程管理和文件储存等功能。
  • 网络进程是面向渲染进程和浏览器进程等提供网络下载功能。
  • 渲染进程的主要职责是把从网络下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。

图中的过程很复杂,它主要分为两个步骤:一是导航流程,二是渲染流程

导航流程

根据上面的示意图,导航流程分为以下几步:

  1. 首先,浏览器进程接收到用户输入的 URL 请求,浏览器进程便将该 URL 转发给网络进程。
  2. 然后,在网络进程中发起真正的 URL 请求。
  3. 接着网络进程接收到了响应头数据,便解析响应头数据,并将数据转发给浏览器进程。
  4. 浏览器进程接收到网络进程的响应头数据之后,发送”提交导航 (CommitNavigation)”消息到渲染进程;
  5. 渲染进程接收到”提交导航”的消息之后,便开始准备接收 HTML 数据,接收数据的方式是直接和网络进程建立数据管道;
  6. 最后渲染进程会向浏览器进程”确认提交”,这是告诉浏览器进程:”已经准备好接受和解析页面数据了”。
  7. 浏览器进程接收到渲染进程”提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态。

用户发出 URL 请求到页面开始解析的这个过程,就叫做导航

用户输入

当用户在地址栏中输入 URL,并键入回车后,页面标签页上就会出现 loading 图标。

image.png

但此时图中页面显示的依然是之前打开的页面内容,并没立即替换为极客时间的页面。因为需要等待提交文档阶段,页面内容才会被替换

在当前页面即将要被替换成新的页面,浏览器还给了当前页面一次执行 beforeunload 事件的机会,beforeunload 事件允许页面在退出之前执行一些数据清理操作,还可以询问用户是否要离开当前页面,比如当前页面可能有未提交完成的表单等情况,因此用户可以通过 beforeunload 事件来取消导航,让浏览器不再执行任何后续工作。

URL 请求过程

浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求后:

  • 首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。

  • 请求前的第一步是要进行 DNS 解析,以获取请求域名的服务器 IP 地址。如果请求协议是 HTTPS,那么还需要建立 TLS 连接。(DNS 也有缓存,这样第二次请求时速度更快)

  • 接下来就是利用 IP 地址和服务器建立 TCP 连接。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。

  • 服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。

问:网络进程收到响应头的信息后直接进行页面解析吗?

答:不是的,它还需要根据响应头里面的信息进行分别处理。

1. 重定向

在接收到服务器返回的响应头后,网络进程开始解析响应头,如果发现返回的状态码是 301 或者 302,那么说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,一切又重头开始了。

2. 响应数据类型处理

URL 请求的数据类型,有时候是一个下载类型,有时候是正常的 HTML 页面,那么浏览器是如何区分它们呢?

答案是 Content-Type

Content-Type 是 HTTP 头中一个非常重要的字段, 它告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据 Content-Type 的值来决定如何显示响应体的内容。

如果 Content-type 字段的值是 text/html,这就是告诉浏览器,服务器返回的数据是 HTML 格式。

如果 Content-Type 的值是 application/octet-stream,显示数据是字节流类型的,通常情况下,浏览器会按照下载类型来处理该请求。

如果服务器配置 Content-Type 不正确,比如将 text/html 类型配置成 application/octet-stream 类型,那么浏览器可能会曲解文件内容,比如会将一个本来是用来展示的页面,变成了一个下载文件。

所以,不同 Content-Type 的后续处理流程也截然不同。如果 Content-Type 字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTML,那么浏览器则会继续进行导航流程。

由于 Chrome 的页面渲染是运行在渲染进程中的,所以接下来就需要准备渲染进程了。

准备渲染进程

默认情况下,Chrome 会为每个页面分配一个渲染进程,也就是说,每打开一个新页面就会配套创建一个新的渲染进程。但是,也有一些例外,在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程中。

通常情况下,打开新的页面都会使用单独的渲染进程;如果从 A 页面打开 B 页面,且 A 和 B 都属于同一站点的话,那么 B 页面复用 A 页面的渲染进程;如果是其他情况,浏览器进程则会为 B 创建一个新的渲染进程。

同一站点: 根域名(geek.org)加上协议(https://)相同,那么就是同一站点。

渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。

提交文档

所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:

  • 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起”提交文档”的消息;

  • 渲染进程接收到”提交文档”的消息后,会和网络进程建立传输数据的”管道”;

  • 等文档数据传输完成之后,渲染进程会返回”确认提交”的消息给浏览器进程;

  • 浏览器进程在收到”确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。

image.png

这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,之前的页面没有立马消失,而是要加载一会儿才会更新页面。到这里,一个完整的导航流程就”走”完了,这之后就要进入渲染阶段了。

导航流程很重要,它是网络加载流程和渲染流程之间的一座桥梁,如果你理解了导航流程,那么你就能完整串起来整个页面显示流程,这对于你理解浏览器的工作原理起到了点睛的作用。

渲染流程

一旦文档被提交,渲染进程便开始页面解析和子资源加载了。

按照渲染的时间顺序,可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。

  1. 构建 DOM 树

浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。为了更加直观地理解 DOM 树,在控制台里面输入document后回车,这样你就能看到一个完整的 DOM 树结构,如下图所示:

image.png

图中的 document 就是 DOM 结构,DOM 和 HTML 内容几乎是一样的,但是和 HTML 不同的是,DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容

  1. 样式计算

和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets,该结构同时具备了查询和修改功能 。

可以在 Chrome 控制台中查看其结构,在控制台中输入 document.styleSheets,然后就看到如下图所示的结构:

image.png

  1. 布局阶段

现在有了 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局

  1. 分层

现在有了布局树,而且每个元素的具体位置信息都计算出来了,那么接下来是不是就要开始着手绘制页面了?

答案依然是否定的。

因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。如果你熟悉 PS,相信你会很容易理解图层的概念,正是这些图层叠加在一起构成了最终的页面图像。

image.png

从上图可以看出,渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。

再来看看这些图层和布局树节点之间的关系,如图所示:

image.png

  1. 图层绘制

在完成图层树的构建之后,渲染引擎会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表

注意:这里并没有实际绘制,只是生成了绘制列表。

  1. 栅格化(raster)操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。结合下图来看下渲染主线程和合成线程之间的关系:

image.png

实际生成图像的操作是由栅格化来执行的。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。

通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成图像的过程叫快速栅格化,或者 GPU 栅格化,生成的图像被保存在 GPU 内存中。

GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成图像的操作是在 GPU 中完成的,这就涉及到了跨进程操作。

  1. 合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——”DrawQuad”,然后将该命令提交给浏览器进程。

浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

从 HTML 到 DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。下面用一张图来总结下这整个渲染流程:

image.png

关于这个阶段的完整详细过程,可以参考详解浏览器的渲染流程(上)详解浏览器的渲染流程(下)两篇文章。

一旦页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。至此,一个完整的页面就生成了。

JS 和 CSS 是如何影响页面渲染的?

JavaScript 是如何影响页面渲染的

当渲染进程接收到 HTML 字符串后 HTML 解析器开始解析,当遇到 script 脚本后,HTML 解析器暂停工作,浏览器会渲染已经解析好了 DOM 结构,即把script 脚本之前的内容进行渲染。

为什么要渲染一次呢?

因为 JS 脚本是有可能获取DOM的,如果浏览器不先去渲染一次,就会是null。比如:

<body>
    <p id="box1">world</p>
    <script>
        console.log(document.getElementById('box1'));
        console.log(document.getElementById('box2'));
    </script>
    <p id="box2">hello</p>
</body>

上面代码中 JS 是可以获取到 id 为 box1 的p标签,但是不能获取 box2 的p标签,原因很简单,因为 script 在box2前面。js执行的时候,box2还没有被解析和渲染。

有了上面的知识就可以回答:为什么script脚本一般放在 html 结构的后面?

如果你把 script 放在最前面,同时大部分情况下 js 脚本都是放在服务器中的,因此会阻碍 html 的解析,这是会导致浏览器的白屏。

如果把 script 脚本放在最后面,当遇到 script 脚本时,浏览器其实已经解析完了脚本之前的大部分 html,因此浏览器会渲染出这部分页面,不至于屏幕上什么都没有,导致白屏。

CSS 是如何影响页面渲染的

我们经常被问到 JS 是如何阻碍页面渲染的,很少被问到 CSS 是如何阻碍页面渲染的。那 CSS 会不会阻碍页面的渲染呢?

答案:CSS 是会阻碍页面渲染的

先通过几个例子来看看CSS是如何影响页面渲染的: 请求的CSS文件延迟3秒获得,来看看页面表现。

  1. link放在标签head里面

页面的效果是:首先页面空白,3秒后页面会显示出带有样式的内容。可以看出link是阻碍页面渲染的。

<!DOCTYPE html>
<html lang="en">
  <head>
    <link rel="stylesheet" href="http://localhost:8001/css" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">test</div>
    <div id="app1">124143</div>
  </body>
</html>
  1. link放在标签div#app后面

页面的效果是: 首先渲染第一个div的内容,然后等待3秒渲染后才渲染第二个div的内容。可以看出link是会阻碍后面内容的渲染的。

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
  </head>
  <body>
    <div id="app">test</div>
    <link rel="stylesheet" href="http://localhost:8001/css" />
    <div id="app1">124143</div>
  </body>
</html>
  1. link放在最后面

页面的效果是: 首先渲染两个div的内容,然后等待3秒渲染后给div添加样式。所以,为了不使页面出现闪动,所以link一般放在head标签中。

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
  </head>
  <body>
    <div id="app">test</div>
    <div id="app1">124143</div>
    <link rel="stylesheet" href="http://localhost:8001/css" />
  </body>
</html>

通过上面的例子,我们可以知道CSS和JS阻碍页面的表现非常类似,浏览器遇到link或者script首先会渲染标签之前的内容, 然后等待并执行link或者script里面的内容,所以它们都会阻碍link或者script后面内容的渲染

我们知道 JS 是通过阻碍HTML解析从而影响页面的渲染,那CSS是怎么阻碍页面渲染的呢?

从前面的内容中了解到,渲染流水线需要等 DOM 和 CSSOM 都构建好之后,渲染引擎就会构造布局树, 有了布局树才能进行页面的渲染。

所以 CSS 文件是肯定会阻碍页面的渲染的。

我们从另外一个角度来解释 CSS 文件为什么会阻碍页面渲染?

我们再来看看稍微复杂一点的场景,还是看下面这段 HTML 代码:

<html>
  <head>
    <style src="theme.css"></style>
  </head>
  <body>
    <div>1</div>
    <script>
      let div1 = document.getElementsByTagName('div')[0]
      div1.innerText = 'time.geekbang' //需要DOM
      div1.style.color = 'red' //需要CSSOM
    </script>
    <div>test</div>
  </body>
</html>

该示例中,JavaScript 代码出现了 div1.style.color = ‘red' 的语句,它是用来操纵 CSSOM 的,所以在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的 CSS 样式

所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本

而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本

所以说, CSS 在这种情况下也会阻塞 DOM 的生成。

总结下:CSS 不管在有没有js脚本的情况下都是会阻碍页面的渲染的。如果没有脚本,因为渲染需要布局树,布局树需要CSS,所以它会影响页面渲染。如果有脚本,因为脚本可能操作CSSOM,所以需要等CSS文件下载完成后才能执行脚本,它其实是阻碍了脚本的执行,间接的阻碍了DOM的解析。

其实这部分内容有点绕,而且浏览器内部极其复杂,我们只需要知道 CSS 和 JS 都会页面渲染。

浏览器在遇到link或者script标签之前都会先渲染这两个标签之前的内容,同时会阻碍后面内容的渲染,至于阻碍的时长就看网络情况和文件大小了

Performance

控制台中的Performance 可以记录站点在运行过程中的性能数据,有了这些性能数据,就可以回放整个页面的执行过程,这样就方便我们来定位和诊断每个时间段内页面的运行情况,从而有效的找出页面的性能瓶颈。

关于控制面板中的Performance说明信息,可以参考如何利用performance进行性能优化

我们监控页面的性能,主要是监控页面的生成速度是否足够的快,不产生卡顿。在 performance中有一个指标叫页面帧速 (FPS)

image.png

如果 FPS 图表上出现了红色块,那么就表示红色块附近渲染出一帧所需时间过久,帧的渲染时间过久,就有可能导致页面卡顿。

因此,我们通过概览面板(图的上部分)来定位问题的时间节点,然后再使用性能面板(图的中间部分,图的下部分是详细数据)分析该时间节点内的性能数据。具体地讲,比如概览面板中的 FPS 图表中出现了红色块,那么我们点击该红色块,性能面板就定位到该红色块的时间节点内了。

通过上面的学习,我们知道把输入的 HTML 数据经过一系列的处理,最后输出屏幕上的像素,这个过程叫做渲染流水线。一条完整的渲染流水线包括了解析 HTML 文件生成 DOM、解析 CSS 生成 CSSOM、执行 JavaScript、样式计算、构造布局树、准备绘制列表、光栅化、合成、显示等一系列操作。

用下面一张简图可以表示:

image.png

导航阶段

该阶段主要是从网络进程接收 HTML 响应头和 HTML 响应体。

image.png

  • 该任务的第一个子过程就是 Send request,该过程表示网络请求已被发送。然后该任务进入了等待状态。

  • 接着由网络进程负责下载资源,当接收到响应头的时候,该任务便执行 Receive Respone 过程,该过程表示接收到 HTTP 的响应头了。

  • 接着执行 DOM 事件:pagehide、visibilitychange 和 unload 等事件,如果你注册了这些事件的回调函数,那么这些回调函数会依次在该任务中被调用。

  • 这些事件被处理完成之后,那么接下来就接收 HTML 数据了,这体现在了 Recive Data 过程,Recive Data 过程表示请求的数据已被接收,如果 HTML 数据过多,会存在多个 Receive Data 过程。

  • 等到所有的 HTML 数据都接收完成之后,渲染进程会触发另外一个任务,该任务主要执行 Finish load 过程,该过程表示网络请求已经完成。

到此阶段,HTML 数据全部接收完成。 注意: 这里只是 HTML 数据,而不是 JS/CSS 数据

解析 HTML 数据阶段

这个阶段的主要任务就是通过解析 HTML 数据、解析 CSS 数据、执行 JavaScript 来生成 DOM 和 CSSOM。

image.png

  • 在 ParserHTML 的过程中,如果解析到了 script 标签,那么便进入了脚本执行过程,也就是图中的 Evalute Script。

  • 要执行一段脚本我们需要首先编译该脚本,于是在 Evalute Script 过程中,先进入了脚本编译过程,也就是图中的 Complie Script。脚本编译好之后,就进入程序执行过程,执行全局代码时,V8 会先构造一个 anonymous 过程,在执行 anonymous 过程中,会调用 setNewArea 函数,setNewArea 执行过程中又调用了 createElement 函数,由于之后调用了 document.append 方法,该方法会触发 DOM 内容的修改,所以又强制执行了 ParserHTML 过程生成的新的 DOM。

  • DOM 生成完成之后,会触发相关的 DOM 事件,比如典型的 DOMContentLoaded,还有 readyStateChanged。

生成可显示的位图阶段

生成了 DOM 和 CSSOM 之后,就进入了第三个阶段:生成页面上的位图。通常这需要经历布局 (Layout)、分层、绘制、合成等一系列操作,同样,将第三个阶段的流程也放大了,如下图所示:

image.png

在生成完了 DOM 和 CSSOM 之后,渲染主线程首先执行了一些 DOM 事件,诸如 readyStateChange、loadpageshow。具体地讲,如果你使用 JavaScript 监听了这些事件,那么这些监听的函数会被渲染主线程依次调用。

到此为止,我们算是把前端性能方面的知识总结完了,那如果用代码实现页面性能的展示呢?

性能监控代码实现

要实现性能监控,就需要使用浏览器提供的一个API: Navigation Timing

Navigation Timing API提供了可用于衡量一个网站性能的数据。我们可以使用它在客户端收集性能数据,并用XMLHttpRequest或其它技术传送到服务端。同时,该 API 使你可以衡量之前难以获取的数据,如卸载前一个页面的时间,在域名解析上的时间,在执行 load 事件处理器上花费的总时间等。

该 API 所有属性可用如下这个表来展示:

image.png

如果看不懂上图,没关系,下面是更为简洁的一个分类:

image.png

详细看过文章前半部分,对这个图应该非常了解了。下面就用代码的形式把这个流程花费的时间进行落地。

Navigation Timing

window.addEventListener('load', function () {
    const timing = window.performance.timing
    console.log('timing', timing)
    // DNS解析花费的时间
    const dns = timing.domainLookupEnd - timing.domainLookupStart
    console.log('dns', dns)
    // TCP 连接的时间
    const tcp = timing.connectEnd - timing.connectStart
    console.log('tcp', tcp)
    // 从发送请求到获取到数据的时间
    const http = timing.responseEnd - timing.requestStart
    console.log('http', http)
    // DOM 解析的事件
    const dom = timing.domComplete - timing.domLoading
    console.log('dom', dom)
    // 执行load事件的事件,不过这个时间在这个api获取不到
    const load = timing.loadEventEnd - timing.loadEventStart
    console.log('load', load)
  })

但是利用window.performance.timing有如下的几个问题:

  1. 精度不足,精度只能精确到毫秒级别
  2. 时机不容易把握,如:无法在load事件中监听loadEventEnd,即无法知道load事件何时结束

PerformanceNavigationTiming

window.addEventListener('load', function () {
    const perfEntries = window.performance.getEntries()
    console.log(perfEntries)
})

从打印结果上可以看到,它能获取到很多信息,包括每个资源(脚本script,图片img)加载的生命周期,以及首屏渲染FP等的信息。

image.png

但是这个 API 仍然是在 load 事件中执行的,它也无法获取到什么时候load执行完的数据。

所以,在项目中我们需要使用PerformanceObserver来获取数据。

PerformanceObserver

PerformanceObserver的基本用法如下:

function observer_callback(list, observer) {
  list.getEntries().forEach(e => {
    if (timing.entryType === 'navigation') {
        const dns = timing.domainLookupEnd - timing.domainLookupStart
        console.log('dns', dns)
        const tcp = timing.connectEnd - timing.connectStart
        console.log('tcp', tcp)
        const http = timing.responseEnd - timing.requestStart
        console.log('http', http)
        const dom = timing.domComplete - timing.domInteractive
        console.log('dom', dom)
        const load = timing.loadEventEnd - timing.loadEventStart
        console.log('load', load)
      }
  });
}

let observer = new PerformanceObserver(observer_callback);
observer.observe({ entryTypes: ['navigation' });

这个 API 会等所有的性能数据采集完成之后触发,这个时候就会拿到所有的数据。如下:

image.png

image.png

但是,一般我们看页面性能的时候,不太可能去看每个阶段的性能数据,一般都是通过几个指标来表示页面的性能如何,下面我们将介绍 FP、FCP 和 LCP 等性能指标。

FP、FCP 和 LCP

建议可以先阅读下这篇文章你还傻傻分不清楚 FP、FCP、LCP、DCL(DOMContentLoaded) 、L(Load) 出现的时机吗?,然后在往下阅读。

页面在从加载到渲染完成这个阶段,会触发下面的这些事件(顺序并不是按照下面的顺序触发):

  • load(Onload Event),它代表页面中依赖的所有资源加载完的事件。

  • DCL(DOMContentLoaded),DOM解析完毕。

  • FP(First Paint),表示渲染出第一个像素点。FP一般在HTML解析完成或者解析一部分时候触发。

  • FCP(First Contentful Paint),表示渲染出第一个内容,这里的”内容”可以是文本、图片、canvas。

  • FMP(First Meaningful Paint),首次渲染有意义的内容的时间,”有意义”没有一个标准的定义,FMP的计算方法也很复杂。

  • LCP(largest contentful Paint),最大内容渲染时间。

image.png

白屏和首屏

  • 白屏时间 = 地址栏输入网址后回车 – 浏览器出现第一个元素

  • 首屏时间 = 地址栏输入网址后回车 – 浏览器第一屏渲染完成

根据白屏和首屏的定义,我们可以用FP和FCP来计算白屏和首屏。

白屏结束时间 = FP事件触发时间

首屏结束时间 = FCP事件触发时间

当然FCP代表第一个内容被渲染出来,有些业务中希望用更关键的内容的渲染来表示首屏,这时候可以用FMP或者LCP来作为首屏的计算指标。

window.addEventListener('load', function () {
    onst paint = window.performance.getEntriesByType('paint')
    console.log('paint', paint)
})

image.png

也就是从在浏览器的地址栏输入网址后,到渲染首个元素花费了 99 毫秒。

除了使用window.performance.getEntriesByType外,我们还可以使用PerformanceObserver:

function callback(perf) {
    perf.getEntries().forEach(timing => {
      console.log(timing)
    })
}
const observer = new PerformanceObserver(callback)
observer.observe({
    entryTypes: ['paint', 'largest-contentful-paint']
})

性能打点

现在如果你想知道你的一段js代码是什么时候执行的,那么就需要performance提供的打点功能。

比如现在要知道load事件里面的代码是什么时候执行的,就可以用window.performance.mark打点

window.addEventListener('load', function () {
    window.performance.mark('onload')
})

然后在PerformanceObserver里面监听:

observer.observe({
    entryTypes: ['navigation', 'mark']
})

现在我们知道获取性能监控有三种方式:

  • 手动监控:在load事件中使用window.performance.timing或者window.performance.getEntriesByType获取性能数据

  • 自动监控: 利用PerformanceObserver获取性能指标数据

  • 自定义监控: 利用window.performance.mark进行某个自定义节点的数据监控

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容