Dom 操作成本 浏览器的重排和重绘

操作 Dom 的成本很高 不要轻易去操作 Dom 这句话从开始入门就听说,那么这里说的成本是指什么?
由此引出今天的问题

首先我们要清楚几个概念

什么是 DOM?

  • DOM 全称 Document Object Model 文档对象模型
  • 它是为 HTML(XML)提供的 API
  • HTML 是一种标记语言 HTML 在 DOM 模型标准中被视为对象
  • DOM 只提供编程接口却无法实际操作 HTML 里面的内容
  • 在浏览器中 前端工程师可以通过脚本语言(js)通过 DOM 去操作 HTML 内容
    (不只 js 能调用 DOM 这个 API Python 也可以)
  • ps:也存在 CSSOM:CSS Object Model 浏览器将 CSS 代码解析成树形的数据结构与 DOM 是两个独立的数据机构

浏览器渲染过程

讨论 DOM 操作成本 首先要了解下该成本的来源 那么就离不开浏览器渲染
浏览器渲染前需要先构建 DOM 和 CSS 树 因此我们需要尽快将 HTML 和 CSS 都提供给浏览器

这里只讨论浏览器拿到 HTML 之后开始解析 渲染

之前的一些另开一篇

  1. 解析 HTML 构建 DOM 树 (这里遇到外链 会发起请求)
  2. 解析 CSS 生成 CSS 规则树
  3. 合并 DOM 树和 CSS 规则 生成 render(渲染)树
  4. 布局 render 树(Layout/reflow)负责各元素的尺寸,位置的计算
  5. 绘制 render 树(paint)绘制页面像素信息
  6. 浏览器会将各层的信息发送给 GPU GPU 将各层合成 (composite) 显示在屏幕上
    构建 DOM 树
    HTML 标记转换成文档对象模型 (DOM)
    DOM 树构建过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点
    构建 CSSOM 树
    CSS 标记转换成 CSS 对象模型 (CSSOM)
    在最终计算各个 2 节点的样式时 浏览器都会先从该节点的普遍属性(比如全局样式)开始 再去应用该节点的具体属性

每个浏览器都有自己的默认样式表因此很多时候这颗 CSSOM 树只是对这张默认样式表的部分替换

DOM 和 CSSOM 都要经过
Bytes→characters→tokens→nodes→objectmodel 这个过程
DOM 和 CSSOM 是独立的数据结构
此处需要一张图片

生成 render(渲染)树 由此 浏览器中会解析并生成两个内部数据结构
  • Dom 树表示页面结构
  • DOM 树和 CSSOM 合并生成 render 树(渲染树),渲染树表示 Dom 节点在页面中如何显示(宽高 位置等)
1
在dom树中每一个需要显示的节点在渲染树种至少存在一个对应的节点 渲染树中的节点被称之为“帧”或者“盒” 符合css模型的定义 一旦Dom树和渲染树构建完成  浏览器就开始 显示(绘制paint)页面元素

简单描述下 render 的过程

1
2
3
DOM树从根节点开始遍历可见节点
设置了类似 display:none (则该节点不可见) 在render过程中是被跳过的
visibility:hidden; opacity:0 这种仍旧占据空间的节点不会被跳过render 保存各个节点的样式信息及其余节点的从属关系
1
2
3
Layout布局
有了各个节点的信息属性 但不知道各个节点的确切位置和大小 所以要通过布局将样式信息和属性转换为实际可视窗口的相对大小和位置
(DOM 树捕获文档标记的属性和关系,但并未告诉我们元素在渲染后呈现的外观。那是 CSSOM 的责任)
1
2
Paint绘制
最后只要将确定好位置大小的各节点通过GPU渲染到屏幕的实际像素

TIPS:

  • 在上述渲染过程中 前三点可能要多次执行 比如 js 脚本去操作 DOM 更改 CSS 样式 浏览器又要重新构建 DOM CSSOM 树 重新 render 重新 layout paint
  • 因为 layout 在 paint 之前 因此每次 layput 重新布局(reflow 回流)后都要重新触发 paint 渲染 这时又要去消耗 GPU
  • paint 不一定会触发 layout 比如改个颜色改个背景(repaint 重绘)
  • 图片下载完也会重新触发 Layout 和 paint
何时触发 reflow(重排)和 repaint(重绘)

reflow (重排):当 dom 树的变化影响了元素的集合属性 =》 意味着元素的内容,结构 位置或者尺寸发生了变化,同样其他元素的集合属性和位置也会因此受到影响,浏览器会使渲染树(render 树)中受到影响的部分失效 需要重新计算样式和渲染树,这个过程称为重排(reflow)

repaint (重绘): 意味着元素发生的改变只你影响了节点的一些样式(背景色 边框颜色 文字元素等)只需要应用新样式绘制这个元素就可以了 (完成重排后 浏览器会重新绘制受影响的部分到屏幕中 这个过程叫做重绘)

并不是所有的 dom 辩护都会影响几何属性 例如 改变元素的背景色不会影响 宽和高 这种情况下 只会发生一次重绘(不需要重排)因为元素的布局没有改变

重排一定会引起浏览器的重绘 重绘则不一定伴随重排

重排的成本开销要高于重绘一个节点的重排往往导致子节点以及同级节点的重排

触发重排的情况

当页面布局的几何属性改变时就需要重排 下列情况会导致重排

1
2
3
4
5
6
7
8
9
页面第一次渲染(初始化)

DOM树变化(如:增删节点)
元素位置改变
元素尺寸改变(外边距 内边距 边框厚度 宽度 高度等)
Render树变化(如:padding改变)
浏览器窗口resize
获取元素的某些属性:
当滚动条出现时,会触发整个页面的重排

由于每次重排都会产生计算消耗,大多数浏览器通过队列化修改并批量执行来优化重排的过程

但是 我们经常会不知不觉强制刷新队列并要求计划任务立即执行
获取布局信息的操作会到最后队列刷新 比如

1
2
3
4
5
6
7
8
offsetTop , offsetLeft , offsetWidth , offsetHeight

scrollTop , scrollLeft , scrollWidth , scrollHeight

clientTop , clientLeft , clientWidth , clientHeight

getComputedStyle() ( currentStyle in IE )

当获取以上的属性和方法时 浏览器为了获取最新的布局信息 不得不立即触发重排以返回正确的值

最小化重绘和重排

重绘和重排代价很昂贵 因此一个号的提高程序响应熟读的策略就是减少此类操作的发生

优化方式
  1. 合并多次对样式属性的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
思考
var el = document.getElementById('mydiv');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';

即使有浏览器有重排机制优化 但最坏的情况也是进行三次重排

修改后

var el = document.getElementById('mydiv');
el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';
或者

var el = document.getElementById('mydiv');
el.className = 'active';

  1. 批量修改 dom
    当需要对 dom 元素进行一系列的操作时候 可以通过以下的步骤来减少重绘和重排的次数

* 使元素脱离文本流
* 操作元素
* 操作完成后 将元素带回文档中
这样儿 只有第一步和第三部触发两次重排

有三种方式可以实现上面的步骤

1. 隐藏元素(display:none)操作元素 重新展示

1
2
3
4
5
var ul = document.getElementById('mylist');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

2. 使用文档片段(document fragment)在当前 DOM 之外构建一个子树,再把它拷贝回文档

1
2
3
 var fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
document.getElementById('mylist').appendChild(fragment);

3. 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素

1
2
3
4
var old = document.getElementById('mylist');
var clone = old.cloneNode(true);
appendDataToElement(clone, data);
old.parentNode.replaceChild(clone, old);

总结:推荐尽可能的使用文档片段(第二个方案),因为它们所产生的 DOM 遍历和重排次数最少。唯一潜在的问题是文档片段未被充分利用,很多人可能并不熟悉这项技术。

  1. 缓存布局信息
1
浏览器获取元素的offsetLeft等属性值时会导致重排 将需要获取的保护局信息的属性值 赋值给变量 然后再操作变量
  1. 定位
1
将需要多次重排的元素,position 属性设置为 absolute 或 fixed,这样元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。

操作 DOM 具体的成本,说到底是造成浏览器重排和重绘,从而消耗 GPU 资源

s