详解浏览器渲染机制!!

1,604 阅读10分钟

前言

浏览器渲染机制也几乎是面试必考题,今天看了几篇文章,做下知识点总结!共分为以下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>会怎么样?

二、浏览器渲染的主要步骤

  1. 浏览器将获取的HTML文档解析成DOM树(在这步中,浏览器从开始解析开始就会启用另外一个线程来下载其他的css,js,静态资源等文件,但是如果遇到<script>(css文件)标签就会停下来,等待加载完成载继续解析,这也是为什么提倡把script标签放到页面底部、不适用import的方式导入css的文件原因。)
  2. CSS下载完成后对CSS文件进行解析,解析成CSS对象,然后对CSS对象进行组装,生成CSSOM树。
  3. 当DOM树和CSSOM树都构建完成后,浏览器根据DOM树和CSSOM树,构建一个渲染树(rendering tree)代表一系列即将被渲染的对象。
  4. 浏览器用一种流式处理的办法对渲染树上的每个节点,计算其在屏幕上的位置,这一步称之为布局layout.
  5. 遍历渲染树,将其绘制到屏幕上。这一步称之为绘制(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)

重绘是指一个元素的外观被改变了,背景颜色、文字颜色、边框颜色等。就会引起浏览器对某一部分的重画,但是并不会引起页面布局的改变。

另外有些情况下,比如修改了元素的样式,浏览器并不会立刻reflowrepaint一次,而是会把这样的操作积攒一批,然后做一次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属性为absolutefixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响。
  • 只在必要的时候,才将元素的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