在线工具 在线编程 在线白板 在线工具 在线编程 在线白板

精读《webreflow》

我请问下,精读《webreflow》
最新回答
雨中的狗尾巴草

2024-10-15 12:43:00

网页重排(回流)是阻碍流畅性的重要原因之一,结合Whatforceslayout/reflow这篇文章与引用,整理一下回流的起因与优化思考。

借用这张经典图:

网页渲染会经历DOM->CSSOM->Layout(重排orreflow)->Paint(重绘)->Composite(合成),其中Composite在精读《深入了解现代浏览器四》详细介绍过,是在GPU进行光栅化。

那么排除JS、DOM、CSSOM、Composite可能导致的性能问题外,剩下的就是我们这次关注的重点,reflow了。从顺序上可以看出来,重排后一定重绘,而重绘不一定触发重排。

概述

什么时候会触发Layout(reflow)呢?一般来说,当元素位置发生变化时就会。但也不尽然,因为浏览器会自动合并更改,在达到某个数量或时间后,会合并为一次reflow,而reflow是渲染页面的重要一步,打开浏览器就一定会至少reflow一次,所以我们不可能避免reflow。

那为什么要注意reflow导致的性能问题呢?这是因为某些代码可能导致浏览器优化失效,即明明能合并reflow时没有合并,这一般出现在我们用jsAPI访问某个元素尺寸时,为了保证拿到的是精确值,不得不提前触发一次reflow,即便写在for循环里。

当然也不是每次访问元素位置都会触发reflow,在浏览器触发reflow后,所有已有元素位置都会记录快照,只要不再触发位置等变化,第二次开始访问位置就不会触发reflow,关于这一点会在后面详细展开。现在要解释的是,这个”触发位置等变化“,到底有哪些?

根据Whatforceslayout/reflow文档的总结,一共有这么几类:

获得盒子模型信息

elem.offsetLeft,elem.offsetTop,elem.offsetWidth,elem.offsetHeight,elem.offsetParent

elem.clientLeft,elem.clientTop,elem.clientWidth,elem.clientHeight

elem.getClientRects(),elem.getBoundingClientRect()

获取元素位置、宽高的一些手段都会导致reflow,不存在绕过一说,因为只要获取这些信息,都必须reflow才能给出准确的值。

滚动

elem.scrollBy(),elem.scrollTo()

elem.scrollIntoView(),elem.scrollIntoViewIfNeeded()

elem.scrollWidth,elem.scrollHeight

elem.scrollLeft,elem.scrollTop访问及赋值

对scrollLeft赋值等价于触发scrollTo,所有导致滚动产生的行为都会触发reflow,笔者查了一些资料,目前主要推测是滚动条出现会导致可视区域变窄,所以需要reflow。

focus()

elem.focus()(源码)

可以根据源码看一下注释,主要是这一段:

//Ensurewehavecleanstyle(includingforceddisplaylocks).GetDocument().UpdateStyleAndLayoutTreeForNode(this)

即在聚焦元素时,虽然没有拿元素位置信息的诉求,但指不定要被聚焦的元素被隐藏或者移除了,此时必须调用UpdateStyleAndLayoutTreeForNode重排重绘函数,确保元素状态更新后才能继续操作。

还有一些其他elementAPI:

elem.computedRole,elem.computedName

elem.innerText(源码)

innerText也需要重排后才能拿到正确内容。

获取window信息

window.scrollX,window.scrollY

window.innerHeight,window.innerWidth

window.visualViewport.height/width/offsetTop/offsetLeft(源码)

和元素级别一样,为了拿到正确宽高和位置信息,必须重排。

document相关

document.scrollingElement仅重绘

document.elementFromPoint

elementFromPoint因为要拿到精确位置的元素,必须重排。

Form相关

inputElem.focus()

inputElem.select(),textareaElem.select()

focus、select触发重排的原因和elem.focus类似。

鼠标事件相关

mouseEvt.layerX,mouseEvt.layerY,mouseEvt.offsetX,mouseEvt.offsetY(源码)

鼠标相关位置计算,必须依赖一个正确的排布,所以必须触发reflow。

getComputedStyle

getComputedStyle通常会导致重排和重绘,是否触发重排取决于是否访问了位置相关的key等因素。

Range相关

range.getClientRects(),range.getBoundingClientRect()

获取选中区域的大小,必须reflow才能保障精确性。

SVG

大量SVG方法会引发重排,就不一一枚举了,总之使用SVG操作时也要像操作dom一样谨慎。

contenteditable

被设置为contenteditable的元素内,包括将图像复制到剪贴板在内,大量操作都会导致重排。(源码)

精读

Whatforceslayout/reflow下面引用了几篇关于reflow的相关文章,笔者挑几个重要的总结一下。

repaint-reflow-restyle

repaint-reflow-restyle提到现代浏览器会将多次dom操作合并,但像IE等其他内核浏览器就不保证有这样的实现了,因此给出了一个安全写法:

//badvarleft=10,top=10;el.style.left=left+"px";el.style.top=top+"px";//betterel.className+="theclassname";//orwhentopandleftarecalculateddynamically...//betterel.style.cssText+=";left:"+left+"px;top:"+top+"px;";

比如用一次className的修改,或一次cssText的修改保证浏览器一定触发一次重排。但这样可维护性会降低很多,不太推荐。

avoidlargecomplexlayouts

avoidlargecomplexlayouts重点强调了读写分离,首先看下面的badcase:

functionresizeAllParagraphsToMatchBlockWidth(){//Putsthebrowserintoaread-write-read-writecycle.for(vari=0;i<paragraphs.length;i++){paragraphs[i].style.width=box.offsetWidth+'px';}}

在for循环中不断访问元素宽度,并修改其宽度,会导致浏览器执行N次reflow。

虽然当JavaScript运行时,前一帧中的所有旧布局值都是已知的,但当你对布局做了修改后,前一帧所有布局值缓存都会作废,因此当下次获取值时,不得不重新触发一次reflow。

而读写分离的话,就代表了集中读,虽然读的次数还是那么多,但从第二次开始就可以从布局缓存中拿数据,不用触发reflow了。

另外还提到flex布局比传统float重排速度快很多(3msvs16ms),所以能用flex做的布局就尽量不要用float做。

reallyfixinglayoutthrashing

reallyfixinglayoutthrashing提到了用fastdom实践读写分离:

ids.forEach(id=>{fastdom.measure(()=>{consttop=elements[id].offsetTopfastdom.mutate(()=>{elements[id].setLeft(top)})})})

fastdom是一个可以在不分离代码的情况下,分离读写执行的库,尤其适合用在reflow性能优化场景。每一个measure、mutate都会推入执行队列,并在window.requestAnimationFrame时机执行。

总结

回流无法避免,但需要控制在正常频率范围内。

我们需要学习访问哪些属性或方法会导致回流,能不使用就不要用,尽量做到读写分离。在定义要频繁触发回流的元素时,尽量使其脱离文档流,减少回流产生的影响。

讨论地址是:精读《webreflow》·Issue#420·dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读-帮你筛选靠谱的内容。

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

原文:https://juejin.cn/post/7103348980259487752