详解 CRP:如何最大化提高首屏渲染速度

在前端性能优化树上有不少值得展开的话题,从输入 URL 到页面加载完成发生了什么 这一道经典的面试题就涉及到不少内容,但前端主要关注的部分仍是 浏览器解析响应的内容并渲染展现给用户 这一步,本文将会详细分析这一步的具体过程并在分析的过程当中理解该如何作性能优化。css

首先介绍一个名词 CRP,即 关键渲染路径 (Critical Rendering Path)(后文统一以 CRP 指代):html

关键渲染路径是浏览器将 HTML CSS JavaScript 转换为在屏幕上呈现的像素内容所经历的一系列步骤。前端

将 HTML 转换成 DOM 树

当咱们请求某个 URL 之后,浏览器得到响应的数据并将全部的标记转换到咱们在屏幕上所看到的 HTML,有没有想过这中间发生了什么?git

浏览器会遵循定义好的完善步骤,从处理 HTML 和构建 DOM 开始:github

  • 浏览器从磁盘或网络中读取 HTML 原始字节,并根据文件的指定编码将它们转成字符。
  • 当遇到 HTML 标记时,浏览器会发出一个令牌,生成诸如 StartTag: HTML StartTag:head Tag: meta EndTag: head 这样的令牌 ,整个浏览由令牌生成器来完成。
  • 在令牌生成的同时,另外一个流程会同时消耗这些令牌并转换成 HTML head 这些节点对象,起始和结束令牌代表了节点之间的关系。
  • 当全部的令牌消耗完之后就转换成了DOM(文档对象模型)。

DOM 是一个树结构,表示了 HTML 的内容属性以及各个节点之间的关系。web

ToDOM

好比如下代码:面试

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>
复制代码

最终就转成下面的 DOM 树:浏览器

DOM

浏览器如今有了页面的内容,那么该如何展现这个页面自己呢?缓存

将 CSS 转换成 CSSOM 树

与转换 HTML 相似,浏览器首先会识别 CSS 正确的令牌,而后将这些令牌转成 CSS 节点,子节点会继承父节点的样式规则,这就是层叠规则和层叠样式表。性能优化

ToCSSOM

好比上面的 HTML 代码有如下的 CSS :

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
复制代码

最终就转成下面的 CSSOM 树:

CSSOM

这里须要特别区分的是,DOM 树会逐步构建来使页面更快地呈现,可是 CSSOM 树构建时会阻止页面呈现

缘由很简单,若是 CSSOM 树也能够逐步呈现页面的话,那么以后新生成的子节点样式规则有可能会覆盖以前的规则,这就会致使页面的错误渲染。

让咱们来作一个思考题,请看如下的 HTML 代码:

<div>
    <h1>H1 title</h1>
    <p>Lorem...</p>
</div>
复制代码

对于如下两个样式规则,哪一个样式规则会渲染得更快?

h1 { font-size: 16px }
div p { font-size: 12px }
复制代码

直觉上很容易以为第二个规则是更具体的,应该会渲染更快,但实际上偏偏相反:

  • 第一条规则是很是简单的,一旦遇到 h1 标记,就会将字号设成 16px。
  • 第二条规则更复杂,首先它规定了咱们应该知足全部 p 标记,可是当咱们找到 p 标记时,还须要向上遍历 DOM 树,只有当父节点是 div 时才会应用这个规则。
  • 因此更加具体的标记要求浏览器处理的工做更多,实际编写中应该尽量避免编写过于具体的选择器。

那么到如今为止,DOM 树包含了页面的全部内容,CSSOM 树包含了页面的全部样式,接下来如何将内容和样式转成像素显示到屏幕上呢?

将 DOM 和 CSSOM 树组成渲染树

浏览器会从 DOM 树的根部开始看有没有相符的 CSS 规则,若是有的话就将节点和样式复制到渲染树上,没有的话就只将节点复制过来,而后继续向下遍历。

特别要注意的是,渲染树最重要的特性是只捕获可见内容 :

  • 对于特殊节点(html head)等,由于它们不会被渲染,所以会直接跳过。
  • 若是一个节点的属性标记为 display: none,表示这个节点不该该呈现,则这个节点和其子项都会直接跳过。

好比如下将 DOM 树和 CSSOM 树合并成渲染树的结果:

渲染树

如今咱们已经有了渲染树,接下来要作的是肯定元素在页面上的位置。

布局与绘制

咱们考虑如下的代码:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>
复制代码

浏览器在渲染时会将这里父 div 的宽度设置成 body 的 50%,将子 div 的宽度设成父 div 的 50%,那么这里 body 的宽度是如何肯定的?

注意咱们在 meta 标签中设置了一行代码:

<meta name="viewport" content="width=device-width,initial-scale=1">
复制代码

咱们在实际进行自适应网页设计时都会加上这行代码表示布局视口的宽度等于设备的宽度,所以呈现出来就是这样:

viewport

最后一步就是将全部准备好的内容 绘制 到页面上。

任什么时候候咱们想要更新渲染树时,可能都会从新进行布局和绘制这一过程,浏览器自己会采起各类智能的功能尝试从新绘制最低请求区域,但具体仍是取决于咱们向渲染树应用了哪一种类型的更新。

如何优化

在谈优化以前,咱们先定义一下用来描述 CRP 的词汇:

  • 关键资源: 可能阻止网页首次渲染的资源。
  • 关键路径长度: 获取全部关键资源所需的往返次数或总时间。
  • 关键字节: 实现网页首次渲染所需的总字节数,等同于全部关键资源传送文件大小的总和。

结合咱们谈过的步骤,咱们着重会考虑的优化策略是在合成渲染树以前。

首先咱们能够优化 DOM,具体体如今如下几步:

  • 删除没必要要的代码和注释包括空格,尽可能作到最小化文件。
  • 能够利用 GZIP 压缩文件。
  • 结合 HTTP 缓存文件。

而后是优化 CSSOM,缩小、压缩以及缓存一样重要,对于 CSSOM 咱们前面重点提过了它会阻止页面呈现,所以咱们能够从这方面考虑去优化,让咱们看下面的代码:

body { font-size: 16px }
@media screen and (orientation: landscape) {
    .menu { float: right }
}
@media print {
    body { font-size: 12px }
}
复制代码

当浏览器遇到 CSS 时,会阻止呈现页面直到 CSSOM 解析完毕,可是对于一些特定场合才会运用的 CSS (好比上面两个媒体查询),浏览器会依旧请求,但不会阻塞渲染了,这也是为何咱们有时会将 CSS 文件拆分到不一样的文件,上面的样式表声明能够优化成这样:

<link href="style.css"    rel="stylesheet">
<link href="landscape.css" rel="stylesheet" media="orientation:landscape">
<link href="print.css"    rel="stylesheet" media="print">
复制代码

当咱们用 PageSpeed Insights 检测咱们的网站时,常常出现的一条就是 建议减小关键 CSS 元素数量

Google 官方文档 也建议: 当咱们声明样式表时,请密切关注媒体查询的类型,它们极大地影响了 CRP 的性能

接下来让咱们考虑 JavaScript 外部依赖能够优化的地方,再看下面的代码:

<p>
    Awesome page
    <script src="write.js"></script>
    is awesome
</p>
复制代码

当浏览器遇到 script 标记时,会阻止解析器继续操做,直到 CSSOM 构建完毕JavaScript 才会运行并继续完成 DOM 构建过程,对于 JavaScript 依赖的优化,咱们最经常使用的一种方法是当网页加载完成,浏览器发出 onload 事件后再去执行脚本(或者直接放在底部),但实际上还有更简单的策略:

  • async: 当咱们在 script 标记添加 async 属性之后,浏览器遇到这个 script 标记时会继续解析 DOM,同时脚本也不会被 CSSOM 阻止,即不会阻止 CRP。
  • defer: 与 async 的区别在于,脚本须要等到文档解析后( DOMContentLoaded 事件前)执行,而 async 容许脚本在文档解析时位于后台运行(二者下载的过程不会阻塞 DOM,但执行会)。
  • 当咱们的脚本不会修改 DOM 或 CSSOM 时,推荐使用 async

这里给出一个参考图:

render

浏览器还有一个特殊的流程,叫作预加载扫描器,它会提早扫描文档并发现关键的 CSS 和 JS 资源来下载,这个过程不会阻塞渲染,想详细了解它的原理能够浏览这篇文章 How the Browser Pre-loader Makes Pages Load Faster,实际的应用可浏览 前端性能优化之关键路径渲染优化

总结一下,为了首屏最快地渲染,咱们一般会采起下列步骤:

  • 分析并用 关键资源数 关键字节数 关键路径长度 来描述咱们的 CRP 。
  • 最小化关键资源数: 消除它们(内联)、推迟它们的下载(defer)或者使它们异步解析(async)等等 。
  • 优化关键字节数(缩小、压缩)来减小下载时间 。
  • 优化加载剩余关键资源的顺序: 让关键资源(CSS)尽早下载以减小 CRP 长度 。

更详细的优化建议能够阅读 PageSpeed Rules and Recommendations

参考

相关文章
相关标签/搜索