浏览器从下载文件到显示页面是一个很复杂的过程浏览器下载完页面中的所有组件 html 标记 JavaScriptCSS 图片 之后会解析并生成两个内部数据结构:
DOM 树
表示页面结构
渲染树
表示 DOM 节点在页面中如何显示(宽高位置等)
DOM 树中的每一个需要显示的节点在渲染树中至少存在一个对应的节点 (隐藏的 DOM 元素在渲染树中没有对应的节点) 渲染树中的节点被称为 帧 (frames) 或 盒(boxes) , 符合 CSS 模型的定义, 理解页面元素为一个具有内边距(padding) 外边距 (margin) 边框 (border) 和位置 (position) 的盒子一旦 DOM 树和渲染树构建完成, 浏览器就开始显示 (绘制 paint) 页面元素
当 DOM 的变化影响了元素的几何属性 (宽和高) 比如改变边框宽度或给段落增加文字, 导致行数增加 浏览器需要重新计算元素的几何属性, 同样其他元素的几何属性和位置也会因此受到影响浏览器会使渲染树中受到影响的部分失效, 并重新构建渲染树这个过程称为 重排(reflow) 完成重排后, 浏览器会重新绘制受影响的部分到屏幕中, 该过程称为 重绘(repaint)
并不是所有的 DOM 变化都会影响几何属性例如, 改变元素的背景色不会影响宽和高, 这种情况下, 只会发生一次重绘(不需要重排), 因为元素的布局并没有改变
重排一定会引起浏览器的重绘, 而重绘则不一定伴随重排
重绘和重排操作都是代价昂贵的操作, 它会导致 web 应用程序的 UI 反应迟钝所以, 应当尽可能减少这类过程的发生
触发重排的情况
当页面布局的几何属性改变时就需要 重排下述情况会导致重排:
添加或删除可见的 DOM 元素
元素位置改变
元素尺寸改变(包括: 外边距内边距边框厚度宽度高度等属性改变)
内容改变例如: 文本改变或图片被另一个不同尺寸的图片代替
页面渲染器初始化
浏览器窗口尺寸改变
获取会导致渲染队列刷新的属性(详细介绍如下)
根据改变的范围和程度, 渲染树中相应部分也需要重新计算
当滚动条出现时, 会触发整个页面的重排
渲染树变化的排队与刷新
由于每次重排都会产生计算消耗, 大多数浏览器通过队列化修改并批量执行来优化重排过程
然而, 你可能会 (经常不知不觉) 强制刷新队列并要求计划任务立刻执行获取布局信息的操作会导致队列刷新, 比如使用以下属性:
- offsetTop , offsetLeft , offsetWidth , offsetHeight
- scrollTop , scrollLeft , scrollWidth , scrollHeight
- clientTop , clientLeft , clientWidth , clientHeight
- getComputedStyle() ( currentStyle in IE )
当获取以上的属性和方法时, 浏览器为了获取最新的布局信息, 不得不执行渲染队列中的 待处理变化 , 并触发重排以返回正确的值
最小化重绘和重排
重绘和重排的代价非常昂贵, 因此一个好的提高程序响应速度的策略就是减少此类操作的发生
合并多次对样式属性的操作
为了减少重绘重排发生的次数, 应该合并多次对 DOM 和样式的修改, 然后一次处理掉
考虑下面的例子:
- var el = document.getElementById('mydiv');
- el.style.borderLeft = '1px';
- el.style.borderRight = '2px';
- el.style.padding = '5px';
示例中有三个样式属性被改变, 每一个都会影响元素的几何结构最糟糕的情况下, 会导致浏览器触发三次重排大部分现代浏览器为此做了优化, 只会触发一次重排, 但是在旧的浏览器中或者有一个分离的异步处理过程时(比如使用计时器), 仍然效率低下如果在上面代码执行时, 有其他代码请求布局信息, 这会导致三次重排而且, 这段代码四次访问 DOM , 可以被优化
上面的代码执行效率更高的方式是: 将多次改变样式属性的操作合并为一次操作, 这样只会修改 DOM 一次如:
- var el = document.getElementById('mydiv');
- el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';
另一个一次性修改样式的办法是修改 CSS 的 class 名称, 而不是修改内联样式这种方法适用于那些不依赖于运行逻辑和计算的情况
- var el = document.getElementById('mydiv');
- el.className = 'active';
批量修改 DOM
当需要对 DOM 元素进行一系列操作时, 可以通过以下步骤来减少重绘和重排的次数:
使元素脱离文档流
操作元素
操作完成后, 将元素带回文档中
这样, 只有在第一步和第三步会触发两次重排
有三种方式可以实现上面的步骤:
隐藏元素(display: none;), 操作元素, 重新显示
- var ul = document.getElementById('mylist');
- ul.style.display = 'none';
- appendDataToElement(ul, data);
- ul.style.display = 'block';
使用文档片段 (document fragment) 在当前 DOM 之外构建一个子树, 再把它拷贝回文档
- var fragment = document.createDocumentFragment();
- appendDataToElement(fragment, data);
- document.getElementById('mylist').appendChild(fragment);
将原始元素拷贝到一个脱离文档的节点中, 修改副本, 完成后再替换原始元素
- var old = document.getElementById('mylist');
- var clone = old.cloneNode(true);
- appendDataToElement(clone, data);
- old.parentNode.replaceChild(clone, old);
推荐尽可能的使用文档片段(第二个方案), 因为它们所产生的 DOM 遍历和重排次数最少唯一潜在的问题是文档片段未被充分利用, 很多人可能并不熟悉这项技术
缓存布局信息
浏览器获取元素的 offsetLeft 等属性值时, 会导致重排最好的做法是, 将需要获取的布局信息的属性值, 赋值给变量, 然后再操作变量
定位
将需要多次重排的元素, position 属性设置为 absolute 或 fixed, 这样元素就脱离了文档流, 它的变化不会影响到其他元素例如有动画效果的元素就最好设置为绝对定位
参考资料
高性能 JavaScript 第三章 DOM 编程
来源: https://juejin.im/entry/5ab9a4cb51882555677e90ae