網頁重排(迴流)是阻礙流暢性的重要原因之一,結合 What forces layout / reflow 這篇文章與引用,整理一下回流的起因與優化思考。
借用這張經典圖:
網頁渲染會經歷 DOM -> CSSOM -> Layout(重排 or reflow) -> Paint(重繪) -> Composite(合成),其中 Composite 在 精讀《深入瞭解現代瀏覽器四》 詳細介紹過,是在 GPU 進行光柵化。
那麼排除 JS、DOM、CSSOM、Composite 可能導致的效能問題外,剩下的就是我們這次關注的重點,reflow 了。從順序上可以看出來,重排後一定重繪,而重繪不一定觸發重排。
概述
什麼時候會觸發 Layout(reflow) 呢?一般來說,當元素位置發生變化時就會。但也不盡然,因為瀏覽器會自動合併更改,在達到某個數量或時間後,會合併為一次 reflow,而 reflow 是渲染頁面的重要一步,開啟瀏覽器就一定會至少 reflow 一次,所以我們不可能避免 reflow。
那為什麼要注意 reflow 導致的效能問題呢?這是因為某些程式碼可能導致瀏覽器優化失效,即明明能合併 reflow 時沒有合併,這一般出現在我們用 js API 訪問某個元素尺寸時,為了保證拿到的是精確值,不得不提前觸發一次 reflow,即便寫在 for 迴圈裡。
當然也不是每次訪問元素位置都會觸發 reflow,在瀏覽器觸發 reflow 後,所有已有元素位置都會記錄快照,只要不再觸發位置等變化,第二次開始訪問位置就不會觸發 reflow,關於這一點會在後面詳細展開。現在要解釋的是,這個 ”觸發位置等變化“,到底有哪些?
根據 What forces layout / 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()
(原始碼)
可以根據原始碼看一下注釋,主要是這一段:
// Ensure we have clean style (including forced display locks).
GetDocument().UpdateStyleAndLayoutTreeForNode(this)
即在聚焦元素時,雖然沒有拿元素位置資訊的訴求,但指不定要被聚焦的元素被隱藏或者移除了,此時必須呼叫 UpdateStyleAndLayoutTreeForNode
重排重繪函式,確保元素狀態更新後才能繼續操作。
還有一些其他 element API:
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
的元素內,包括將影像複製到剪貼簿在內,大量操作都會導致重排。(原始碼)
精讀
What forces layout / reflow 下面引用了幾篇關於 reflow 的相關文章,筆者挑幾個重要的總結一下。
repaint-reflow-restyle
repaint-reflow-restyle 提到現代瀏覽器會將多次 dom 操作合併,但像 IE 等其他核心瀏覽器就不保證有這樣的實現了,因此給出了一個安全寫法:
// bad
var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// better
el.className += " theclassname";
// or when top and left are calculated dynamically...
// better
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
比如用一次 className 的修改,或一次 cssText
的修改保證瀏覽器一定觸發一次重排。但這樣可維護性會降低很多,不太推薦。
avoid large complex layouts
avoid large complex layouts 重點強調了讀寫分離,首先看下面的 bad case:
function resizeAllParagraphsToMatchBlockWidth() {
// Puts the browser into a read-write-read-write cycle.
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
在 for 迴圈中不斷訪問元素寬度,並修改其寬度,會導致瀏覽器執行 N 次 reflow。
雖然當 JavaScript 執行時,前一幀中的所有舊佈局值都是已知的,但當你對佈局做了修改後,前一幀所有佈局值快取都會作廢,因此當下次獲取值時,不得不重新觸發一次 reflow。
而讀寫分離的話,就代表了集中讀,雖然讀的次數還是那麼多,但從第二次開始就可以從佈局快取中拿資料,不用觸發 reflow 了。
另外還提到 flex 佈局比傳統 float 重排速度快很多(3ms vs 16ms),所以能用 flex 做的佈局就儘量不要用 float 做。
really fixing layout thrashing
really fixing layout thrashing 提到了用 fastdom 實踐讀寫分離:
ids.forEach(id => {
fastdom.measure(() => {
const top = elements[id].offsetTop
fastdom.mutate(() => {
elements[id].setLeft(top)
})
})
})
fastdom
是一個可以在不分離程式碼的情況下,分離讀寫執行的庫,尤其適合用在 reflow 效能優化場景。每一個 measure
、mutate
都會推入執行佇列,並在 window.requestAnimationFrame 時機執行。
總結
迴流無法避免,但需要控制在正常頻率範圍內。
我們需要學習訪問哪些屬性或方法會導致迴流,能不使用就不要用,儘量做到讀寫分離。在定義要頻繁觸發迴流的元素時,儘量使其脫離文件流,減少迴流產生的影響。
討論地址是:精讀《web reflow》· Issue #420 · dt-fe/weekly
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)