日期:2025/03/30 05:11来源:未知 人气:54
reflow 和 repaint 在pc端只要不是怀有明知山有虎,偏向虎山行的心态写代码,这两货几乎不会引发性能问题,但是移动端的渲染能力和pc端差了不止一个大截,一个不小心reflow和repaint就成了移动端的“性能杀手”。所以了解 reflow 和 repaint 也是很有必要的,在考量页面性能的时候分析 reflow 和 repaint 也算是一个切入点。
reflow
回流,或者叫重排都可以。回流 reflow 这个名词指的是浏览器为了重新渲染部分或全部的文档而重新计算文档中元素的位置和几何结构的过程。
简单来说就是当页面布局或者几何属性改变时就需要 reflow。
在一个页面中至少在页面刚加载的时候有一次reflow,在reflow的过程中浏览器会将render tree中受影响的节点失效,再重新构建render tree,有时候,即使仅仅回流一个单一的元素,也可能要求它的父元素以及任何跟随它的元素也产生回流
repaint
重绘,当页面中的元素只需要更新样式风格不影响布局,比如更换背景色background-color,这个过程就是重绘。
从reflow的定义中就可以听出一些来,元素的布局和几何属性改变时就会触发reflow。主要有这些属性:
除开这三大类的属性变动会触发reflow,以下情况也会触发:
页面中的元素更新样式风格相关的属性时就会触发重绘,如background,color,cursor,visibility,etc
注意 :由页面的渲染过程可知,reflow必将会引起repaint,而repaint不一定会引起reflow
了解有哪些属性值改变会触发回流或者重绘点击这里
设想一个这样的场景,我们需要在一个循环中不断修改一个dom节点的位置或者是内容
document.addEventListener('DOMContentLoaded', function () { var date = newDate(); for (var i = 0; i < 70000; i++) { var tmpNode = document.createElement("div"); tmpNode.innerHTML = "test" + i; document.body.appendChild(tmpNode); } console.log(newDate() - date);});
这里多次测量消耗时间大概在500ms(运行环境均为pc端,小霸王笔记本)。看到这个结果可能就有疑问了,这里有70000 次内容的修改,就有70000 reflow操作,也就用了500ms的时间(归功于迟缓的dom操作),说好的reflow消耗性能呢。  其实在这个过程中,浏览器为了防止我们犯二把多次reflow操作放在循环中而引发浏览器假死,做了一个聪明的小动作。它会收集reflow操作到缓存队列中直到一定的规模或者过了特定的时间,再一次性地flush队列,反馈到render tree中,这样就将多次的reflow操作减少为少量的reflow。但是这样的小动作带来了另外一个问题,如果我们想要在一次reflow过后就获取元素变动过后的值呢?这个时候浏览器为了获取真实的值就不得不立即flush缓存的队列。这些值或方法包括:
犯二代码如下:
document.addEventListener('DOMContentLoaded', function () { var date = newDate(); for (var i = 0; i < 70000; i++) { var tmpNode = document.createElement("div"); tmpNode.innerHTML = "test" + i; document.body.offsetHeight; // 获取body的真实值document.body.appendChild(tmpNode); } console.log("speed time", newDate() - date); });
一般人应该不会去运行这种代码,如果你运行了的话,恭喜你的电脑-1s。但是如果没有衡量指标,优化性能也就无从谈起。
If you cannot measure it, you cannot improve it. -Lord Kelvin
为了防止浏览器假死,把循环次数都改为7000次,得出的结果是(多次平均):
通过这两个样例印证了浏览器确实有优化reflow的小动作,聪明的程序员不会依赖浏览器的优化策略,在日常开发中遇到for循环就应该慎重编写循环体内部的代码。
如何减少reflow和repaint呢?回到定义去,reflow在页面布局或者定位发生变化时才会发生
,从定义中我们至少可以得出两个优化思路
其本质上为减少对render tree的操作。render tree也就是渲染树,它的每个节点都是可见,且包含该节点的内容和对应的规则样式,这也是render tree和dom数最大的区别所在, 减少reflow操作,主旨是合并多个reflow,最后再反馈到render tree中,诸如:
// 不好的写法var left = 1; var top = 2; ele.style.left = left + "px"; ele.style.top = top + "px"; // 比较好的写法 ele.className += " className1";
或者直接修改cssText:
ele.style.cssText += "; left: " + left + "px; top: " + top + "px;";
Dom规定文档片段(document fragment)是一种“轻量级”的文档,可以包含和控制节点,但不会想完整的文档那样占用额外的资源。虽然不能把文档片段直接添加到文档中,但是可以将它作为一个“仓库”来使用,即可以在里面保存将来可能会添加到文档中的节点。 比如最开始的样例结合DocumentFragment就可以这样写:
document.addEventListener('DOMContentLoaded', function () { var date = newDate(), fragment = document.createDocumentFragment(); for (var i = 0; i < 7000; i++) { var tmpNode = document.createElement("div"); tmpNode.innerHTML = "test" + i; fragment.appendChild(tmpNode); } document.body.appendChild(fragment); console.log("speed time", newDate() - date); });
将多个修改结果收纳到了documentFragment这个“仓库”中,这个过程并不会影响到render tree,待循环完毕再将这个“仓库”的“存货”添加到dom上,以此达到减少reflow的目的,使用cloneNode也是同理。 而使用display:none来降低reflow的性能开销的原理在于使节点从render tree中失效,等经过多个reflow后再“上线”
// 不好的写法for(let i = 0; i < 20; i++ ) { el.style.left = el.offsetLeft + 5 + "px"; el.style.top = el.offsetTop + 5 + "px"; }// 比较好的写法var left = el.offsetLeft, top = el.offsetTop, s = el.style; for (let i = 0; i < 20; i++ ) { left += 5; top += 5; s.left = left + "px"; s.top = top + "px"; }
我们可以将一些会触发回流的属性替换,来避免reflow。比如用translate代替top,用opacity替代visibility
样例代码:
<!DOCTYPE html>