精讀《web reflow》

黃子毅發表於2022-05-30

網頁重排(迴流)是阻礙流暢性的重要原因之一,結合 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()

可以根據原始碼看一下注釋,主要是這一段:

// 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()

focusselect 觸發重排的原因和 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 效能優化場景。每一個 measuremutate 都會推入執行佇列,並在 window.requestAnimationFrame 時機執行。

總結

迴流無法避免,但需要控制在正常頻率範圍內。

我們需要學習訪問哪些屬性或方法會導致迴流,能不使用就不要用,儘量做到讀寫分離。在定義要頻繁觸發迴流的元素時,儘量使其脫離文件流,減少迴流產生的影響。

討論地址是:精讀《web reflow》· Issue #420 · dt-fe/weekly

如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章