引子

经常有人问,页面是如何渲染的呢?回答解析html、css 合成Render Tree 然后就可以渲染了,但是具体都做了些什么呢?
如果我们需要对一个页面体验进行优化,我们应该关注那些指标?
也许你也曾有过这些疑惑,让我们从chrome的性能工具开始,逐步了解这些吧。

开始

我们从老二刺猿网站 www.bilibili.com 为例,来介绍下performance面板,通过分析performance面板,来串联起chrome页面渲染流程,以及页面的部分量化指标的含义。

获取performance数据

首先,打开chrome devTools, 选择 performace面板,点击录制⏺按钮就可以录制了。
获取performance数据
但是为了我们分析页面时没有无关的干扰,我们先需要完成以下步骤:

  1. 打开chrome 无痕模式。
  2. 关闭所有在chrome无痕模式下启用的拓展(如果有的话)。
  3. 在地址栏输入www.bilibili.com前,先打开devTools,选择performance面板,点击录制按钮。
  4. 在已经录制的情况下,地址栏回车,请求B站,大概10s后,停止录制。

此时我们可以得到一个色彩缤纷密密麻麻的图。
获取performance数据

这都是啥? 这些花花绿绿的都是啥意思?不要着急,我们慢慢看。
我们从上到下,将这些分成几块来看。
performance数据
从上到下分别是:
① 控制面板
② 概览面板
③ 网络面板
④ web Vitals
⑤ 线程面板
⑥ 内存面板
⑦ 聚合面板

首先要分别简述下各自的用途:

控制面板

有4个内容:
控制面板

  • disable javascript samples:启用后会隐藏一些JS调用栈的展示。在一些性能较弱的设备例如移动端上,可以开启这项功能。
  • network:可以用来模拟各种网络状况。
  • enableadvanced paint instrumention (slow): 启用后paint 面板会显示与绘制相关事件的更详细的信息。
  • CPU:可以用来模拟不同的CPU性能。

概览面板

概览面板
这部分就是对各项指标的一个概览。有FPS帧数、CPU占用、NET情况、内存使用情况。
从这个面板我们可以直观的看出 FPS 在哪里比较低(FPS栏绿色低的部分)。
cpu那一栏里黄色代表js,紫色代表计算样式和布局。绿色代表绘制。

网络面板

网络面板
展示一些请求中的各部分组成情况。

web vitals

网站的web体验指标。其中把包括 LCP(最大内容绘制)、FID(首次输入延迟)、cls (累计布局偏移)等。
在面板中我们可以回看到:
web vitals
FCP是首次内容绘制
LCP是最大内容绘制
LS 是布局偏移

线程面板

展示渲染当前页面所使用到的线程。
有 Main线程、GPU线程、Raster线程、chrome_ChildIOThread、Compositor 线程等等。其中我们最关注的就是Main线程,也就是我们平时说的大部分js的运行环境,主线程。

内存面板

展示js内存、GPU内存、节点数、监听事件数的变化。

聚合面板

当点击主线程中的火焰图时,此面板会显示显示具体的信息集合。包括执行时间、执行组成、调用栈等等。

chrome是如何将页面渲染的?

从第一个请求开始

在performance面板我们从第1ms开始看起:
从第一个请求开始
就是上图中红线中的部分,也是中间NET栏蓝色细长条开始的部分,也是Network中水平箱线图开始的部分。

network面板与箱线图

在这里,对 https://www.bilibili.com 的请求,开启了~~ 老二刺猿 ~~ performance之旅。
首先我们要了解,这个两边横线中间深浅色方框的是什么玩意儿?
这个叫**水平箱线图**一般是用来展示某部分在整体中的比例关系的。也就是说,我们看到这个长长的箱型图,在直观感受上,就可以知道对前面一部分横线挺长的,蓝色部分里浅色部分很长,深色的短,右边的横线几乎看不到。
那么这个箱型图每一部分到底代表了啥呢?

点击这个箱型图,在最下面的聚合面板(Summary)里可以看到一些信息:
赫然写着,
此乃页面源,
欲求小破站。
终生皆让我,

耗时一秒半。
network面板与箱线图
然后我们在network tab里去查看这个请求,看他的timing部分:

Network tab 里的请求

Network tab 里的请求
我们来注意看看这里的各个部分代表什么:

Queueing 排队

浏览器会在一些情况下让请求排队等待,比如:

  1. 这个请求的优先级不高,有更高优先级的请求存在。
  2. 在使用HTTP/1.0 或者HTTP/1.1时,同域请求最大并发数量为6个,此时已经达到了最大值。
  3. 浏览器正在硬盘缓存中分配空间。

    我们是最高优先级的第一个请求,所以属于第3种情况,这14.72ms用来在磁盘缓存中分配空间了。

Stalled 停顿

它可能会因为上述排队中的任何原因而停顿

DNS lookup DNS查询

解析这个域名的IP地址。(当你多次访问这个域名的时候,可能在timing里就看不到这部分了)

Initial connection 初始连接

浏览器建立连接,包括tcp三次握手、重试 以及协商ssl。图中的紫色部分,就代表了在初始连接过程中的ssl协商部分。

Request sent 发送请求

正在发送请求

Waiting (TTFB) 等待第一字节时间

浏览器在等待第一个响应的字节,TTFB即 Time To First Byte。这个时间包括一个往返的延迟和服务准备响应的时间之和。

Content Download 内容下载

浏览器正在接收响应,浏览器可以通过网络或者serviceWorker来直接接收。这个值是读取响应体的总时间。由于网络不佳或者浏览器正在忙于执行其他工作而延迟了对响应体的读取,读取的时间可能会比预期的要长
注意这里,浏览器忙于其他事情也会让读取时间边长,也就是说,当你的js把主线程长期占据的时候,也会影响content download哦…

我们来看下network下的对应资源的 waterfall:
内容下载

所以在水平箱线图中:
内容下载
左上角的深蓝色小方块代表着这个请求有着更高的优先级。遇到有浅蓝色的,则表示较低优先级。
左边横线对应NetWork面板中显示的Request Sent之前的所有的事情的时间
浅色的bar对应NetWork中Request Sent 和Waiting(TTFB)的时间
深色的bar对应NetWork中Content Download的时间
右边的横线表示等待主线程所花费的时间, 在network面板中没有体现。

可能有些同学注意到,在蓝色箱线图上面还可以看到还有几个灰色的箱线图,不是说www.bilibili.com 是页面的第一个请求吗?怎么它之前还有请求?
如果你是通过重新录制的方式记录performance的,那么就会经历页面刷新的过程,上面几个灰色的其实是页面刷新unload时发起的,是bilibili用来记录页面卸载时的一些数据,刷新页面也会触发的。
相当于是上一个页面的结束,不用在意。

在summary中显示 Duration 1.08 s (822.88 ms network transfer + 260.20 ms resource loading)
也就是260ms的时间是在 resource loading ,这里resource loading所花费的时间其实就是箱线图右侧的那条横线,等待主线程的时间。
在main进程中,有横线结束的地方,可以看到:
内容下载
解码的数据 138,933 Bytes
但是这里也有几个问题:
为什么这里的Encode Data 33479 bytes算下来 33479/1024 = 32.69 k ?不是 前面network 面板里的 33.5k ?
而且 Decode body 138993/1024 = 135.7k也不是前面的139k
这少的一部分数据是什么?

为了验证这个问题,清空过去所有请求记录,重新点击录制,录制完成后,导出网络请求的HAR文件。
这个文件可以在vscdoe打开,是json格式的,在这个文件里,寻找 GET https://www.bilibili.com/ content-Type: text/html的那个请求。
经过对比,找到了这个请求的 response content

内容下载


可以看到,这里的size,是140682,单位是字节。
这里的text是base64编码的HTML内容,已经是被decode过的了。(这里的decode不是对base64的decode,是对gzip的decode)
在这个长长的text内容之后,还有一段内容:

内容下载


里面有个_transferSize: 35593,这个是网络传输的体积。此时我们得到了传输的体积 35593 和 decode体积 140682
而我们在performance里的 主进程中的 finish loading中看到:

内容下载


这里是对上了!说明这个HTML的传输体积就是 35593 Bytes !!!
而在network面板里,我们却看到:
内容下载


赫然写着35.6k transferred over network!!!
这就说明,在network里展示的体积,不是除以1024计算的,而是除以1000,然后四舍五入后的结果。
或许这是有意为之,但是我们的问题至少有了答案。

但是Summary 里的pending for xxx ms, 似乎是也是等待主线程的时间,但如何在performance体现的还没搞清楚。

请求资源

言归正传,我们现在获取到了 bilibili这个html了。那么就需要对这个HTML进行处理。
通过response header 我们得知了content-type: html,那么一个渲染进程就会被创建,也就是我们拥有主线程的这个进程。
在主线程中的 蓝色parse HTML 之前,已经有很多set request 被发起了,这些send request是 html文档中的一些js和css。
为什么会这样呢? 不应该是先 解析 html,才能知道对哪些资源进行发起请求吗?
内容下载


在html中引入的js,存在修改dom的可能,所以浏览器在遇到一般script标签的时候,会暂停html解析,而是先下载,然后执行js。
一般来说下载是相对耗时的,如果因为下载时间久而卡住了页面解析,体验就会很差,于是chrome采用了一些优化策略。
当chrome渲染引擎接收到html的字节流时候,就会开启一个预解析线程,专门用来分析字节流中所包含的js、css文件,解析到相关信息之后,预解析线程会提前开始下载这些资源文件,等用到的时候直接执行就好了。

但是也能观察到,在Parse HTML蓝色方块下方,其实也有一些 send request, 为什么有些是提前下载的,而有些是Parse HTML 过程中下载的呢?
对此我的理解是,这些资源其实都是在预解析线程下载的,由于和主线程不属于同一个线程,在时间上他们会存在重叠。后面那几个 send request 与 parse html 这个过程在时间上重叠,所以performance 工具会这么显示。但这又有另外的问题,为什么有些js明明在html的后面,却在前面就send request了,而有些link/script明明写在html里的前面,却在performance里后 send request ?
我猜测跟跟资源的优先级有关。
这些资源可能会被优先处理:

  1. 普通的script标签引用的资源
  2. 普通link引用的资源
  3. rel=prelaodas="style"预加载的资源

而当资源是 prefetch、或者用 <link rel="stylesheet" href="//s1.hdslb.com/bfs/static/jinkela/long/font/regular.css" media="print" onload="this.media='all'"/>这种方式的,由于优先级低,可能会被延后下载。
一般的其他资源,则按顺序下载。

同时在network面板里,可以看到在www.bilibil.com的箱线图之后,是一连串js、css、webp 资源需要加载的请求被发起了。
内容下载


把鼠标移动到这些箱线图上,可以看到上面有优先级 lowest low high highest 。这表示资源的重要程度。

一般来说,

  1. 访问域名获取的html、 以及预加载资源时as="style", 拥有最高优先级。
  2. 普通的