深刻理解浏览器解析渲染HTML

前言

做为Web工程师,咱们天天写HTML,CSS和JavaScript,可是浏览器是如何解析这些文件,最终将它们以像素显示在屏幕上的呢?javascript

这一过程叫作Critical Rendering Pathcss

Critical Rendering Path

Critical Rendering Path,中文翻译过来,叫作关键渲染路径。指的是浏览器从请求HTML,CSS,JavaScript文件开始,到将它们最终以像素输出到屏幕上这一过程。包括如下几个部分:html

  1. 构建DOM
    • 将HTML解析成许多Tokens
    • 将Tokens解析成object
    • 将object组合成为一个DOM树
  2. 构建CSSOM
    • 解析CSS文件,并构建出一个CSSOM树(过程相似于DOM构建)
  3. 构建Render Tree
    • 结合DOM和CSSOM构建出一颗Render树
  4. Layout
    • 计算出元素相对于viewport的相对位置
  5. Paint
    • 将render tree转换成像素,显示在屏幕上

值得注意的是,上面的过程并非依次进行的,而是存在必定交叉,后面会详细解释。java

想要提升网页加载速度,提高用户体验,就须要在第一次加载时让重要的元素尽快显示在屏幕上。而不是等全部元素所有准备就绪再显示,下面一幅图说明了这两种方式的差别。git

构建DOM

DOM (Document Object Model),文档对象模型,构建DOM是必不可少的一环,浏览器从发出请求开始到获得HTML文件后,第一件事就是解析HTML成许多Tokens,再将Tokens转换成object,最后将object组合成一颗DOM树。github

这个过程是一个按部就班的过程,咱们假设HTML文件很大,一个*RTT (Round-Trip Time)*只能获得一部分,浏览器获得这部分以后就会开始构建DOM,并不会等到整个文档就位才开始渲染。这样作能够加快构建过程,并且因为自顶向下构建,所以后面构建的不会对前面的形成影响。web

后面咱们将会提到,CSSOM则必须等到全部字节收到才开始构建。浏览器

构建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,可能会获得错误的结果。

构建Render Tree

这也是关键的一步,浏览器使用DOM和CSSOM构建出Render Tree。此时不像构建DOM同样把全部节点构建出来,浏览器只构建须要在屏幕上显示的部分,所以像<head>,<meta>这些标签就无需构建了。同时,对于display: none的元素,也无需构建。

display: none告诉浏览器这个元素无需出如今Render Tree中,可是visibility: hidden只是隐藏了这个元素,可是元素还占空间,会影响到后面的Layout,所以仍然须要出如今Render Tree中。

构建过程遵循如下步骤

  1. 浏览器从DOM树开始,遍历每个“可见”节点。
  2. 对于每个"可见"节点,在CSSOM上找到匹配的样式并应用。
  3. 生成Render Tree。

扩展:CSS匹配规则为什么从右向左

相信大多数初学者都会认为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中)。

Layout

咱们如今为止已经获得了全部元素的自身信息,可是还不知道它们相对于Viewport的位置和大小,Layout这一过程须要计算的就是这两个信息。

根据这两个信息,Layout输出元素的Box Model,关于这个,我也写过一篇文章Understand CSS Formatting Model

目前为止,如今咱们已经拿到了元素相对于Viewport的详细信息,全部的值都已经计算为相对Viewport的精确像素大小和位置,就差显示了。

Paint

浏览器将每个节点以像素显示在屏幕上,最终咱们看到页面。

这一过程须要的时间与文档大小,CSS应用样式的多少以及复杂度,还有设备自身都有关,例如对简单的颜色进行Paint是简单的,可是box-shadow进行paint则是复杂的

引入JavaScript

前面的过程都没有提到JavaScript,但在现在,JavaScript倒是网页中不可缺的一部分。这里对它如何影响CRP作一个概要,具体细节我后面使用Chrome Dev Tools进行了测验

  1. 解析HTML构建DOM时,遇到JavaScript会被阻塞
  2. JavaScript执行会被CSSOM构建阻塞,也就是说,JavaScript必须等到CSS解析完成后才会执行(这只针对在头部放置<style><link>的状况,若是放在尾部,浏览器刚开始会使用User Agent Style构建CSSOM)
  3. 若是使用异步脚本,脚本的网络请求优先级下降,且网络请求期间不阻塞DOM构建,直到请求完成才开始执行脚本

使用Chrome Dev Tools检测CRP

为了模拟真实网络状况,我把Demo部署到了个人githubpage,你也能够在仓库找到源代码

同时,不要混淆DOM, CSSOM, Render Tree这三个概念,我刚开始就容易混淆DOM和Render Tree,这两个是不一样的

下面的Chrome截图部分,若是不清晰,请直接点击图片查看原图

0. 代码部分

HTML

<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>
复制代码

JavaScript

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);
复制代码

CSS

/* // [START full] */
body {
  font-size: 16px
}

p {
  font-weight: bold
}

span {
  color: red
}

p span {
  display: none
}

img {
  float: right
}
/* // [END full] */
复制代码

1. 不加载JS状况

首先来看没有加载JS的状况

上图中,浏览器收到HTML文件后,便开始解析构建DOM。

须要注意,上图接收的只是图片的一部分

接下来咱们详细看看这三个部分:

DOM构建(体现为parse html)

能够看出,浏览器解析到<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,好比

  1. html文档中的<style>或者<link>标签应该放在<head>里并尽早发现被解析(第4部分我会分析将这两个标签放在html文档后面形成的影响)
  2. 减小第一次请求的CSS文件大小
  3. 甚至能够将最重要部分的CSS Rule以<style>标签发在<head>里,无需网络请求

页面首次出现图片

上图说明,浏览器接收到部分图片字节后,便开始渲染了,而不是等整张图片接收完成才开始渲染,至于渲染次数,本例中的图片大小为90KB左右,传输了6次,渲染了2次。我以为这应该和网络拥塞程度以及图片大小等因素有关。

还有一点须要注意,两次渲染中,只有首次渲染引起了Layout和以后的Update Layer Tree,而第二次渲染只有Update Layer Tree,我猜测对于图片来讲,浏览器第一次渲染便知道了其大小,因此从新进行Layout并留出足够空间,以后的渲染只须要在该空间上进行paint便可。整张图片加载完毕以后,触发Load事件

上图包括以后图片中的Chrome扩展脚本能够忽视,虽然使用了隐私模式作测验(避免缓存和一些扩展脚本的影响),但我发现仍是有一个脚本没法去除,虽然这不影响测验结果

接下来咱们考虑JavaScript脚本对CRP的影响

2. 引入JS

行内Script (Script位于html尾部)

上图来看,Parse HTML这一过程被JavaScript执行打断,并且JavaScript会等待CSSOM构建完成以后再执行,执行完成以后,DOM继续构建

前面的例子中,咱们看到DOM几乎都是在CSSOM构建完成前就构建完成了,而引入JS后,DOM构建被JS执行打断,而JS执行又必须等CSSOM构建完毕,这无疑延长了第一次CRP时间,让页面首次出现画面的时间更长

若是使用外部script脚本,这一时间会更长

外部Script (Script位于html尾部)

对于网络请求的资源,浏览器会为其分配优先级,优先级越高的资源响应速度更快,时间更短,在这个例子中,CSS的优先级最高,其次是JS,优先级最低的是图片

咱们主要来看第一部分,后面部分和第1个研究相似

能够看到,增长了对JS文件的网络请求时间,一轮CRP时间更长了,对比上面的行内Script可能时间差别没有那么明显,是由于这个例子中的JS文件体积小,传输时间只比CSS多一点,主要决定JS什么时候执行的仍是CSS,若是JS稍大,因为请求优先级低于CSS,则差别会明显变大

3. Async Script

若是Script会对页面首次渲染形成这么大的影响,有没有什么好的办法解决它呢?

答案是确定的,就是使用异步脚本<script src="" async />

使用异步脚本,其实就是告诉浏览器几件事

  1. 无需阻塞DOM,在对Script执行网络请求期间能够继续构建DOM,直到拿到Script以后再去执行
  2. 将该Script的网络请求优先级下降,延长响应时间

须要注意以下几点

  1. 异步脚本是网络请求期间不阻塞DOM,拿到脚本以后立刻执行,执行时仍是会阻塞DOM,可是因为响应时间被延长,此时每每DOM已经构建完毕(下面的测验图片将会看到,CSSOM也已经构建完毕并且页面很快就发生第一次渲染),异步脚本的执行发生在第一次渲染以后
  2. 只有外部脚本可使用async关键字变成异步,并且注意其与延迟脚本 (<script defer>)的区别,后者是在Document被解析完毕而DOMContentLoaded事件触发以前执行,前者则是在下载完毕后执行
  3. 对于使用document.createElement建立的<script>,默认就是异步脚本

直接看图

因为Script执行修改了DOM和CSSOM,所以从新通过Recalculate Style生成Render Tree,从新计算Layout,从新Paint,最终呈现页面。因为这一过程仍然很快(只用了140ms左右),所以咱们仍是察觉不到这个变化

4. CSS在HTML中不一样位置的影响

前面留下了一个问题,CSSOM没有构建完成,为何刚开始的Parse HTML同时就有Recalculate Style这部分?或许这部分会给你一个答案

这里为了不JS带来的影响,使对比更有针对性,删除了JavaScript

设置style在html文件头部

先来回顾一下在头部设置<link>

link tag on top of html file

前面的DOM构建部分出现了Recalculate Style,以后得到CSS并解析后还有一次,一共出现了2次

再来看看改为<style>,Recalculate Style一共出现1次

<style>在头部,一开始就直接解析完成,没有网络请求

设置style或者link在尾部

先来看看设置<style>在尾部,Recalculate Style出现了1次

再看设置<link>在尾部,Recalculate Style一共出现3次

先总结实验结果

实验中将<link>放在头部,<style>放在头部,<link>放在尾部,<style>放在尾部,Recalculate Style的次数分别是2,1,3,1

而后咱们须要了解Chrome Dev Tools Performance Tab的几个关键过程

  1. Performance Tab里的Recalculate Style,官方是这样解释的

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的构建

  1. 对于<style>里的CSS,解析过程发生在Recalculate Style中,而<link>得到的CSS,解析过程是单独的,叫作Parse CSS (和Parse HTML相似)

  2. 同时,要明确浏览器还有一个默认的User Agent Style,咱们的Style只是对其进行一个覆盖

最后猜测这4个结果的缘由以下

  1. 浏览器若是发现<head>里存在<link>,则会等待CSS网络请求完成并解析好以后才开始Render Tree,至于第一次的Recalculate Style,我猜测是默认的User Agent Style,此时CSSOM已经开始构建了,而接收到CSS文件,咱们设置的Style会对默认的Style进行覆盖。这里第一次Recalculate Style只包含CSSOM构建,第二次则包含了CSSOM更新以及Render Tree构建
  2. <style>放在头部,浏览器由于能够立刻拿到CSS,就能够立刻进行解析,此时User Agent Style的解析和咱们自定义的Style解析合并,Recalculate Style包含了CSSOM构建和Render Tree构建
  3. <style>放在尾部,和放在头部相似。只不过晚点发现CSS,可是因为是行内<style>,仍是能够立刻解析
  4. <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的实践。

疏漏之处,欢迎指正。

参考

  1. developers.google.com/web/fundame… (Recommended)
  2. Website Performance Optimization - Udacity
  3. stackoverflow.com/questions/5…
  4. www.youtube.com/watch?v=PkO…
相关文章
相关标签/搜索