本文作者:任家樂
原創宣告:本文為閱文前端團隊 YFE 成員出品,請尊重原創,轉載請聯絡公眾號 ( id: yuewen_YFE ) 獲取授權,並註明作者、出處和連結。
前言
曾寫過一篇效能優化 “ 長篇報告 ” 「 checkbox 美化引發的蝴蝶效應 」 ,也曾感嘆 CSS 對渲染的影響是如此大,也許深化記憶點的代價就是被同一塊石頭絆倒2次 ?是的,效能優化“報告”第二彈來了,希望本篇文章可以在優化頁面效能上給大家提供一些思路。
看點
某些 CSS 屬性或 JS 操作,看似簡單卻易隱藏陷阱?
- visibility - 在不改變文件佈局的情況下展示或隱藏某個元素。
- clientHeight / clientWidth - 獲取元素內部的寬高。
磨刀不誤砍柴工,效能工具的親密接觸總是必不可少的。
- Edge 瀏覽器效能工具怎麼用
- 如何快速的定位、解決效能問題?
同是瀏覽器,渲染層面的表現卻如此不同?
- Edge 瀏覽器的渲染策略和 Chrome 有何不同?
效能炸彈
問題始於一個 “ 紅色效能炸彈 ” ,瞄準的是 Webnovel「起點海外版」 PC 站點閱讀頁,關鍵字:Edge 瀏覽器、CPU、RAM。Webnovel 的國外使用者指出,在 Edge 瀏覽器下,閱讀頁 CPU 消耗的非常大,他的腿甚至感受到了膝上型電腦的灼燒。
Webnovel 閱讀頁,為了更趨近於完美的閱讀體驗,我們做了:預載入上下章、支援上下鍵切換章節、無重新整理跳章節等優化,在閱讀過程中,你甚至不會看到內容載入過程中的 loading 。然而面對這次的效能突擊,這些優化在 Edge 瀏覽器下顯得如此渺小...而對於是否值得做此次效能優化,Google Analytics 則給出了答案:
「 Webnovel 瀏覽器佔比一覽」
Edge 瀏覽器 TOP4 (2.53%)的佔比情況已不容忽略!看來註定免不了和 Edge 的一場針鋒相對了,立馬去 Edge 瀏覽器上一探究竟~
問題的剖析
定位問題之前,內心有個聲音在提醒我:是 Google 廣告指令碼的問題!因為前不久閱讀頁引入了 Google 廣告,並且針對廣告的展示及隱藏做過一些邏輯的調整,帶著目的首先從 JS 開始分析。
效能工具中尋找問題程式碼 - Google 指令碼在不斷獲取 clientHeight、clientWidth?
我是用 Edge 瀏覽器自帶的效能工具來進行分析的,由於 Edge 只能在 win10 系統上執行,我工作中用的是 iMac 所以幾乎沒有使用過 ,也只能回家 “踢走” 老公倒騰他專門打遊戲的桌上型電腦了,其 Edge 瀏覽器版本:42.17134.1.0。
開啟 Edge 開發者工具,效能工具的入口映入眼簾。
「 Edge 開發者工具 」
切入正題:「 訪問 Webnovel 閱讀頁 - 開啟“ 效能 ” 標籤欄 - 按下執行按鈕 」。為了還原使用者的使用場景,我也認真的滾動瀏覽了十幾章。執行的結果:
「 首次效能工具的結果圖 」
這滿屏的原諒色,看到這張圖,答應我請第一眼就確認是 “渲染” 的問題好嗎?請立馬註釋 CSS 一探究竟好嗎?但我並沒有做到。
繼續觀察,發現效能工具中有2個子標籤欄: 時間線、JavaScript 呼叫堆疊。呼叫堆疊裡很清楚的指明瞭 CPU 佔用率最高的2項(下圖),果然出自於 Google 廣告的 osd.js 指令碼。
「 首次效能工具結果圖之 JavaScript 呼叫堆疊 」
不能慌,證據還不夠確鑿。Edge 的效能工具其實可以精確定位到 JS 中的問題程式碼片段,在紅框區域點選滑鼠右鍵,也可以定位到問題原始碼,這一點 very good!感覺離事實的真相更近了一步。
「 追溯程式碼問題細節圖 」
「 問題程式碼片段1 」
「 問題程式碼片段2 」
顯而易見,Google osd.js 指令碼設定了計時器,不停地獲取 HTML 節點的 clientWidth 和 clientHeight,所以禍根源於此?
clientHeight、clientWidth 的一些隱患
Facebook 的工程師 Stoyan 在他的部落格中曾提過, 瀏覽器是聰明的,為了節約資源、不頻繁觸發 Repaint(重繪)、Reflow(重排),它會將一系列指令碼中需要執行的 UI 改動放在一個佇列中一起執行,而不是每個改動都單獨執行從而引發無數次的 Repaint、Reflow。BUT!有一些操作會打亂它原有的 plan 和秩序:
- offsetWidth,offsetHeight,offsetTop,offsetLeft
- scrollTop, scrollLeft,scrollWidth, scrollHeight
- clientlTop, clientLeft,clientWidth, clientHeight
- getComputedStyle(),or currentStyle in IE
瀏覽器對於這些操作,會“ 非常及時地 ”給予最準確實時的答案,因此會觸發 Reflow,例如重新計算樣式、更新圖層等。要知道 Reflow 相比 Repaint 對渲染的影響更大、損耗更多。
demo time - 獲取 clientWidth 是否會觸發不必要的渲染?
功能簡述:
- 頁面 onload 後設定計時器每隔 4ms(瀏覽器預設最小時間間隔) 獲取 HTML 節點的clientWidth。
- 點選 click me 按鈕水平位移 box1 到最右側。
- 點選 no timer 按鈕清除計時器,同時水平位移 box1 到原位。
「 demo 動圖 」
過程 | 結果 |
階段一:before “ click me ” | 未觸發 Reflow 等渲染事件 |
階段二:after “ click me ” | 觸發了額外的渲染(recalculate style、update layers) |
階段三:after “ no timer ” | 沒有觸發 Reflow 等渲染事件 |
Chrome Performance 工具則更直觀地描述了表格中的結果(紅框中處於階段二,清楚表現了額外的渲染):
為了更突出差異,我將水平移動的 div 數量增加到了 20 個,Recalculate Style 的資料相差了 70ms:
「 渲染資料的差異:動畫期間獲取 clientWidth(上),動畫期間不做任何操作(下) 」
demo 的結果驗證了在 Chrome 瀏覽器中,動畫期間獲取 clientWidth / clientHeight 確實會引發瀏覽器的 Reflow,隨著動畫複雜度的增加以及元素的增加,該影響只會更大,進而增加了不必要的渲染消耗。
首次嘗試 - 刪除 Google 廣告
既然閱讀頁 CPU 飆升極有可能是因為 Google 的 osd.js 不斷獲取 HTML 節點的 clientWidth / clientHeight 意外觸發了多次 Reflow ,那麼解決問題的第一步即是刪除 Google 廣告。我利用 fiddler 代理 JS 指令碼到本地,刪除了廣告相關的程式碼。
但其結果盡和我的猜測完全不同,刪除廣告並沒有使頁面效能變得更好,有點沒有頭緒...真的不是 Google 廣告的問題?
再次試探 - Google 廣告在其他站點有無類似問題
繼續用 Edge 效能工具對一類似網站進行測試,因為這個網站同樣使用了 Google 廣告,並且相比於 Webnovel 閱讀頁首次初始化1枚廣告,它的首頁盡然同時載入了 4 枚廣告。然而結果卻沒有渲染上的問題(如下圖),網站 CPU 利用率一欄甚至完全沒有飈綠的痕跡。
推翻設想,轉換方向到 CSS
太過堅信設想反而偏離正軌?前期我太堅信是 Google 廣告指令碼的問題,既然以上 2 次的嘗試都證明不是 Google 廣告指令碼的鍋,那麼是時候轉換方向了。
曾經寫的「 checkbox 美化引發的蝴蝶效應 」帶給了我一些靈感:CSS 在渲染效能上有著相比 JS 更不容忽視的影響力。結合前面提到的 demo 結論,不由得猜測也許閱讀頁的效能問題是由於某些 CSS 屬性或動畫與 Google 廣告指令碼中的 clientHeight、clientWidth 產生了相互作用,引發了額外的渲染。此時嘗試註釋所有樣式,最終綠色竟然消失了,這說明極有可能是 CSS 的鍋。
「 CPU 佔用率中消失的渲染 」
罪魁禍首是 visibility : hidden ?
繼續從效能工具中尋找更精確的答案,意外發現 Edge 效能工具裡的 “ 時間線 ” 標籤欄裡可以捕捉到很多有用的資訊,甚至可以找到樣式相關的條目?展開即可定位到目前引發渲染的 DOM,這一點太讚了!
這對問題的定位有了很大的幫助,因為我很清楚地看到了有問題的 DOM ,也就是我們寫的 loading 元件。
Webnovel 閱讀頁確實存在幾枚隱藏的 loading,分別存在於章評彈窗中、目錄彈窗中、各章節內容之間。
「 章評彈窗、目錄彈窗 」
「 章節之間的 loading 」
隱藏 loading ,是為了簡化邏輯,將其“ 埋伏隱藏 ” 在各自需要的地方,在獲取所需的非同步資料之前,可以第一時間將 loading 展示給使用者,獲取到需要的資料後再將 loading 隱藏,這樣的互動邏輯是合理的,那麼隱藏的 loading 為什麼會對渲染產生影響?Webnovel 閱讀頁彈窗中的 loading 是用 visibility: hidden 來隱藏的,這樣的方式不對?
visibility: hidden 有利有弊
看不見 loading 的蹤跡,它卻依然能對渲染產生這麼大的影響?你認為正確地隱藏了其中的 loading,而 loading 卻實際地存在於文件流中,它的每一個動畫,都會影響同在文件流中的其他節點。
- visibility: hidden // DOM 的渲染不會忽略其所有節點
- display: none // DOM 的渲染忽略其所有節點
如此來看,效能更優的應該是 display: none,不在文件流中就不會影響其他的元素。但每個被創造出來的 CSS 屬性必然有創造它的道理,如果 display: none 是最推薦的方式,為什麼還會有 visibility : hidden 的存在?任何事物都存在兩面性,例如以下場景中,visibility: hidden 竟協助某大神解決了效能問題。
Google 的工程師 Jake Archibald 在他的部落格「Solving rendering performance puzzles」中,用一個有趣的 SVG 動畫驗證了 Layout (佈局)在網頁效能中舉足輕重的地位。隨著動畫中 SVG 文字的不斷變化,動畫的幀率由原來最優的 60fps 最終降低到不足於 10fps。Chrome 的 Performance 工具揭示了其主要因素是產生了大量的 Layout 消耗(稱為 Layout Thrashing or Reflow )。
此時 Jake 利用 visibility: hidden 解決了 Layout 消耗過多的問題,他提到,visibility: hidden 的元素,因為其在文件流中佔據一席之地,因而將其設定為 visibility: visible 並不會產生 Layout 上的消耗,只會產生額外的 paint,藉助此想法,他事先將 SVG 中的所有字元分別放置於 <textpath> 節點中的子節點 <tspan> 中,並利用 visibility: hidden 進行隱藏:
「 DOM 中隱藏的 SVG 文字 」
隨後逐次將 <tspan> 的 visibility 屬性值變更為 visible ,字元就依次展示了出來,對比下,他原來的做法就真的是非常粗暴了,即:將所有字元存於單獨的一個節點中,每次擷取固定長度的文字設定為 <textPath> 的值。大量的重置 textpath 節點值最終引發了瀏覽器每個字元的重新佈局( layout )。
因此並不能籠統地下定論 visibility: hidden 不如、或優於 display: none ,只能說在不同的場景下他們都有各自的優勢。
問題的修復
顯然,loading 動畫用 visibility: hidden 進行隱藏確實產生了很多額外的渲染消耗。修復這個問題的方案很多,例如,可以將 loading 的隱藏方式改為 display: none,或者是在開啟彈窗的時候將 loading 實時新增到 DOM 中,最終為避免改動彈窗元件的 CSS 產生的全域性影響,我們在開啟彈窗的時候新增了 loading 的 DOM 節點,關閉彈窗時則將其移除。一週後使用者的反饋也證明了此方案是有效的(下圖)。
「 使用者的反饋 」
新方案的嘗試
Edge 瀏覽器渲染上和 Chrome 有什麼不同 ?
Webnovel 閱讀頁的問題雖然得以解決,但可能我們都會感到疑惑,為什麼這樣的效能問題在 Chrome 瀏覽器上並沒有發生,或者說表現不明顯?這裡我藉助「 visibility: hidden 有利有弊」一節中提到的 Jake 的 SVG 動畫做了一波測試。效能工具簡單探測一下,發現未優化前的動畫其效能和 Chrome 一樣無法直視:
「 Edge 瀏覽器效能結果 」
優化後的動畫其表現和 Chrome 也相差甚遠,直觀上的對比如動圖所示,Edge 瀏覽器(第二張圖)上的動畫比 Chrome(第一張圖) 緩慢許多。
「 上 Chrome, 下 Edge 」
Chrome Performance 工具中可以證明 Jake 的優化策略確實將 Layout 產生的消耗降低了很多,由原來的 TOP3 變為最後一位 :
「 優化前,優化後 」
但 Edge 效能工具的結果甚至比不上優化之前,渲染上未降低,並且出現了很多 JS 上的 CPU 衝擊(圖中橘色區域):
「 優化前 」
「 優化後 」
優化後渲染層面的開銷甚至大於優化前的開銷(在動畫執行差不多時長的情況下):
「 優化前、後 UI執行緒資料 」
在此 demo 的表現上,Edge 瀏覽器對 SVG 動畫的渲染情況與 Chrome 截然相反,更重要的是,Chrome 對於 Layout(佈局)和 Paint (重繪) 在渲染效能方面的 “ 權衡 ”似乎和 Edge 是不一樣的,降低大量的 Layout 開銷確實可以使 Chrome 中的動畫更為流暢,但這在 Edge 中卻行不通,甚至表現相反。
解決問題的新思路
Chrome 和 Edge 瀏覽器在 SVG 動畫上的表現差異提供了我一些新的思路,再次回到 Webnovel 閱讀頁,我們的頁面中確實使用了數量不少的 SVG,我嘗試著將 SVG 的 display: inline-block 改為 display: none 後,CPU 竟然迅速降了下來,效能上也得到了緩解(如下圖),感到非常驚喜。
「 註釋 SVG 樣式之前 」
「 註釋 SVG 樣式之後 」
但目前效能問題已然解決,並且 Webnovel 全站都使用的了 SVG ,此時若全域性替換掉 SVG 未免有些粗暴,加之 Jake 的 demo 還不足以說明過多的 SVG 影響了頁面的效能,因此該方案並沒有實施,但這確實可以作為 Webnovel 閱讀頁效能問題的另一個突破點,後續還需要花更多的時間來探索這其中的奧祕。
一些碎碎念
當然,問題的修復總是需要總結一下:
- visibility: hidden 是把雙刃劍。
- clientWidth、clientHeight 等操作在動畫過程中不建議使用。
- 如果效能工具展示了大面積飆綠現象,請第一時間註釋你的 CSS 再一探究竟
- 無法定位問題的時候可以對比其他類似的網站得到啟發,從而進一步定位
- Edge 瀏覽器渲染層面和 Chrome 略有不同,這是值得重視的。
效能優化本就是積極嘗試和敢於試錯的過程,正所謂積跬步以至千里,點滴的積累和探索不是為了產出 “ 最佳效能優化方案 n 條 ” ,而是為了能更好地提煉定位問題的思路、擴充套件認知範圍、同時形成一種對待效能瓶頸不懼怕的態度,希望我們都可以做到微笑面對效能瓶頸。
參考連結
- developers.google.com/speed/docs/…「google 開發者文件 - 瀏覽器的重排行為」
- jakearchibald.com/2013/solvin…「 Jake Archibald - 解決渲染效能問題」
- developers.google.com/web/tools/c…「google 開發者文件 - 利用渲染工具分析執行時的效能」
- www.w3cplus.com/animation/a…「 CSS Animation 效能優化」