浏览器的渲染机制
一、一些概念:
1.DOM:文档对象模型(Document Object Model),浏览器将HTML解析成树形的数据结构,简称DOM
(1)DOM是什么
DOM就是一个编程接口,就是一套API。
DOM是针对HTML文档、XML等文档的一套API。就类似于JDBC是针对数据库的一套API一样。
(2)DOM的用途
DOM 是用来访问或操作HTML文档、XHTML文档、XML文档中的节点元素。
(3)DOM与其他技术的联系
JavaScript 可以通过 DOM 来访问和操作HTML文档所有的元素。
JavaScript是一种脚本语言,通常通过DOM来获得和操作HTML属性。
(4)DOM Tree是指通过DOM将HTML页面进行解析,并生成的HTML tree树状结构和对应访问方法
(5)Render Tree(渲染树):DOM 和 CSSOM 合并后生成 Render Tree
2.CSSOM:CSS Object Model,浏览器将CSS代码解析成树形的数据结构。
DOM 和 CSSOM 都是以 Bytes → characters → tokens → nodes → object model
二、浏览器的渲染:
1.浏览器会解析三个东西:
(1)HTML/SVG/XHTML:Webkit 有三个 C++ 的类对应这三类文档。解析这三种文件会产生一个 DOM Tree;
(2)CSS:解析 CSS 会产生 CSS 规则树;
(3)Javascript:脚本,通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree.
2.解析完成后,浏览器引擎会通过 DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree;
3.DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
4.Render Tree 和DOM一样,以多叉树的形式保存了每个节点的css属性、节点本身属性、以及节点的孩子节点。
5.CSS Rule 结点 Attach 到 DOM Tree 上,我们可以得到一个叫 Style Context Tree(样式上下文树),Firefox 基本上来说是通过 CSS 解析生成 CSS Rule Tree,然后,通过比对 DOM 生成 Style Context Tree,然后 Firefox 通过把 Style Context Tree 和其 Render Tree(Frame Tree)关联上,就完成了,(注:Webkit 不像 Firefox 要用两个树来干这个,Webkit 也有 Style 对象,它直接把这个 Style 对象存在了相应的 DOM 结点上了)
注意:
1.display:none 的节点不会被加入 Render Tree,而 visibility: hidden 则会,所以,如果某个节点最开始是不显示的,设为 display:none 是更优的;
2.建立 CSS Rule Tree 是需要比照着 DOM Tree 来的。CSS 匹配 DOM Tree 主要是从右到左解析 CSS 的 Selector,CSS 匹配 HTML 元素是一个相当复杂和有性能问题的事情。 DOM Tree 要小,CSS 尽量用 id 和 class,千万不要过渡层叠下去。
3.Render Tree 会把一些不可见的结点去除掉( display:none)
三、浏览器的渲染过程(webkit):
1.计算 CSS 样式:
Create/Update DOM And request css/image/js:浏览器请求到HTML代码后,在生成DOM的最开始阶段(应该是 Bytes → characters 后),并行发起css、图片、js的请求,无论他们是否在HEAD里。
注意:发起 js 文件的下载 request 并不需要 DOM 处理到那个 script 节点,比如:简单的正则匹配就能做到这一点,虽然实际上并不一定是通过正则:)。
2.构建 Render Tree:
(1)Create/Update Render CSSOM:CSS文件下载完成,开始构建CSSOM。
(2)Create/Update Render Tree:所有CSS文件下载完成,CSSOM构建结束后,和 DOM 一起生成 Render Tree。
3.Layout:计算出每个节点在屏幕中的位置。
有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。定位坐标和大小,是否换行,各种 position, overflow, z-index 属性 ……
4.正式开画(Painting):
Layout后,浏览器已经知道了哪些节点要显示(which nodes are visible)、每个节点的CSS属性是什么(their computed styles)、每个节点在屏幕中的位置是哪里(geometry)。就进入了最后一步:Painting,按照算出来的规则,通过显卡,把内容画到屏幕上。
注意:
1.以上四个步骤前2个步骤之所有使用 “Create/Update” 是因为DOM、CSSOM、Render Tree都可能在第一次Painting后又被更新多次,比如JS修改了DOM或者CSS属性。
2.两个重要概念:(重绘重排)
(1)Repaint——屏幕的一部分要重画,比如某个 CSS 的背景色变了。但是元素的几何尺寸没有变
Reflow——意味着元件的几何尺寸变了,我们需要重新验证并计算 Render Tree。是 Render Tree 的一部分或全部发生了变化。
(Reflow 的成本比 Repaint 的成本高得多的多)
Layout 和 Painting 也会被重复执行,除了DOM、CSSOM更新的原因外,图片下载完成后也需要调用Layout 和 Painting来更新网页。
DOM Tree 里的每个结点都会有 reflow 方法,一个结点的 reflow 很有可能导致子结点,甚至父点以及同级结点的 reflow
(2)下面这些动作有很大可能会是成本比较高的:
a.增加、删除、修改 DOM 结点时,会导致 Reflow 或 Repaint。
b.移动 DOM 的位置,或是搞个动画的时候。
c.修改 CSS 样式的时候。
d. Resize 窗口的时候(移动端没有这个问题),或是滚动的时候。
e.修改网页的默认字体时。
注:display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发现位置变化。
(3)reflow 有如下的几个原因:
a.Initial。网页初始化的时候。
b.Incremental。一些 Javascript 在操作 DOM Tree 时。
c.Resize。其些元件的尺寸变了。
d.StyleChange。如果 CSS 的属性发生变化了。
e.Dirty。几个 Incremental 的 reflow 发生在同一个 frame 的子树上。
(4)减少 reflow/repaint:
a.不要一条一条地修改 DOM 的样式。与其这样,还不如预先定义好 css 的 class,然后修改 DOM 的 className。
b.把 DOM 离线后修改。如:
*使用 documentFragment 对象在内存里操作 DOM。
*先把 DOM 给 display:none (有一次 repaint),然后你想怎么改就怎么改。比如修改 100 次,然后 再把他显示出来。
*clone 一个 DOM 结点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下
c.不要把 DOM 结点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。
d.尽可能的修改层级比较低的 DOM。当然,改变层级比较底的 DOM 有可能会造成大面积的 reflow,但是也可能影响范围很小。
e.为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
f.千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。
四、面试可能的问题:
script标签的位置会影响首屏时间么(为什么script要放在body底部)?
答案:不影响(如果这里里的首屏指的是页面从白板变成网页画面——也就是第一次Painting),但有可能截断首屏的内容,使其只显示上面一部分。
为什么说是“有可能”呢?,如果该js下载地比css还快,或者script标签不在第一屏的html里,实际上是不影响的。明白这一影响边界非常重要,这样我们在考察页面性能瓶颈的时候就有的放矢了。举个例子:在网页的第二屏有一个通用模块,实际上我们是可以把它的js逻辑独立成一个文件,将模块的html和js标签放在一起做成独立的模板引进来的。
问题的总结补充:
1.如果script标签的位置不在首屏范围内,不影响首屏时间(首屏时间和DomContentLoad事件没有必然的先后关系);
2.所有CSS尽早加载是减少首屏时间的最关键;
2.所有的script标签应该放在body底部是很有道理的;
3.script标签放在body底部,做与不做async(异步)或者defer(推迟)处理,都不会影响首屏时间,但影响DomContentLoad和load的时间,进而影响依赖他们的代码的执行的开始时间。
从性能最优的角度考虑,即使在body底部的script标签也会拖慢首屏出来的速度,因为浏览器在最一开始就会请求它对应的js文件,而这,占用了有限的TCP链接数、带宽甚至运行它所需要的CPU。这也是为什么script标签会有async或defer属性的原因之一。
可是,在复杂的实际应用场景中,要贯彻这几条结论可能会遇到问题,比如:
你的页面是分模块来写的,每一个模块都有自己的html、js甚至css,当把这些模块凑到一个页面中的时候就会出现js自然而然地出现在HTML中间部分。你很难把script标签都放到底部
即使你把script标签都放到底部,但script标签的存在终究是拖慢了首屏时间、DomContendLoad和loaded的时间。如果只有一个script标签,我们可以加一个async,但多个async的script标签的结果会是js文件被乱序执行的,这显然不是我们想要的。所以:
这时候,如果有个组件,帮助我根据优先级的不同,在特定的时间下载特定的资源,同时需要保证脚本的执行顺序,就能完美的解决这个问题,因此出现:
Tiny-Loader 组件:它与一般资源加载器不同的是,它可以保证资源下载以后的执行顺序,可以按需进行资源加载。
在前端性能优化过程中,发现许多js,css并不是页面一开始就需要的,而是在用户某个操作以后,才需要执行/渲染出来的。将那些js、css缓加载,可以大大减小页面的首屏时间,减少页面出load事件的负担.
使用方法:
Loader.async(['xxxx.css', 'yyyy.js'])
eg:
<html>
<head></head>
<body>
<div class="container">container</div>
<script>
Loader.sync(['xxxx.js', 'yyyy.js', 'zzzz.js'])
</script>
</body>
</html>
把各自模块需要的js文件放在对应的html里,然后通过Tiny-loader引用,就可以保证每个模块文件里有各自的html和js,模块间互不影响,js也会在页面最后才会下载,不会堵塞住页面的渲染.