序言

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎。
渲染引擎在不同的浏览器中也不是都相同的。
目前市情上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。
这里面大家最耳熟能详的可能便是 Webkit 内核了,Webkit 内核是当下浏览器天下真正的霸主。

本文我们就以 Webkit 为例,对当代浏览器的渲染过程进行一个深度的阐发。

html5划光广州蓝景技巧分享阅读器衬着道理流程 jQuery

页面加载过程

在先容浏览器渲染过程之前,我们简明扼要先容下页面的加载过程,有助于更好理解后续渲染过程。

要点如下:

向浏览器输入网址浏览器根据 DNS 做事器得到域名的 IP 地址向这个 IP 的机器发送 HTTP 要求做事器收到、处理并返回 HTTP 要求浏览器吸收到做事器返回的内容

例如在浏览器输入https://www.baidu.com,然后经由 DNS 解析,www.baidu.com对应的 IP 14.215.177.38(不同韶光、地点对应的 IP 可能会不同)。
然后浏览器向该 IP 发送 HTTP 要求。

做事端吸收到 HTTP 要求,然后经由打算(向不同的用户推送不同的内容),返回 HTTP 要求,返回的内容如下:

实在便是一堆 HMTL 格式的字符串,由于只有 HTML 格式浏览器才能精确解析,这是 W3C 标准的哀求。
接下来便是浏览器的渲染过程。

浏览器渲染过程

从上面这个图上,我们可以看到,浏览器渲染过程如下:

解析HTML,天生DOM树,解析CSS,天生CSSOM树

将DOM树和CSSOM树结合,天生渲染树(Render Tree)

Layout(回流):根据天生的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)

Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素

Display: 将像素发送给GPU,末了通过调用操作系统Native GUI的API绘制,展示在页面上。
(这一步实在还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。
而css3硬件加速的事理则是新建合成层,这里我们不展开,之后有机会再写一篇博客来先容)

渲染过程看起来也不繁芜,让我们来详细理解下每一步详细做了什么。

构建DOM详细流程

浏览器会遵守一套步骤将HTML文件转换为DOM树。
宏不雅观上,可以分为几个步骤:

浏览器从磁盘或网络读取HTML的原始字节(字节数据),并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。

在网络中传输的内容实在都是 0 和 1 这些字节数据。
当浏览器吸收到这些字节数据往后,它会将这些字节数据转换为字符串,也便是我们写的代码

将字符串转换成Token,例如:、等。
Token中会标识出当前Token是“开始标签”或是“结束标签”或着是“文本”等信息。

这时候你一定会有疑问,节点与节点之间的关系如何掩护?

事实上,这便是Token要标识“起始标签”和“结束标签”等标识的浸染。

例如“title”Token的起始标签和结束标签之间的节点肯定是属于“head”的子节点。

上图给出了节点之间的关系,例如:“Hello”Token位于“title”开始标签与“title”结束标签之间,表明“Hello”Token是“title”Token的子节点。
同理“title”Token是“head”Token的子节点。

天生节点工具并构建DOM

事实上,构建DOM的过程中,不是等所有Token都转换完成后再去天生节点工具,而是一边天生Token一边花费Token来天生节点工具。
换句话说,每个Token被天生后,会急速花费这个Token创建出节点工具。
把稳:带有结束标签标识的Token不会创建节点工具。

接下来我们举个例子,假设有段HTML文本:

<html><head> <title>Web page parsing</title></head><body> <div> <h1>Web page parsing</h1> <p>This is an example Web page.</p> </div></body></html>

上面这段HTML会解析成这样:

构建CSSOM详细流程

DOM会捕获页面的内容,但浏览器还须要知道页面如何展示,以是须要构建CSSOM。

构建CSSOM的过程与构建DOM的过程非常相似,当浏览器吸收到一段CSS,浏览器首先要做的是识别出Token,然后构建节点并天生CSSOM。

在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程实在是很花费资源的。
由于样式你可以自行设置给某个节点,也可以通过继续得到。
在这一过程中,浏览器得递归 CSSOM 树,然后确定详细的元素到底是什么样式。

把稳:CSS匹配HTML元素是一个相称繁芜和有性能问题的事情。
以是,DOM树要小,CSS只管即便用id和class,千万不要过渡层叠下去。

构建渲染树

当我们天生 DOM 树和 CSSOM 树往后,就须要将这两棵树组合为渲染树。

在这一过程中,不是大略地将两者合并就行了。
渲染树只会包括须要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

把稳:渲染树只包含可见的节点

我们或许有个迷惑:浏览器如果渲染过程中碰着JS文件怎么处理?

渲染过程中,如果碰着<script>就停滞渲染,实行 JS 代码。
由于浏览器有GUI渲染线程与JS引擎线程,为了防止渲染涌现不可预期的结果,这两个线程是互斥的关系。
JavaScript的加载、解析与实行会壅塞DOM的构建,也便是说,在构建DOM时,HTML解析器若碰着了JavaScript,那么它会停息构建DOM,将掌握权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方规复DOM构建。

也便是说,如果你想首屏渲染得越快,就越不应该在首屏就加载 JS 文件,这也是建议将 script 标签放在 body 标签底部的缘故原由。
当然在当下,并不是说 script 标签必须放在底部,由于你可以给 script 标签添加 defer(延迟) 或者 async(异步) 属性(下文会先容这两者的差异)。

JS文件不但是壅塞DOM的构建,它会导致CSSOM也壅塞DOM的构建。

原来DOM和CSSOM的构建是互不影响,井水不犯河水,但是一旦引入了JavaScript,CSSOM也开始壅塞DOM的构建,只有CSSOM构建完毕后,DOM再规复DOM构建。

这是什么情形呢?

这是由于JavaScript不但是可以改DOM,它还可以变动样式,也便是它可以变动CSSOM。
由于不完全的CSSOM是无法利用的,如果JavaScript想访问CSSOM并变动它,那么在实行JavaScript时,必须要能拿到完全的CSSOM。

以是就导致了一个征象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本实行和DOM构建,直至其完成CSSOM的下载和构建。
也便是说,在这种情形下,浏览器会先下载和构建CSSOM,然后再实行JavaScript,末了在连续构建DOM。

布局与绘制

当浏览器天生渲染树往后,就会根据渲染树来进行布局(也可以叫做回流)。
这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。
常日这一行为也被称为“自动重排”。

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对丈量值都将转换为屏幕上的绝对像素。

布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事宜,将渲染树转换成屏幕上的像素。

回流

前面我们通过布局渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还须要打算它们在设备视口(viewport)内的确切位置和大小,这个打算的阶段便是回流。

为了弄清每个工具在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历,我们可以以下面这个实例来表示:

<!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将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%。
而在回流这个阶段,我们就须要根据视口详细的宽度,将其转为实际的像素值。
(如下图)

重绘

终极,我们通过布局渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和详细的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

既然知道了浏览器的渲染过程后,我们就来磋商下,何时会发生回流重绘。

何时发生回流重绘

我们前面知道了,回流这一阶段紧张是打算节点的位置和几何信息,那么当页面布局和几何信息发生变革的时候,就须要回流。

比如以下情形发生回流:

根据改变的范围和程度,渲染树中或大或小的部分须要重新打算,有些改变会触发全体页面的重排,比如,滚动条涌现的时候或者修正了根节点。

页面一开始渲染的时候(这肯定避免不了)浏览器的窗口尺寸变革(由于回流是根据视口的大小来打算元素的位置和大小的)添加或删除可见的DOM元素元素的位置发生变革元素的尺寸发生变革(包括外边距、内边框、边框大小、高度和宽度等)内容发生变革,比如文本变革或图片被另一个不同尺寸的图片所替代。
元素字体大小变革激活CSS伪类(例如::hover)

一些常用且会导致回流的属性和方法:

clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()getComputedStyle()getBoundingClientRect()scrollTo()

以下情形发生重绘而不回流

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式授予给元素并重新绘制它,这个过程重绘而不回流。

把稳:回流一定会触发重绘,而重绘不一定会回流

性能影响

回流比重绘的代价要更高。

有时纵然仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。

浏览器优化机制

当代浏览器会对频繁的回流或重绘操作进行优化:

浏览器会掩护一个行列步队,把所有引起回流和重绘的操作放入行列步队中,如果行列步队中的任务数量或者韶光间隔达到一个阈值的,浏览器就会将行列步队清空,进行一次批处理,这样可以把多次回流和重绘变成一次。

当你访问以下属性或方法时,浏览器会急速清空行列步队:

clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftwidth、heightgetComputedStyle()getBoundingClientRect()

由于行列步队中可能会有影响到这些属性或方法返回值的操作,纵然你希望获取的信息与行列步队中操作引发的改变无关,浏览器也会强行清空行列步队,确保你拿到的值是最精确的。

以上属性和方法都须要返回最新的布局信息,因此浏览器不得不清空行列步队,触发回流重绘来返回精确的值。
因此,我们在修正样式的时候,最好避免利用上面列出的属性,他们都会刷新渲染行列步队。
如果要利用它们,最好将值缓存起来。

减少回流和重绘

利用 transform 替代 top利用 visibility 更换 display: none ,由于前者只会引起重绘,后者会引发回流(改变告终构)不要把节点的属性值放在一个循环里当成循环里的变量。
不要利用 table 布局,可能很小的一个小改动会造玉成部 table 的重新布局动画实现的速率的选择,动画速率越快,回流次数越多,也可以选择利用 requestAnimationFrameCSS 选择符从右往左匹配查找,避免节点层级过多将频繁重绘或者回流的节点设置为图层,图层能够阻挡该节点的渲染行为影响别的节点。
比如对付 video 标签来说,浏览器会自动将该节点变为图层。

最小化回流和重绘

由于回流和重绘可能代价比较昂贵,因此最好便是可以减少它的发生次数。
为了减少发生次数,我们可以合并多次对DOM和样式的修正,然后一次处理掉。
考虑这个例子

const el = document.getElementById('test');el.style.padding = '5px';el.style.borderLeft = '1px';el.style.borderRight = '2px';

例子中,有三个样式属性被修正了,每一个都会影响元素的几何构造,引起回流。
当然,大部分当代浏览器都对其做了优化,因此,只会触发一次重排。
但是如果在旧版的浏览器或者在上面代码实行的时候,有其他代码访问告终构信息(上文中的会触发回流的布局信息),那么就会导致三次重排。

因此,我们可以合并所有的改变然后依次处理,比如我们可以采纳以下的办法:

1.利用cssText

const el = document.getElementById('test');el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';2.利用class, 把css样式用个class包住,修正CSS的class.active{ border-left: 1px; border-right: 2px; padding: 5px; }

const el = document.getElementById('test');el.className += ' active';

批量修正DOM

当我们须要对DOM对一系列修正的时候,可以通过以下步骤减少回流重绘次数:

使元素分开文档流对其进行多次修正将元素带回到文档中。

该过程的第一步和第三步可能会引起回流,但是经由第一步之后,对DOM的所有修正都不会引起回流,由于它已经不在渲染树了。

有三种办法可以让DOM分开文档流:

隐蔽元素,运用修正,重新显示利用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
将原始元素拷贝到一个分开文档的节点中,修正节点后,再更换原始的元素。

下面来个例子演示下

我们要实行一段批量插入节点的代码:

function appendDataToElement(appendToElement, data) { let li; for (let i = 0; i < data.length; i++) { li = document.createElement('li'); li.textContent = 'text'; appendToElement.appendChild(li); }}const ul = document.getElementById('list');appendDataToElement(ul, data);

如果我们直接这样实行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。

我们可以利用这三种办法进行优化:

隐蔽元素,运用修正,重新显示

第一种方法:隐蔽元素,这个会在展示和隐蔽节点的时候,产生两次重绘

function appendDataToElement(appendToElement, data) { let li; for (let i = 0; i < data.length; i++) { li = document.createElement('li'); li.textContent = 'text'; appendToElement.appendChild(li); }}const ul = document.getElementById('list');ul.style.display = 'none';appendDataToElement(ul, data);ul.style.display = 'block';

第二种:利用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档

const ul = document.getElementById('list');const fragment = document.createDocumentFragment();appendDataToElement(fragment, data);ul.appendChild(fragment);

第三种:将原始元素拷贝到一个分开文档的节点中,修正节点后,再更换原始的元素。

const ul = document.getElementById('list');const clone = ul.cloneNode(true);appendDataToElement(clone, data);ul.parentNode.replaceChild(clone, ul);

避免触发同步布局事宜

上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器逼迫清空行列步队,进行逼迫同步布局。
举个例子,比如说我们想将一个p标签数组的宽度赋值为一个元素的宽度,我们可能写出这样的代码:

function initP() { for (let i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = box.offsetWidth + 'px'; }}

这段代码看上去是没有什么问题,可是实在会造成很大的性能问题。
在每次循环的时候,都读取了box的一个offsetWidth属性值,然后利用它来更新p标签的width属性。
这就导致了每一次循环的时候,浏览器都必须先使上一次循环中的样式更新操作生效,才能相应本次循环的样式读取操作。
每一次循环都会逼迫浏览器刷新行列步队。
我们可以优化为:

const width = box.offsetWidth;function initP() { for (let i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = width + 'px'; }}

对付繁芜动画效果,利用绝对定位让其分开文档流

对付繁芜动画效果,由于会常常的引起回流重绘,因此,我们可以利用绝对定位,让它分开文档流。
否则会引起父元素以及后续元素频繁的回流。
这个我们就直接上个例子。

打开这个例子后,我们可以打开掌握台,掌握台上会输出当前的帧数(虽然不准)。

从例子中,我们可以看到,帧数一贯都没到60。
这个时候,只要我们点击一下那个按钮,把这个元素设置为绝对定位,帧数就可以稳定60。

css3硬件加速(GPU加速)

比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。
这个时候,css3硬件加速就闪亮登场啦!

划重点:利用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。
但是对付动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

如何利用css3硬件加速(GPU加速)

常见的触发硬件加速的css3属性:

transformopacityfiltersWill-change

css3硬件加速的坑

如果你为太多元素利用css3硬件加速,会导致内存占用较大,会有性能问题。
在GPU渲染字体会导致抗锯齿无效。
这是由于GPU和CPU的算法不同。
因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

几点补充解释

1.async和defer的浸染是什么?有什么差异?

接下来我们比拟下 defer 和 async 属性的差异:

个中蓝色线代表JavaScript加载;赤色线代表JavaScript实行;绿色线代表 HTML 解析。

1)情形1<script src="script.js"></script>

没有 defer 或 async,浏览器会立即加载并实行指定的脚本,也便是说不等待后续载入的文档元素,读到就加载并实行。

2)情形2 <script defer src="script.js"></script>(延迟实行)

defer 属性表示延迟实行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停滞解析,这两个过程是并行的。
全体 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会实行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事宜。

3)情形3<script async src="script.js"></script> (异步下载)

async 属性表示异步实行引入的 JavaScript,与 defer 的差异在于,如果已经加载好,就会开始实行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。
须要把稳的是,这种办法加载的 JavaScript 依然会壅塞 load 事宜。
换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后实行,但一定在 load 触发之前实行。

defer 与比较普通 script,有两点差异:载入 JavaScript 文件时不壅塞 HTML 的解析,实行阶段被放到 HTML 标签解析完成之后。
在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。

2.为什么操作DOM慢

把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。
——《高性能 JavaScript》

JS是很快的,在JS中修正DOM工具也是很快的。
在JS的天下里,统统是大略的、迅速的。
但DOM操作并非JS一个人的独舞,而是两个模块之间的协作。

由于DOM是属于渲染引擎中的东西,而 JS 又是JS引擎中的东西。
当我们用JS去操作DOM时,实质上是JS引擎和渲染引擎之间进行了“跨界互换”。
这个“跨界互换”的实现并不大略,它依赖了桥接接口作为“桥梁”(如下图)。

过“桥”要收费——这个开销本身便是不可忽略的。
我们每操作一次DOM(不管是为了修正还是仅仅为了访问其值),都要过一次“桥”。
过“桥”的次数一多,就会产生比较明显的性能问题。
因此“减少 DOM 操作”的建议,并非空穴来风。

性能优化策略

基于上面先容的浏览器渲染事理,DOM 和 CSSOM 构造构建顺序,初始化可以对页面渲染做些优化,提升页面性能。

JS优化: <script> 标签加上 defer属性 和 async属性 用于在不壅塞页面文档解析的条件下,掌握脚本的下载和实行。
defer属性: 用于开启新的线程下载脚本文件,并使脚本在文档解析完成后实行。
async属性: HTML5新增属性,用于异步下载脚本文件,下载完毕立即阐明实行代码。
CSS优化:<link>标签的 rel属性 中的属性值设置为 preload 能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻须要的,最优的配置加载顺序,提高渲染性能

总结

综上所述,我们得出这样的结论:

浏览器事情流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
CSSOM会壅塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。
常日情形下DOM和CSSOM是并行构建的,但是当浏览器碰着一个不带defer或async属性的script标签时,DOM构建将停息,如果此时又适值浏览器尚未完成CSSOM的下载和构建,由于JavaScript可以修正CSSOM,以是须要等CSSOM构建完毕后再实行JS,末了才重新DOM构建。