做为Web工程师,咱们天天写HTML,CSS和JavaScript,可是浏览器是如何解析这些文件,最终将它们以像素显示在屏幕上的呢?javascript
这一过程叫作Critical Rendering Path。css
Critical Rendering Path,中文翻译过来,叫作关键渲染路径。指的是浏览器从请求HTML,CSS,JavaScript文件开始,到将它们最终以像素输出到屏幕上这一过程。包括如下几个部分:html
值得注意的是,上面的过程并非依次进行的,而是存在必定交叉,后面会详细解释。java
想要提升网页加载速度,提高用户体验,就须要在第一次加载时让重要的元素尽快显示在屏幕上。而不是等全部元素所有准备就绪再显示,下面一幅图说明了这两种方式的差别。git
DOM (Document Object Model),文档对象模型,构建DOM是必不可少的一环,浏览器从发出请求开始到获得HTML文件后,第一件事就是解析HTML成许多Tokens,再将Tokens转换成object,最后将object组合成一颗DOM树。github
这个过程是一个按部就班的过程,咱们假设HTML文件很大,一个*RTT (Round-Trip Time)*只能获得一部分,浏览器获得这部分以后就会开始构建DOM,并不会等到整个文档就位才开始渲染。这样作能够加快构建过程,并且因为自顶向下构建,所以后面构建的不会对前面的形成影响。web
后面咱们将会提到,CSSOM则必须等到全部字节收到才开始构建。浏览器
CSSOM (CSS Object Model),CSS对象模型,构建过程相似DOM,当HTML解析中遇到<link>
标签时,会请求对应的CSS文件,当CSS文件就位时,便开始解析它(若是遇到行内<style>
时则直接解析),这一解析过程能够和构建DOM同时进行。缓存
假设有以下CSS代码网络
body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }
复制代码
构建出来的CSSOM是这样的:
须要注意的是,上面并非一颗完整的CSSOM树,文档有一些默认的CSS样式,称做user agent styles,上面只展现了咱们覆盖的部分。
CSSOM的构建必需要得到一份完整的CSS文件,而不像DOM的构建是一个按部就班的过程。由于咱们知道,CSS文件中包含大量的样式,后面的样式会覆盖前面的样式,若是咱们提早就构建CSSOM,可能会获得错误的结果。
这也是关键的一步,浏览器使用DOM和CSSOM构建出Render Tree。此时不像构建DOM同样把全部节点构建出来,浏览器只构建须要在屏幕上显示的部分,所以像<head>
,<meta>
这些标签就无需构建了。同时,对于display: none
的元素,也无需构建。
display: none
告诉浏览器这个元素无需出如今Render Tree中,可是visibility: hidden
只是隐藏了这个元素,可是元素还占空间,会影响到后面的Layout,所以仍然须要出如今Render Tree中。
构建过程遵循如下步骤
相信大多数初学者都会认为CSS匹配是左向右的,其实偏偏相反。学习了CRP,也就不难理解为何了。
CSS匹配就发生在Render Tree构建时(Chrome Dev Tools里叫作Recalculate Style),此时浏览器构建出了DOM,并且拿到了CSS样式,此时要作的就是把样式跟DOM上的节点对应上,浏览器为了提升性能须要作的就是快速匹配。
首先要明确一点,浏览器此时是给一个"可见"节点找对应的规则,这和jQuery选择器不一样,后者是使用一个规则去找对应的节点,这样从左到右或许更快。可是对于前者,因为CSS的庞大,一个CSS文件中或许有上千条规则,并且对于当前节点来讲,大多数规则是匹配不上的,到此为止,稍微想一下就知道,若是从右开始匹配(也是从更精确的位置开始),能更快排除不合适的大部分节点,而若是从左开始,只有深刻了才会发现匹配失败,若是大部分规则层级都比较深,就比较浪费资源了。
除了上面这点,咱们前面还提到DOM构建是"按部就班的",并且DOM不阻塞Render Tree构建(只有CSSOM阻塞),这样也是为了能让页面更早有元素呈现。考虑以下状况,若是咱们此时构建的只是部分DOM,而此时CSSOM构建完成,浏览器此时须要构建Render Tree,若是对每个节点,找到一条规则进行从左向右匹配,则必需要求其子元素甚至孙子元素都在DOM上(而此时DOM未构建完成),显然会匹配失败。若是反过来,咱们只须要查找该元素的父元素或祖先元素(它们确定在当前DOM中)。
咱们如今为止已经获得了全部元素的自身信息,可是还不知道它们相对于Viewport的位置和大小,Layout这一过程须要计算的就是这两个信息。
根据这两个信息,Layout输出元素的Box Model,关于这个,我也写过一篇文章Understand CSS Formatting Model。
目前为止,如今咱们已经拿到了元素相对于Viewport的详细信息,全部的值都已经计算为相对Viewport的精确像素大小和位置,就差显示了。
浏览器将每个节点以像素显示在屏幕上,最终咱们看到页面。
这一过程须要的时间与文档大小,CSS应用样式的多少以及复杂度,还有设备自身都有关,例如对简单的颜色进行Paint是简单的,可是box-shadow
进行paint则是复杂的
前面的过程都没有提到JavaScript,但在现在,JavaScript倒是网页中不可缺的一部分。这里对它如何影响CRP作一个概要,具体细节我后面使用Chrome Dev Tools进行了测验
<style>
和<link>
的状况,若是放在尾部,浏览器刚开始会使用User Agent Style构建CSSOM)为了模拟真实网络状况,我把Demo部署到了个人githubpage,你也能够在仓库找到源代码。
同时,不要混淆DOM, CSSOM, Render Tree这三个概念,我刚开始就容易混淆DOM和Render Tree,这两个是不一样的
下面的Chrome截图部分,若是不清晰,请直接点击图片查看原图
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../css/main.css" />
<title>Critical Rendering Path with separate script file</title>
</head>
<body>
<p>What's up? <span>Bros. </span>My name is tianzhich</p>
<div><img src="../images/xiaoshuang.jpg" alt="小爽-流星雨" height="500"></div>
<script src="../js/main.js"></script>
</body>
</html>
复制代码
var span = document.getElementsByTagName('span')[0];
span.textContent = 'Girls. '; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
复制代码
/* // [START full] */
body {
font-size: 16px
}
p {
font-weight: bold
}
span {
color: red
}
p span {
display: none
}
img {
float: right
}
/* // [END full] */
复制代码
首先来看没有加载JS的状况
上图中,浏览器收到HTML文件后,便开始解析构建DOM。
须要注意,上图接收的只是图片的一部分
接下来咱们详细看看这三个部分:
能够看出,浏览器解析到<link>
,<img>
等等标签时,会立刻发出HTTP请求,并且解析也将继续进行,解析完成后会触发readystatechange事件和DOMContentLoaded事件,在上图中,因为时间间隔已经到了100微秒级别,事件间隔有些许差别,但不影响咱们对这一过程的理解
细心的话可能会注意到上图中还触发了Recalculate Style (紫色部分),这一过程发生在CSSOM树构建完成或者发生变化须要更新Render Tree时,可是此时咱们并无拿到CSS,更没有构建出CSSOM,这一部分从何而来呢?我在下面第4部分作了分析
下面这一过程依次展现了CSS解析构建CSSOM,Render Tree生成,layout和paint,最终页面首次出现画面
下图中有一点错误:render tree构建应该发生在Recalculate Style (layout前一部分),Layout以及后一部分Update Layer Tree做为Layout
从这里咱们能够看出,DOM即便构建完成,也须要等CSSOM构建完成,才能通过一个完整的CRP并呈现画面,所以为了画面尽快呈现,咱们须要尽早构建出CSSOM,好比
<style>
或者<link>
标签应该放在<head>
里并尽早发现被解析(第4部分我会分析将这两个标签放在html文档后面形成的影响)<style>
标签发在<head>
里,无需网络请求上图说明,浏览器接收到部分图片字节后,便开始渲染了,而不是等整张图片接收完成才开始渲染,至于渲染次数,本例中的图片大小为90KB左右,传输了6次,渲染了2次。我以为这应该和网络拥塞程度以及图片大小等因素有关。
还有一点须要注意,两次渲染中,只有首次渲染引起了Layout和以后的Update Layer Tree,而第二次渲染只有Update Layer Tree,我猜测对于图片来讲,浏览器第一次渲染便知道了其大小,因此从新进行Layout并留出足够空间,以后的渲染只须要在该空间上进行paint便可。整张图片加载完毕以后,触发Load事件
上图包括以后图片中的Chrome扩展脚本能够忽视,虽然使用了隐私模式作测验(避免缓存和一些扩展脚本的影响),但我发现仍是有一个脚本没法去除,虽然这不影响测验结果
接下来咱们考虑JavaScript脚本对CRP的影响
上图来看,Parse HTML这一过程被JavaScript执行打断,并且JavaScript会等待CSSOM构建完成以后再执行,执行完成以后,DOM继续构建
前面的例子中,咱们看到DOM几乎都是在CSSOM构建完成前就构建完成了,而引入JS后,DOM构建被JS执行打断,而JS执行又必须等CSSOM构建完毕,这无疑延长了第一次CRP时间,让页面首次出现画面的时间更长
若是使用外部script脚本,这一时间会更长
对于网络请求的资源,浏览器会为其分配优先级,优先级越高的资源响应速度更快,时间更短,在这个例子中,CSS的优先级最高,其次是JS,优先级最低的是图片
咱们主要来看第一部分,后面部分和第1个研究相似
能够看到,增长了对JS文件的网络请求时间,一轮CRP时间更长了,对比上面的行内Script可能时间差别没有那么明显,是由于这个例子中的JS文件体积小,传输时间只比CSS多一点,主要决定JS什么时候执行的仍是CSS,若是JS稍大,因为请求优先级低于CSS,则差别会明显变大
若是Script会对页面首次渲染形成这么大的影响,有没有什么好的办法解决它呢?
答案是确定的,就是使用异步脚本<script src="" async />
使用异步脚本,其实就是告诉浏览器几件事
须要注意以下几点
async
关键字变成异步,并且注意其与延迟脚本 (<script defer>
)的区别,后者是在Document被解析完毕而DOMContentLoaded事件触发以前执行,前者则是在下载完毕后执行document.createElement
建立的<script>
,默认就是异步脚本直接看图
因为Script执行修改了DOM和CSSOM,所以从新通过Recalculate Style生成Render Tree,从新计算Layout,从新Paint,最终呈现页面。因为这一过程仍然很快(只用了140ms左右),所以咱们仍是察觉不到这个变化
前面留下了一个问题,CSSOM没有构建完成,为何刚开始的Parse HTML同时就有Recalculate Style这部分?或许这部分会给你一个答案
这里为了不JS带来的影响,使对比更有针对性,删除了JavaScript
先来回顾一下在头部设置<link>
前面的DOM构建部分出现了Recalculate Style,以后得到CSS并解析后还有一次,一共出现了2次
再来看看改为<style>
,Recalculate Style一共出现1次
<style>
在头部,一开始就直接解析完成,没有网络请求
先来看看设置<style>
在尾部,Recalculate Style出现了1次
再看设置<link>
在尾部,Recalculate Style一共出现3次
先总结实验结果
实验中将<link>
放在头部,<style>
放在头部,<link>
放在尾部,<style>
放在尾部,Recalculate Style的次数分别是2,1,3,1
而后咱们须要了解Chrome Dev Tools Performance Tab的几个关键过程
To find out how long the CSS processing takes you can record a timeline in DevTools and look for "Recalculate Style" event: unlike DOM parsing, the timeline doesn’t show a separate "Parse CSS" entry, and instead captures parsing and CSSOM tree construction, plus the recursive calculation of computed styles under this one event.
在Performance Tab里面,没有看到Render Tree构建这一过程,这一过程也被浏览器隐藏在Recalculate Style里面,因此Recalculate Style既可能包括CSSOM的构建,也可能包括Render Tree的构建
对于<style>
里的CSS,解析过程发生在Recalculate Style中,而<link>
得到的CSS,解析过程是单独的,叫作Parse CSS (和Parse HTML相似)
同时,要明确浏览器还有一个默认的User Agent Style,咱们的Style只是对其进行一个覆盖
最后猜测这4个结果的缘由以下
<head>
里存在<link>
,则会等待CSS网络请求完成并解析好以后才开始Render Tree,至于第一次的Recalculate Style,我猜测是默认的User Agent Style,此时CSSOM已经开始构建了,而接收到CSS文件,咱们设置的Style会对默认的Style进行覆盖。这里第一次Recalculate Style只包含CSSOM构建,第二次则包含了CSSOM更新以及Render Tree构建<style>
放在头部,浏览器由于能够立刻拿到CSS,就能够立刻进行解析,此时User Agent Style的解析和咱们自定义的Style解析合并,Recalculate Style包含了CSSOM构建和Render Tree构建<style>
放在尾部,和放在头部相似。只不过晚点发现CSS,可是因为是行内<style>
,仍是能够立刻解析<link>
放在尾部,浏览器一开始没发现<link>
,会使用User Agent Style(2次 Recalculate Style),后面才发CSS网络请求,最后再触发CSSOM的更新(1次 Recalculate Style),这是最糟糕的状况。这里的3次Recalculate Style分别指CSSOM构建,Render Tree构建,CSSOM更新和Render Tree构建。Render Tree构建两次,页面发生两次渲染,为最糟糕的状况因此,咱们须要将<style>
和<link>
放在头部,对于<style>
在尾部,这个例子省略了JS的影响,若是加入JS,则结果又会不同
原本想再测试一下JS在HTML中不一样位置的影响,可是就CRP这一过程来说,这部分比较容易叙述清楚
由于JS无论在哪一个位置都会默认阻塞DOM。若是DOM还没有构建完成,JS中对不在DOM的元素进行操做,会抛出错误,脚本直接结束。若是将脚本设置为async
,则放在前面也是OK的,例如使用document.createElement
建立的<script>
,其默认使用的就是异步
这篇文章是我阅读了Google Developer的Web Performance Fundamentals后,本身作实践获得的总结。很是建议每位FEDers阅读这一系列文章。文章做者Ilya Grigorik还和Udacity开设了联合课程Website Performance Optimization以及他关于Web Performance的一次演讲,都值得一看。
因为水平有限,我只看了前半部分(关于CRP),后半部分则关于在Web Performance Optimization的实践。
疏漏之处,欢迎指正。