前端性能优化——关键渲染路径
总结于: Critical Rendering Path
关键渲染路径与性能优化
从收到 HTML、CSS 和 JavaScript 字节到对其进行必需的处理,从而将它们转变成渲染的像素这一过程中有一些中间步骤,优化性能其实就是了解这些步骤中发生了什么, 即关键渲染路径。优化关键渲染路径是指优先显示与当前用户操作有关的内容。
通过优化关键渲染路径,我们可以显著缩短首次渲染页面的时间。 此外,了解关键渲染路径还可以为构建高性能交互式应用打下基础。
渲染一个网页,浏览器需要完成的步骤:
- 处理 HTML 标记并构建 DOM 树。
- 处理 CSS 标记并构建 CSSOM 树。
- 将 DOM 与 CSSOM 合并成一个渲染树。
- 根据渲染树来布局(Layout),以计算每个节点的几何信息。
- 将各个节点绘制到屏幕上。
我们的演示网页看起来可能很简单,实际上却需要完成相当多的工作。如果 DOM 或 CSSOM 被修改,您只能再执行一遍以上所有步骤,以确定哪些像素需要在屏幕上进行重新渲染。
优化关键渲染路径就是指最大限度缩短执行上述第 1 步至第 5 步耗费的总时间。 这样一来,就能尽快将内容渲染到屏幕上,此外还能缩短首次渲染后屏幕刷新的时间,即为交互式内容实现更高的刷新率。
关键渲染路径性能分析
发现和解决关键渲染路径性能瓶颈需要充分了解常见的陷阱。 让我们踏上实践之旅,找出常见的性能模式,从而帮助您优化网页。
让我们定义一下用来描述关键渲染路径的词汇:
- 关键资源: 可能阻止网页首次渲染的资源。
- 关键路径长度: 获取所有关键资源所需的往返次数或总时间。
- 关键字节: 实现网页首次渲染所需的总字节数,它是所有关键资源传送文件大小的总和。我们包含单个 HTML 页面的第一个示例包含一项关键资源(HTML 文档);关键路径长度也与 1 次网络往返相等(假设文件较小),而总关键字节数正好是 HTML 文档本身的传送大小。
Hello World 体验
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>Critical Path: No Style</title> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
我们将从基本 HTML 标记和单个图像(无 CSS 或 JavaScript)开始。让我们在 Chrome DevTools 中打开 Network 时间线并检查生成的资源瀑布:
当 HTML 内容可用后,浏览器会解析字节,将它们转换成令牌,然后构建 DOM 树。请注意,为方便起见,DevTools 会在底部报告 DOMContentLoaded 事件的时间(216 毫秒),该时间同样与蓝色垂直线相符。HTML 下载结束与蓝色垂直线 (DOMContentLoaded) 之间的间隔是浏览器构建 DOM 树所花费的时间 — 在本例中仅为几毫秒。
请注意,我们的“趣照”并未阻止 domContentLoaded 事件。这证明,我们构建渲染树甚至绘制网页时无需等待页面上的每个资产:并非所有资源都对快速提供首次绘制具有关键作用。事实上,当我们谈论关键渲染路径时,通常谈论的是 HTML 标记、CSS 和 JavaScript。图像不会阻止页面的首次渲染,不过,我们当然也应该尽力确保系统尽快绘制图像!
即便如此,系统还是会阻止图像上的 load 事件(也称为 onload):DevTools 会在 335 毫秒时报告 onload 事件。回想一下,onload 事件标记的点是网页所需的所有资源均已下载并经过处理的点,这是加载微调框可以在浏览器中停止微调的点(由瀑布中的红色垂直线标记)。
T0 与 T1 之间的时间捕获的是网络和服务器处理时间。在最理想的情况下(如果 HTML 文件较小),我们只需一次网络往返便可获取整个文档。由于 TCP 传输协议工作方式的缘故,较大文件可能需要更多次的往返。因此,在最理想的情况下,上述网页具有单次往返(最少)关键渲染路径。
使用外部 CSS 文件
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> </body> </html>
我们同样需要一次网络往返来获取 HTML 文档,然后检索到的标记告诉我们还需要 CSS 文件;这意味着,浏览器需要返回服务器并获取 CSS,然后才能在屏幕上渲染网页。因此,这个页面至少需要两次往返才能显示出来。CSS 文件同样可能需要多次往返,因此重点在于最少。
这里有:
- 2 项关键资源
- 2 次或更多次往返的最短关键路径长度
- 9 KB 的关键字节
我们同时需要 HTML 和 CSS 来构建渲染树。所以,HTML 和 CSS 都是关键资源:CSS 仅在浏览器获取 HTML 文档后才会获取,因此关键路径长度至少为两次往返。两项资源相加共计 9KB 的关键字节。
使用外部的 Javascript 文件及 CSS 文件
现在,让我们向组合内额外添加一个 JavaScript 文件。
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js"></script> </body> </html>
我们添加了 app.js,它既是网页上的外部 JavaScript 资源,又是一种解析器阻止(即关键)资源。更糟糕的是,为了执行 JavaScript 文件,我们还需要进行阻止并等待 CSSOM;回想一下,JavaScript 可以查询 CSSOM,因此在下载 style.css 并构建 CSSOM 之前,浏览器将会暂停。
即便如此,如果我们实际查看一下该网页的“网络瀑布”,就会注意到 CSS 和 JavaScript 请求差不多是同时发起的;浏览器获取 HTML,发现两项资源并发起两个请求。
因此,上述网页具有以下关键路径特性:
- 3 项关键资源
- 2 次或更多次往返的最短关键路径长度
- 11 KB 的关键字节
现在,我们拥有了三项关键资源,关键字节总计达 11 KB,但我们的关键路径长度仍是两次往返,因为我们可以同时传送 CSS 和 JavaScript。了解关键渲染路径的特性意味着能够确定哪些是关键资源,此外还能了解浏览器如何安排资源的获取时间。让我们继续探讨示例。
内联 JavaScript:
我们减少了一个请求,但 onload 和 domContentLoaded 时间实际上没有变化。为什么呢?怎么说呢,我们知道,这与 JavaScript 是内联的还是外部的并无关系,因为只要浏览器遇到 script 标记,就会进行阻止,并等到 CSSOM 构建完毕。此外,在我们的第一个示例中,浏览器是并行下载 CSS 和 JavaScript,并且差不多是同时完成。在此实例中,内联 JavaScript 代码并无多大意义。但是,我们可以通过多种策略加快网页的渲染速度。
script 标记添加“async”属性
向 script 标记添加“async”属性来解除对解析器的阻止:
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js" async></script> </body> </html>
异步(外部)JavaScript:
解析 HTML 之后不久即会触发 domContentLoaded 事件;浏览器已得知不要阻止 JavaScript,并且由于没有其他阻止解析器的脚本,CSSOM 构建也可同步进行了。
异步脚本具有以下几个优点:
- 脚本不再阻止解析器,也不再是关键渲染路径的组成部分。
- 由于没有其他关键脚本,CSS 也不需要阻止 domContentLoaded 事件。
- domContentLoaded 事件触发得越早,其他应用逻辑开始执行的时间就越早。
因此,我们优化过的网页现在恢复到了具有两项关键资源(HTML 和 CSS),最短关键路径长度为两次往返,总关键字节数为 9 KB。
CSS 媒体类型和媒体查询
如果 CSS 样式表只需用于打印,那会如何呢?
<html> <head> <meta name="viewport" content="width=device-width,initial-scale=1"> <link href="style.css" rel="stylesheet" media="print"> </head> <body> <p>Hello <span>web performance</span> students!</p> <div><img src="awesome-photo.jpg"></div> <script src="app.js" async></script> </body> </html>
因为 style.css 资源只用于打印,浏览器不必阻止它便可渲染网页。所以,只要 DOM 构建完毕,浏览器便具有了渲染网页所需的足够信息。因此,该网页只有一项关键资源(HTML 文档),并且最短关键渲染路径长度为一次往返。
优化步骤
为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:
- 关键资源的数量。
- 关键路径长度。
- 关键字节的数量。
关键资源是可能阻止网页首次渲染的资源。这些资源越少,浏览器的工作量就越小,对 CPU 以及其他资源的占用也就越少。
同样,关键路径长度受所有关键资源与其字节大小之间依赖关系图的影响:某些资源只能在上一资源处理完毕之后才能开始下载,并且资源越大,下载所需的往返次数就越多。
最后,浏览器需要下载的关键字节越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。
优化关键渲染路径的常规步骤如下:
- 对关键路径进行分析和特性描述:资源数、字节数、长度。
- 最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等。
- 优化关键字节数以缩短下载时间(往返次数)。
- 优化其余关键资源的加载顺序:您需要尽早下载所有关键资产,以缩短关键路径长度。
评估方法
作为每个可靠性能策略的基础,准确的评估和检测必不可少。 无法评估就谈不上优化。本文说明了评估 CRP(关键渲染路径)
性能的不同方法。
因为 style.css 资源只用于打印,浏览器不必阻止它便可渲染网页。所以,只要 DOM 构建完毕,浏览器便具有了渲染网页所需的足够信息。因此,该网页只有一项关键资源(HTML 文档),并且最短关键渲染路径长度为一次往返。
使用 Lighthouse 检测关键请求链
关键请求链这个概念源自关键渲染路径 (CRP) 优化策略。 CRP 通过确定优先加载的资源以及加载顺序,允许浏览器尽可能快地加载页面。
在 Lighthouse 的 Chrome 扩展程序版本中,您的报告将生成一个类似如下的图表:
Initial navigation |---lighthouse/ (developers.google.com) |---/css (fonts.googleapis.com) - 1058.34ms, 72.80KB |---css/devsite-googler-buttons.css (developers.google.com) - 1147.25ms, 70.77KB |---jsi18n/ (developers.google.com) - 1155.12ms, 71.20KB |---css/devsite-google-blue.css (developers.google.com) - 2034.57ms, 85.83KB |---2.2.0/jquery.min.js (ajax.googleapis.com) - 2699.55ms, 99.92KB |---contributors/kaycebasques.jpg (developers.google.com) - 2841.54ms, 84.74KB |---MC30SXJEli4/photo.jpg (lh3.googleusercontent.com) - 3200.39ms, 73.59KB
此图表表示页面的关键请求链。从 lighthouse/ 到 /css 的路径形成一条链。 从 lighthouse/ 到 css/devsite-googler-buttons.css
的路径形成另一条链。 以此类推。审查的最高得分体现了这些链条的数量。 例如,上面的图表的分数为七分。
该图表也详细列出下载每个资源花了多少时间,以及下载每个资源所需的字节数。
您可以根据此图表利用以下方式提升您的 CRP:
- 将关键资源数降至最低:消除关键资源、延迟关键资源的下载并将它们标记为不同步等。
- 优化关键字节数以缩短下载时间(往返次数)。
- 优化其余关键资源的加载顺序:尽早下载所有关键资产,以缩短关键路径长度。
优化以上任一因素都可提升页面加载速度。
详情请参阅关键请求链
使用 Navigation Timing API 设置您的代码
结合使用 Navigation Timing API 和页面加载时发出的其他浏览器事件,您可以捕获并记录任何页面的真实 CRP 性能。
上图中的每一个标签都对应着浏览器为其加载的每个网页追踪的细粒度时间戳。
那么,这些时间戳有什么含义呢?
- domLoading:这是整个过程的起始时间戳,浏览器即将开始解析第一批收到的 HTML 文档字节。
- domInteractive:表示浏览器完成对所有 HTML 的解析并且 DOM 构建完成的时间点。
domContentLoaded:表示 DOM 准备就绪并且没有样式表阻止 JavaScript 执行的时间点,这意味着现在我们可以构建渲染树了。
- 许多 JavaScript 框架都会等待此事件发生后,才开始执行它们自己的逻辑。因此,浏览器会捕获 EventStart 和 EventEnd 时间戳,让我们能够追踪执行所花费的时间。
- domComplete:顾名思义,所有处理完成,并且网页上的所有资源(图像等)都已下载完毕,也就是说,加载转环已停止旋转。
- loadEvent:作为每个网页加载的最后一步,浏览器会触发 onload 事件,以便触发额外的应用逻辑。
HTML 规范中规定了每个事件的具体条件:应在何时触发、应满足什么条件等等。对我们而言,我们将重点放在与关键渲染路径有关的几个关键里程碑上:
- domInteractive 表示 DOM 准备就绪的时间点。
- domContentLoaded 一般表示 DOM 和 CSSOM 均准备就绪的时间点。
如果没有阻塞解析器的 JavaScript,则 DOMContentLoaded 将在 domInteractive 后立即触发。
- domComplete 表示网页及其所有子资源都准备就绪的时间点。
PageSpeed 规则和建议
消除阻塞渲染的 JavaScript
要以最快速度完成首次渲染,需要最大限度减少网页上关键资源的数量并(尽可能)消除这些资源,最大限度减少下载的关键字节数,以及优化关键路径长度。
延迟解析 JavaScript
为了最大限度减少浏览器渲染网页的工作量,应延迟任何非必需的脚本(即对构建首次渲染的可见内容无关紧要的脚本)。
CSS 标记为非关键资源
CSS 是构建渲染树的必备元素,首次构建网页时,JavaScript 常常受阻于 CSS。确保将任何非必需的 CSS 都标记为非关键资源(例如打印和其他媒体查询),并应确保尽可能减少关键 CSS 的数量,以及尽可能缩短传送时间。
将 CSS 置于文档 head 标签内
尽早在 HTML 文档内指定所有 CSS 资源,以便浏览器尽早发现 <link> 标记并尽早发出 CSS 请求。
避免使用 CSS import
一个样式表可以使用 CSS import (@import) 指令从另一样式表文件导入规则。不过,应避免使用这些指令,因为它们会在关键路径中增加往返次数:只有在收到并解析完带有 @import 规则的 CSS 样式表之后,才会发现导入的 CSS 资源。
参考
- Critical Rendering Path
- 网站性能优化—CRP