前言
浏览器渲染机制也几乎是面试必考题,今天看了几篇文章,做下知识点总结!共分为以下4个主要的方面:
- !DCTYPE
- 浏览器渲染的主要步骤
- 回流(reflow)和重绘(repaint)的主要概念、触发条件、以及如何尽可能的避免发生。
一、!DCTYPE
概念
<!DOCTYPE>标签并不是HTML标签,而是来提醒浏览器使用哪个HTML版本来对页面进行解析。接下来对几个HTML协议简单介绍下
HTML 4.01 Strict(包含所有的HTML元素和属性,不包括展示性的元素、弃用的元素、框架集)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
HTML 4.01 Transitional(包含所有的HTML元素和属性,包括展示性的元素、弃用的元素、框架集)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
HTML 4.01 Frameset(该 DTD 等同于 HTML 4.01 Transitional,但允许框架集内容。)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"
"http://www.w3.org/TR/html4/frameset.dtd">
HTML 5
<!DOCTYPE html>
为什么HTML5中没有引用DTD呢,因为HTML4.01都是基于SGML语言的,DTD规定了标记语言的规则,所以需要,HTML5不是基于SGML的所以并不需要。
浏览器按照何种方式解析页面这个问题同解,这里提出另外一个问题,如果在html页面中没有写<!DOCTYPE>会怎么样?
二、浏览器渲染的主要步骤
- 浏览器将获取的HTML文档解析成DOM树(在这步中,浏览器从开始解析开始就会启用另外一个线程来下载其他的css,js,静态资源等文件,但是如果遇到<script>(css文件)标签就会停下来,等待加载完成载继续解析,这也是为什么提倡把script标签放到页面底部、不适用import的方式导入css的文件原因。)
- CSS下载完成后对CSS文件进行解析,解析成CSS对象,然后对CSS对象进行组装,生成CSSOM树。
- 当DOM树和CSSOM树都构建完成后,浏览器根据DOM树和CSSOM树,构建一个渲染树(
rendering tree
)代表一系列即将被渲染的对象。 - 浏览器用一种流式处理的办法对渲染树上的每个节点,计算其在屏幕上的位置,这一步称之为布局layout.
- 遍历渲染树,将其绘制到屏幕上。这一步称之为绘制(painting).
这张图很形象的说明了这个过程:
三、渲染的具体过程
1. 浏览器将获取的HTML文档解析成DOM树
当浏览器接收到服务器响应来的HTML文档后,会遍历文档节点,生成DOM树。
需要注意以下几点:
- DOM树在构建的过程中可能会被CSS和JS的加载而执行阻塞
-
display:none
的元素也会在DOM树中 - 注释也会在DOM树中
-
script
标签会在DOM树中
无论是DOM还是CSSOM,都是要经过Bytes→characters→tokens→nodes→object model
这个过程,如下图:
当浏览器接收到服务器响应来的HTML文档后,会遍历文档节点,生成DOM树。
需要注意以下几点:
- DOM树在构建的过程中可能会被CSS和JS的加载而执行阻塞
-
display:none
的元素也会在DOM树中 - 注释也会在DOM树中
-
script
标签会在DOM树中
无论是DOM还是CSSOM,都是要经过Bytes→characters→tokens→nodes→object model
这个过程。
DOM树的生成过程中可能会被CSS和JS的加载执行阻塞,具体可以参见下一章。当HTML文档解析过程完毕后,浏览器继续进行标记为deferred模式的脚本加载,然后就是整个解析过程的实际结束触发DOMContentLoaded事件,并在async文档文档执行完之后触发load事件。
2.CSSOM树
浏览器解析CSS文件并生成CSSOM,每个CSS文件都被分析成一个StyleSheet对象,每个对象都包含CSS规则(Style Rules),Style Rules也叫CSSOM(CSS Object Model)。CSS规则对象包含对应于CSS语法的选择器和声明对象以及其他对象。
在这个过程需要注意的是:
- CSS解析可以与DOM解析同时进行。
- CSS解析与
script
的执行互斥 。 - 在Webkit内核中进行了
script
执行优化,只有在JS访问CSS时才会发生互斥。
3. rendering tree
Render Tree的构建其实就是DOM Tree和CSSOM Attach的过程(每个DOM节点都有一个attach方法),浏览器会先从DOM树的根节点开始遍历每个可见节点,然后对每个可见节点找到适配的CSS样式规则并应用。
在渲染树这一阶段有几点必须要注意:
- Render Tree和DOM Tree并不完全一样。
- 因为Render Tree是将来要显示出来的内容,所以head等标签不会显示到其中。
- display:none 具有这个属性的元素不会显示在渲染树中。
- visibility:hidden 具有这个属性的元素会显示在渲染树中。
渲染树生成后,还是不能渲染到屏幕上,因为还无法确定各个节点的的位置,此时进入第四步布局layout.
4. 布局layout.
在我们创建完渲染树后,我们就可以遍历渲染树,因为渲染树上的每个节点都Render Object对象,包含这个节点的宽高、背景颜色等信息。我们就可以计算出每一个渲染对象的正确位置信息,将其渲染到正确的位置。这一过程也可称为回流或者布局,当我们加载js后还有可能发生一些对DOM的更改,这时候就会触发浏览器进行重新布局(回流)在后面我将会写到。布局阶段的输出就是我们常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。需要注意的是:
-
float
元素,absoulte
元素,fixed
元素会发生位置偏移。 - 我们常说的脱离文档流,其实就是脱离Render Tree。
5. 渲染树绘制(Painting the render tree)
在绘制阶段,浏览器会遍历渲染树,调用渲染器的paint()
方法在屏幕上显示其内容。渲染树的绘制工作是由浏览器的UI后端组件完成的。
四、阻塞渲染
当浏览器对HTML页面进行解析时,如果遇到了<script>,内部脚本则等到脚本执行结束再去继续解析,如果是外部脚本的话,要等到脚本下载完成,再解析页面。
如果js脚本还操作了CSS,而此时CSSOM树还没有构建完成的话则会停止js脚本的加载,直到CSSOM树构建完成。因为CSSOM树中包含了各个节点的样式信息,CSSOM树没有加载完成就没有办法进入下一阶段,在这之前用户看到的页面一直是空白的。这也是为什么要把CSS代码放在<head>标签中的原因。
为什么遇到script标签就是要停下来HTML文档的解析,因为js脚本中可能包含有对DOM或者CSSOM的操作,脚本解析会将脚本中改变DOM和CSS的地方分别解析出来,追加到DOM树和CSSOM规则树上。
五、回流(reflow)与重绘(repaint)
1. 回流(reflow)
回流通俗的来讲就是页面的布局发生了变化,浏览器需要重新的计算各个节点的位置,大小等信息了,浏览器会从root frame递归向下计算。这个回去重新计算的过程就是回流。回流是无法避免的,并且也不知道页面具体哪部分会受到影响,因为他们是相互依存,互相影响的关系。
有以下行为将会引起回流:
- 网页初始化时
- DOM操作(元素添加、删除、修改、元素顺序变化)
- 内容变化,包括表单域内文本改变
- CSS属性的计算或改变
- 添加或删除样式表
- 更改“类”属性
- 浏览器窗口的缩放、滚动等
- 伪类激活(例如:hover悬停)
- JS 获取 Layout 属性值(如:offsetLeft、scrollTop、getComputedStyle 等)也会引起回流。因为浏览器需要通过回流计算最新值
回流必定会引起重绘,但是重绘不一定会引起回流。
2. 重绘(repaint)
重绘是指一个元素的外观被改变了,背景颜色、文字颜色、边框颜色等。就会引起浏览器对某一部分的重画,但是并不会引起页面布局的改变。
reflow
或repaint
一次,而是会把这样的操作积攒一批,然后做一次reflow
,这又叫异步reflow
或增量异步reflow
。但是在有些情况下,比如resize
窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行reflow
。六. 如何避免回流与重绘
- 可以将需要多次修改的DOM元素设置
display:none
,操作完再显示。(因为隐藏元素不在render
树内,因此修改隐藏元素不会触发回流重绘) - 用
transform
做形变和位移可以减少reflow
- 操作完成后再显示 需要创建多个 DOM 节点时,使用 DocumentFragment 创建完后一次性的加入 document
- 缓存 Layout 属性值,如:var left = elem.offsetLeft; 这样,多次使用 left 只产生一次回流
- 尽量避免用 table 布局(table 元素一旦触发回流就会导致 table 里所有的其它元素回流)
- 通过绝对位移将复杂的节点元素脱离文档流,形成新的Render Layer,降低回流成本
- 避免使用 css 表达式(expression),因为每次调用都会重新计算值(包括加载页面)
- 尽量使用 css 属性简写,如:用 border 代替 border-width, border-style, border-color 批量修改元素样式:elem.className 和 elem.style.cssText 代替 elem.style.xxx
七. 一些浏览器优化的建议
- 合法地去书写HTML和CSS ,且不要忘了文档编码类型。
- 样式文件应当在
head
标签中,而脚本文件在body
结束前,这样可以防止阻塞的方式。 - 简化并优化CSS选择器,尽量将嵌套层减少到最小。
- DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。
- 如果某个样式是通过重排得到的,那么最好缓存结果。避免下一次用到的时候,浏览器又要重排。
- 不要一条条地改变样式,而要通过改变
class
,或者csstext
属性,一次性地改变样式。 - 尽量用
transform
来做形变和位移 - 尽量使用离线DOM,而不是真实的网页DOM,来改变元素样式。比如,操作
Document Fragment
对象,完成后再把这个对象加入DOM。再比如,使用cloneNode()
方法,在克隆的节点上进行操作,然后再用克隆的节点替换原始节点。 - 先将元素设为
display: none
(需要1次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,你就用两次重新渲染,取代了可能高达100次的重新渲染。 -
position
属性为absolute
或fixed
的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响。 - 只在必要的时候,才将元素的
display
属性为可见,因为不可见的元素不影响重排和重绘。另外,visibility : hidden
的元素只对重绘有影响,不影响重排。 - 使用
window.requestAnimationFrame()
、window.requestIdleCallback()
这两个方法调节重新渲染。
最后,本篇文章共借鉴了以下几篇文章,感谢!
- https://www.jianshu.com/p/e6252dc9be32
- https://blog.csdn.net/csdnnews/article/details/95267307
- https://blog.csdn.net/liujianfeng1214/article/details/86690284