页面是如何渲染的?通常会得到“解析 HTML、css 合成 Render Tree,就可以渲染了”的回答。但是具体都做了些什么,却很少有人细说,我们今天就从 Chrome 的性能工具开始,具体看看一个页面是如何进行渲染的,以及进行页面优化时需要关注哪些指标。
以“老二次元”网站 bilibili 为例,我们将通过分析 performance 面板,串联起 Chrome 页面渲染流程,以及页面的部分量化指标的含义,来看页面具体是如何渲染的。
获取performance数据
首先,打开Chrome devTools, 选择 performace面板,点击录制按钮开始录制。
之后为了防止我们分析页面时出现无关的干扰,我们通过以下步骤降低干扰项:
1、打开 Chrome 无痕模式。
2、关闭所有在 Chrome 无痕模式下启用的拓展(如果有的话)。
3、在地址栏输入 www.bilibili.com 前,先打开 devTools,选择 performance 面板,点击录制按钮。
4、在已经录制的情况下,地址栏回车,请求 B 站,大概 10s 后,停止录制。
我们从上到下,将图分成以下几块,如下图所示:
1、控制面板
2、概览面板
3、网络面板
4、Web Vitals
5、线程面板
6、内存面板
7、聚合面板
控制面板
控制面板有 4 部分内容,分别为:
-
disable javascript samples:启用后会隐藏一些 JS 调用栈的展示。在一些性能较弱的设备例如移动端上,可以开启这项功能。
-
Network:可以用来模拟各种网络状况。
-
enableadvanced paint instrumention (slow):启用后 paint 面板会显示与绘制相关事件的更详细的信息。
CPU:可以用来模拟不同的 CPU 性能。
概览面板
概览面板是各项指标的一个概览,包含了 FPS 帧数、CPU 占用、NET 情况、内存使用情况等。
简单举个例子,比如 FPS 帧数可以直观的看出 FPS 的高低,绿色代表低的部分。而 CPU 栏的黄色代表着 js,紫色代表计算样式和布局,绿色代表绘制。
网络面板
网络面板用于展示正在请求中的各部分的组成情况。
Web vitals
Web vitals 是网站的 Web 体验指标,其中包括 LCP(最大内容绘制)、FID(首次输入延迟)、cls (累计布局偏移)等。
线程面板
线程面板用于展示渲染当前页面所使用到的线程,包含有 Main 线程、GPU 线程、Raster 线程、Chrome_ChildIOThread、Compositor 线程等等。其中 Main 线程,就是我们平时说的大部分 js 的运行环境,即主线程。
内存面板
展示 js 内存、GPU 内存、节点数、监听事件数的变化。
聚合面板
当点击主线程中的火焰图时,此面板会显示显示具体包含执行时间、执行组成、调用栈等等的信息集合。
Chrome是如何渲染页面的?
第一个请求
以第一个请求为例,我们来具体看一下 Chrome 是如何进行页面渲染的?依然是以对 https://www.bilibili.com 的请求为例,来看一下 1ms 的 performance 面板,即下图中红线部分、中间 NET 栏蓝色细长条开始的部分和 Network 中水平箱线图开始的部分。
其中两边横线中间深浅色方框的部分是水平箱线图,是用来展示某部分在整体中的比例关系。比如我们看到这个长长的箱型图,通过直观感受,就能知道对前面一部分横线挺长的,蓝色部分里浅色部分很长,深色的短,右边的横线几乎看不到。那这些又分别能展示什么信息?
首先,点开箱型图最下方的聚合面板(Summary),上面赫然写着:此乃页面源。欲求小破站, 终生皆让我……耗时一秒半。
然后在 Network tab 里查看该请求的 timing 部分,可以得到如下图:
这里的各个部分分别代表:
- Queueing(排队):浏览器会在一些情况下让请求排队等待,比如这个请求的优先级不高,有更高优先级的请求存在;在使用 HTTP/1.0 或者 HTTP/1.1 时,同域请求最大并发数量为 6 个,此时已经达到了最大值;而上图中的请求是属于最高优先级的第一个请求,即浏览器正在硬盘缓存中分配空间,从图上可以看到有 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 格式的 HAR 文件,寻找 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,这些怎么就不是提前下载的呢?
我的理解是,这些资源其实都是在预解析线程下载的,尽管在时间上会存在重叠,但和主线程不属于同一个线程,所以 performance 工具会这么显示。但这又带来了另一个问题,为什么有些 js 明明在 HTML 的后面,却在前面就 send request 了,而有些 link/script 明明写在 HTML 里的前面,却在 performance 里后 send request?
这是跟资源的优先级有关。比如普通的 script 标签引用的资源,普通 link 引用的资源,或是rel=prelaod 或 as="style"预加载的资源,可能会被优先处理。而当资源是 prefetch,或者用 这种方式的,由于优先级低,就会被延后下载。一般的其他资源,则按顺序下载。
回到 Network,可以看到在 www.bilibil.com 的箱线图之后,是一连串 js、css、Webp 资源需要加载的请求被发起了。把鼠标移动到这些箱线图上,会看到上面有优先级 lowest low high highest,这就表示了资源的重要程度。
那么这些资源的优先级是如何评定的?一般来说,访问域名获取的 HTML、 以及预加载资源时as="style",拥有最高优先级。普通的