为了把HTML、CSS和JavaScript转化成活灵活现、绚丽多彩的网页,浏览器需要处理一系列的中间过程,优化性能其实就是了解这个过程中发生了什么-即CRP(Critical Rendering Path,关键渲染路径)。首先,我们从头开始快速学习一下浏览器是如何显示一个简单网页的。
浏览器渲染一个网页的过程
构建对象模型
文档对象模型(DOM)
Critical Path
Hello web performance students!
一个普通的页面,里面包含一些文本和一张图片,浏览器是如何处理这个页面的呢?
转换:浏览器从磁盘或网络读取HTML的原始字节,然后根据指定的文件编码格式(例如 UTF-8)将其转换为相应字符
令牌化:浏览器把字符转化成W3C HTML5 标准指定的各种确切的令牌,比如""、"
"以及其他在尖括号内的字符串。每个令牌都有特殊的含义以及它自己的一套规则词法分析:生成的令牌转化为对象,这个对象定义了它们的属性及规则
DOM构建:最后,由于HTML标记定义了不同标签之间的关系(某些标签嵌套在其他标签中),创建的对象在树状的数据结构中互相链接,树状数据结构也捕获了原始标签定义的父子关系:HTML对象是body对象的父对象,body是p对象的父对象等等
上述整个流程的最终输出是文档对象模型,即这个简单网页的 "DOM",浏览器完成页面的所有后续处理都是建立在这个DOM基础上的。
打开Chrome DevTools > Timeline,录制时间轴,上述过程对应Loading事件中的Parse HTML事件,可以查看到执行这一过程所需要的时间。
DOM树捕获了文档标记的属性及关系,但它没有告诉我们元素在渲染时是什么样子的。这是CSSOM要干的活,也就是接下来要讲的。
CSS对象模型(CSSOM)
当浏览器构建上述网页DOM的时候,在head里面碰到一个link标签,这个标签引用了一个外部的CSS样式表:style.css。浏览器预测会需要这个资源来渲染页面,因此会立即发出一个该资源的请求,该请求返回以下内容:
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
与HTML一样,我们需要将收到的 CSS 规则转换为浏览器可以理解、能够处理的东西。因此,我们重复与处理 HTML 非常相似的过程:
想要查看CSS处理过程所花费的时间,可以在录制的时间轴中查看Rendering事件中的Recalculate Style事件:与DOM解析不同,timeline不显示单独的“Parse CSS”条目,而是在Recalculate Style事件下一同捕获CSS解析、CSSOM构建以及computed styles的递归计算。
构建渲染树、布局及绘制
前面介绍了我们根据输入的HTML及CSS构建了DOM树和CSSOM树,但二者是独立的对象:DOM描述的是文档内容,CSSOM描述的是应用于文档的样式规则。浏览器会把DOM和CSSOM组合起来构建一个渲染树(Render-tree),渲染树会捕获页面上所有可见的DOM内容以及应用在每个节点上的CSSOM样式。
构建渲染树的过程大致如下:
-
从DOM树的根节点开始,遍历每个可见的节点
某些节点不可见(例如 script 标签、meta 标签等),因为它们不会体现在渲染结果中,所以会被忽略
某些通过 CSS 隐藏的节点在渲染树中也会被忽略,比如应用了 display:none 规则的节点
为每一个可见的节点匹配并应用对应的CSSOM规则
生成有内容和计算样式的可见节点
小提示:注意
visibility: hidden
和display: none
二者的区别。visibility: hidden
只是让元素在视觉上不可见,但是元素在页面布局中仍然占据空间。而display: none
则是从渲染树中删除某一个元素,不仅视觉上不可见,渲染树上也没有,更不会影响到页面的布局。
最终输出的就是一个包含了所有可见节点的内容及样式信息的渲染树。
到目前为止,我们已经计算出了哪些节点是可见的以及它们的计算样式,但是我们还没有计算它们在设备视口(viewport)中的准确位置及尺寸大小——这就是布局(Layout)阶段要做的工作,也就是常说的重排(reflow)。
为了计算出页面中每个对象的准确大小和位置,浏览器从渲染树的根节点开始遍历,计算页面上每个对象的几何信息。举例如下:
Critial Path: Hello world!
Hello world!
上面页面的 body 包含两个嵌套 div:第一个 div(父元素)将节点尺寸大小设置为视口宽度的 50%,第二个 div 的宽度为父元素的 50%,即视口宽度的 25%!
布局过程的输出是一个“盒子模型”,它精确地捕获了每个元素在视口中的准确位置及尺寸大小:所有相对度量单位都被转换为屏幕上的绝对像素。
自此,我们已经知道了哪些节点是可见的以及它们的计算样式和几何信息,然后我们就可以把这些信息传送到最后一个阶段,即把渲染树中的每一个节点都转化到屏幕上实际的像素点。这个步骤通常被称为绘制(painting)或者栅格化(rasterizing)。
构建渲染树、布局与绘制所消耗的时间也可以通过timeline来查看:
"Layout" 事件捕获渲染树的构建及位置、尺寸的计算
布局完成时,浏览器会触发 'Paint' 事件:将渲染树转化为屏幕上的实际像素
现在回顾一下浏览器执行的几个步骤:
处理 HTML 标记,构建 DOM 树
处理 CSS 标记,构建 CSSOM 树
将 DOM 树和 CSSOM 树融合成渲染树
根据渲染树进行布局,计算每个节点的几何信息
在屏幕上绘制各个节点
优化关键渲染路径即尽可能地缩短上述第 1 步到第 5 步耗费的总时间。
优化CRP
阻塞渲染的CSS
在构建渲染树部分我们已了解到:CRP要求DOM和CSSOM两者融合在一起才能构建渲染树。这就导致了一个性能问题:HTML和CSS都是阻塞渲染的资源。HTML很显然,没有DOM就没有内容去渲染。CSS没有那么明显,但确实是阻塞渲染的资源。我们知道一个正常的网页如果没有引入专用的css,页面有多丑陋。当我们的网页引入了专用的css,页面一加载出来的时候就是绚丽多彩的,如果css不阻塞渲染,我们看到的很可能是这样的一个画面:页面刚加载出来的时候其丑无比,过了一会,页面又变漂亮了……
既然CSS是阻塞渲染的资源,这就意味着在CSSOM构建完成之前,浏览器不会去渲染任何已处理的内容。要尽早、尽快地把CSS下载到客户端以优化首次渲染的时间。
使用CSS“媒体类型”和“媒体查询”优化阻塞渲染的CSS:
第一条声明阻塞渲染,匹配所有情况
第二条声明只适用于打印(媒体类型),因此,页面在浏览器中首次加载时,不会阻塞渲染
第三条声明提供了媒体查询,由浏览器判断:如果条件符合,则在该样式表下载并处理完以前,浏览器会阻塞渲染
小提示:「阻塞渲染」仅是指该资源是否会阻塞浏览器的首次页面渲染。无论 CSS 是否阻塞渲染,CSS 资源都会被下载,只是说非阻塞性资源的优先级比较低而已。
阻塞解析的JavaScript
JS可以修改页面的内容、样式以及响应用户的交互,JS在DOM、CSSOM和JS执行之间引入了很多新的依赖关系,导致浏览器在处理和渲染页面上出现大幅延迟: