1 重绘和重排
1.1 重绘和重排是什么
重绘是指一些样式的修改, 元素的位置和大小都没有改变;
重排是指元素的位置或尺寸发生了变化, 浏览器需要重新计算渲染树, 而新的渲染树建立后, 浏览器会重新绘制受影响的元素
1.2 浏览器渲染页面
去参加面试总会被问到一个问题, 那就是向浏览器输入一行 url 会发生什么?, 这个问题的答案除了要回答网络方面的知识还牵扯到浏览器渲染页面问题当我们的浏览器接收到从服务器响应的页面之后便开始逐行渲染, 遇到 CSS 的时候会异步的去计算属性值, 再继续向下解析 dom 解析完毕之后形成一颗 DOM 树, 将异步计算好的样式 (样式盒子) 与 DOM 树相结合便成为了一个 Render 树, 再由浏览器绘制在页面上 DOM 树与 Render 树的区别在于: 样式为 display:none; 的节点会在 DOM 树中而不在渲染树中浏览器绘制了之后便开始解析 js 文件, 根据 js 来确定是否重绘和重排
1.3 引起重绘和重排的原因
产生重绘的因素:
改变 visibilityoutline 背景色等样式属性, 并没有改变元素大小位置等浏览器会根据元素的新属性重新绘制
产生重排的因素:
内容改变
文本改变或图片尺寸改变
DOM 元素的几何属性的变化
例如改变 DOM 元素的宽高值时, 原渲染树中的相关节点会失效, 浏览器会根据变化后的 DOM 重新排建渲染树中的相关节点如果父节点的几何属性变化时, 还会使其子节点及后续兄弟节点重新计算位置等, 造成一系列的重排
DOM 树的结构变化
添加 DOM 节点修改 DOM 节点位置及删除某个节点都是对 DOM 树的更改, 会造成页面的重排浏览器布局是从上到下的过程, 修改当前元素不会对其前边已经遍历过的元素造成影响, 但是如果在所有的节点前添加一个新的元素, 则后续的所有元素都要进行重排
获取某些属性
除了渲染树的直接变化, 当获取一些属性值时, 浏览器为取得正确的值也会发生重排, 这些属性包括: offsetTopoffsetLeft offsetWidthoffsetHeightscrollTopscrollLeftscrollWidthscrollHeight clientTopclientLeftclientWidthclientHeightgetComputedStyle()
浏览器窗口尺寸改变
窗口尺寸的改变会影响整个网页内元素的尺寸的改变, 即 DOM 元素的集合属性变化, 因此会造成重排
滚动条的出现(会触发整个页面的重排)
总之你要知道, js 是单线程的, 重绘和重排会阻塞用户的操作以及影响网页的性能, 当一个页面发生了多次重绘和重排比如写一个定时器每 500ms 改变页面元素的宽高, 那么这个页面可能会变得越来越卡顿, 我们要尽可能的减少重绘和重排那么我们对于 DOM 的优化也是基于这个开始
2 优化
2.1 减少访问
减少访问次数自然是想到缓存元素, 但是要注意
var ele = document.getElementById('ele');
这样并不是对 ele 进行缓存, 每一次调用 ele 还是相当于访问了一次 id 为 ele 的节点
2.1.1 缓存 NodeList
var foods = document.getElementsByClassName('food');
我们可以用 foods[i]来访问第 i 个 class 为 food 的元素, 不过这里的 foods 并不是一个数组, 而是一个 NodeListNodeList 是一个类数组, 保存了一些有序的节点并可以通过位置来访问这些节点 NodeList 对象是动态的, 每一次访问都会运行一次基于文档的查询所以我们要尽量减少访问 NodeList 的次数, 可以考虑将 NodeList 的值缓存起来
- // 优化前
- var lis = document.getElementsByTagName('li');
- for (var i = 0; i < lis.length; i++) {
- // do something...
- }
- // 优化后, 将 length 的值缓存起来就不会每次都去查询 length 的值
- var lis = document.getElementsByTagName('li');
- for (var i = 0,
- len = lis.length; i < len; i++) {
- // do something...
- }
而且由于 NodeList 是动态变化的, 所以如果不缓存可能会引起死循环, 比如一边添加元素, 一边获取 NodeList 的 length
2.1.2 改变选择器
获取元素最常见的有两种方法, getElementsByXXX()和 queryselectorAll(), 这两种选择器区别是很大的, 前者是获取动态集合, 后者是获取静态集合, 举个例子
- // 假设一开始有 2 个 li
- var lis = document.getElementsByTagName('li'); // 动态集合
- var ul = document.getElementsByTagName('ul')[0];
- for (var i = 0; i < 3; i++) {
- console.log(lis.length);
- var newLi = document.createElement('li');
- ul.appendChild(newLi);
- }
- // 输出结果: 2, 3, 4
- var lis = document.querySelector('li'); // 静态集合
- var ul = document.getElementsByTagName('ul')[0];
- for (var i = 0; i < 3; i++) {
- console.log(lis.length);
- var newLi = document.createElement('li');
- ul.appendChild(newLi);
- }
- // 输出结果: 2, 2, 2
对静态集合的操作不会引起对文档的重新查询, 相比于动态集合更加优化
2.1.3 避免不必要的循环
- // 优化前
- for(var i = 0; i < 10; i++) {
- document.getElementById('ele').innerhtml += 'a';
- }
- // 优化后
- var str = '';
- for(var i = 0; i < 10; i++) {
- str += 'a';
- }
- document.getElementById('ele').innerHTML = str;
优化前的代码访问了 10 次 ele 元素, 而优化后的代码只访问了一次, 大大的提高了效率
2.1.4 事件委托
js 中的事件函数都是对象, 如果事件函数过多会占用大量内存, 而且绑定事件的 DOM 元素越多会增加访问 dom 的次数, 对页面的交互就绪时间也会有延迟所以诞生了事件委托, 事件委托是利用了事件冒泡, 只指定一个事件处理程序就可以管理某一类型的所有事件
- // 事件委托前
- var lis = document.getElementsByTagName('li');
- for(var i = 0; i < lis.length; i++) {
- lis[i].onclick = function() {
- console.log(this.innerHTML);
- };
- }
- // 事件委托后
- var ul = document.getElementsByTagName('ul')[0];
- ul.onclick = function(event) {
- console.log(event.target.innerHTML);
- };
事件委托前我们访问了 lis.length 次 li, 而采用事件委托之后我们只访问了一次 ul
2.2 减少重绘重排
2.2.1 改变一个 dom 节点的多个样式
我们想改变一个 div 元素的宽度和高度, 通常做法可以是这样
- var div = document.getElementById('div1');
- div.style.width = '220px';
- div.style.height = '300px';
以上操作改变了元素的两个属性, 访问了三次 dom, 触发两次重排与两次重绘我们说过优化是减少访问次数以及减少重绘重排次数, 从这个出发点可不可以只访问一次元素以及重排次数降低到 1 呢? 显然是可以的, 我们可以在 css 里写一个 class
- /* css
- .change {
- width: 220px;
- height: 300px;
- }
- */
- document.getElementById('div').className = 'change';
这样就达到了一次操作多个样式
2.2.2 批量修改 dom 节点样式
上面代码的情况是针对于一个 dom 节点的, 如果我们要改变一个 dom 集合的样式呢?
第一时间想到的方法是遍历集合, 给每个节点加一个 className 再想想这样岂不是访问了多次 dom 节点? 想想文章开头说的 dom 树和渲染树的区别, 如果一个节点的 display 属性为 none 那么这个节点不会存在于 render 树中, 意味着对这个节点的操作也不会影响 render 树进而不会引起重绘和重排, 基于这个思路我们可以实现优化:
将待修改的集合的父元素 display: none;
之后遍历修改集合节点
将集合父元素 display: block;
- // 假设增加的 class 为. change
- var lis = document.getElementsByTagName('li');
- var ul = document.getElementsByTagName('ul')[0];
- ul.style.display = 'none';
- for(var i = 0; i < lis.length; i++) {
- lis[i].className = 'change';
- }
- ul.style.display = 'block';
3 总结
减少访问 dom 的次数
缓存节点属性值
选择器的使用
避免不必要的循环
事件委托
减少重绘与重排
使用 className 改变多个样式
使父元素脱离文档流再恢复
如果以后看到其他优化方案我会更新, 欢迎大家与我交流
参考文档:
高性能 JS-DOM
来源: https://www.cnblogs.com/wind-lanyan/p/8588544.html