週末長知識: scrollable <div> 的捲動重繪問題

Update 2020/05/05 12:00 Chrome 瀏覽器在當前版本(v81)已經沒有這個問題,本文就留做記念吧!(我想應該更早就已經修正了,但我遲遲沒有重新檢查)

前端也是做了很久,直到最近才發現這個事實,感覺有點羞愧 ww 為了讓同樣的事情不要再重演,決定專文說明之!

以下文章的內容基本以 Chrome 瀏覽器作為展示用的平台,所以一些 render performance 的問題不見得會出你家的瀏覽器上,畢竟各家瀏覽器為了讓使用者能夠順暢瀏覽網頁,可能還是有做一些各自的調整。

現在已知的是 Opera 在本文主題的範圍內所受的影響與 Chrome 是一樣的(同為 Blink引擎)。

scrollable <div>

其實直接看一下 CSS 就明白我的意思啦

.scrollable {
    height: 100%;
    overflow: auto;
}

其實就是帶有捲軸的 <div> 而已,它的特性是在內容超出設定的 寬度/高度 之後會出現捲軸(用 overflow: scroll 的話,會一直有捲軸)

捲動重繪問題,初次見面!

其實就是在新版的 手打 首頁。

其實一開始還感覺不大出來,但多捲幾下之後發現這個捲動的吃力程度連我的(三年前的)超級遊戲桌機都有點受不了,馬上來用 Chrome DevTools 看兩把!

before.png

從圖中可以清楚的看到每個 frame 中有很長的時間用在 painting,並且很多 frame 的運作時間超過 16~17ms,造成整體的 fps 沒辦法達到 60。

簡單的重現

Preview 的時候先按「Launch the preview in a separate window」在新視窗開啟,再來打開 DevTools 觀察比較準(原本擺在 iframe 裡可能會影響結果)

確認的方法就是開啟 DevTools 裡的「Enable paint flashing」,就可以在捲動的時候看到重繪的區域。

捲動時不重繪

https://plnkr.co/edit/Xg80bahiQcGZyVWqnp6W?p=preview

demo-norepaint.gif

捲動時重繪

https://plnkr.co/edit/UGDn3QOeET57SGuCPByk?p=preview

demo-repaint.gif

簡單的 workaround

其實以往會用到這樣的 <div> 的部分在網頁上並不多見,可能相對在 web app 裡面比較多見;然而現在隨著網頁設計/網頁技術的演進,出現各種嶄新的 layout 方法,於是 scrollable <div> 也變得不是這麼少見,而區塊也不見得是小小一塊(像 手打 就是整個內容全包在裡面)。

換句話說,要是你頭已經洗下去了,怎麼快速的挽救。

其實做法非常簡單,只要用 webkit/blink 引擎的神秘萬靈丹 -> 推到 composite layer 就好了

推到 composite layer 的方法也要與時俱進,從 Chrome 36 以後就可以用 will-change: transform 來建立新 layer。 (更多關於 will-change 的資訊可以看: https://dev.opera.com/articles/css-will-change-property/)

will-change: transform 加在 捲動的那個 <div> 上,即完成修正!

.scrollable {
    height: 100%;
    overflow: auto;
    will-change: transform;  /* yeah ! */
}

捲動時重繪 -> 修正成果

https://plnkr.co/edit/ZrFj9Gh9JrafHx6q1pPf?p=preview

demo-repaint-fixed.gif

Material Design Lite

其實上面有說到 各種嶄新的 layout 方法 說的就是 Material Design Lite

稍微看一下就會發現它的 layout 完全是基於層層的 flexbox,然後最外層會用一個 position: absolute 的滿版 <div> 全部包起來。

換句話說只要是使用 Material Design Lite,你的整個網頁內容都會是包在 scrollable <div> 裡…

對,新版的 手打 就是套用了使用 Materia Design Lite 所以才會有捲動重會比較嚴重的現象。

所以同樣的到 MDL 官網捲看看會發現他們也是一直重繪。

demo-repaint-mdl.gif

TL;DR

內部可捲動的 <div>

  • 不做任何處理的話,會在捲動的時候重繪內容
  • 如果版面很大、內容複雜,重繪很吃捲動效能
  • 例: Material Design Lite

解決方法

  • 將關鍵的 <div> 推到 composite layer
  • 使用新的 will-change: transform

後記

新版 手打 網站其實在撰文的時候已經透過上面的 workaround 修正了 XD

solda-io-after.png

主要還是想說 MDL 超先進 layout 方法真是表翻了 Blink引擎 的瀏覽器(感覺受 scrollable <div> 影響效能較別的瀏覽器嚴重)… 明明算是 Google 自家出品的啊 www

其實這個問題有人在 MDL 那邊開了 issue,不過目前看似被放置了。