本文做者:任家樂css
原創聲明:本文爲閱文前端團隊 YFE 成員出品,請尊重原創,轉載請聯繫公衆號 (id: yuewen_YFE) 獲取受權,並註明做者、出處和連接。前端
「聽說亞馬遜雨林的一隻蝴蝶偶爾扇動幾下翅膀,能夠在兩週之後引發美國得克薩斯州的一場龍捲風」曾經起點的訂閱頁也經歷了相似的龍捲風襲擊事件,那隻蝴蝶即是咱們加工過的 Checkbox(複選框)。咱們對無公害的 Checkbox 究竟作了什麼,才引起了這場性能風暴?c++
起點訂閱頁是龍捲風襲擊的現場,也是本文不得不小刀的對象。還記得當時「望向遠方的回憶臉」開發、聯調一切順利,彷彿能立立刻線 「驚喜」。但提測後,畫風突變,測試同窗扔過來幾個重磅炸彈,訂閱頁章節數目超過 3000 章時,會出現反應遲鈍、頁面假死、瀏覽器奔潰等現象。看到這幾個 Bug 內心一沉,3000 章以上的超長做品場景在聯調過程當中確實被忽略了,無論怎樣,趕忙搬磚 「奔潰臉」web
在此順便提下訂閱頁的主打功能:陳列書籍全部收費章節,經過勾選複選框來進行單章、單卷、多卷訂閱,在發起訂閱操做時,也提供了餘額支付、快捷支付、以及風險控制等功能。算法
抓緊時間體驗了下起點近 7000 章的「帶着農場混異界)」的訂閱頁,嘴裏唸叨着「誰會看這麼長的文」,內心卻很誠實的總結出 3 個明顯的問題:後端
章節數 | Loading 耗時 (s) |
---|---|
3000 | 6.519 |
5000 | 14.732 |
7000 | 24.753 |
▲ 打印時間戳取 10 次實驗平均數獲得瀏覽器
問題很嚴重,性能優化勢在必行!緩存
7000 章數據 Loading 24s 「 excuse me? 」按照老習慣,直奔代碼循環體找緣由,在此以前先講一下訂閱頁的渲染邏輯:性能優化
抓了下請求數據包,250kb 耗時 493ms ,可見瓶頸不在數據請求,因而用本地數據對步驟 二、三、4 作了性能測試,結果以下:網絡
章節數 | 渲染 EJS 模板 | DOM插入 EJS 模板 | 初始化 Checkbox 實例 |
---|---|---|---|
3000 | 92ms | 75ms | 6345ms |
5000 | 149ms | 133ms | 13948ms |
7000 | 278ms | 301ms | 23866ms |
▲模擬次數:10
以上數據代表,實例化 Checkbox UI 組件是耗時最多的環節。因爲數據是由書籍卷構成的基本結構,要實例化單個 Checkbox ,同時考慮到對整卷的操做需求,必先循環卷,再循環卷中的全部章節,這就有了雙層循環。另外根據訂閱頁的視覺要求,兼容到 IE7 ,則不能使用原生的 Checkbox 組件,因此選擇了兼容性更好的 Checkbox UI 組件。扒一下代碼:
var chapter;
for (var v = 0; v < volumeNum; v++) {
//do something with book volume
for(var c = 0; c < volChapters.length; c++){
//初始化章節相關checkbox
chapter = new Checkbox({
selector: '#' + volChapters[c].id
});
//do something with book chapter
}
//do something after init checkbox
}
複製代碼
打印時間戳發現 Checkbox 組件的初始化比較耗時,嘗試去除初始化 Checkbox UI 組件的邏輯,改用咱們爲業務定製的,採用 CSS 漸進增減的 UI 組件 LULU UI 代替 Checkbox 的美化組件,狀況果真有所好轉,結果以下:
▲數據請求成功的回調總時間相比以前的 24s ,Loading 時間上已驟減至 1.925s(數據請求時長+上圖成功邏輯時長),考慮到網絡環境的不一樣,真實狀況下應大於 2s 。同時拖動滾動條卡頓的狀況有所緩解,但依然存在。
Let’s go on!
嘗試勾選「選擇所有章節」複選框,響應依舊延遲,渲染耗時居然達到了 5840.9ms !頁面卡頓也依舊明顯 「奔潰臉」,繼續填坑!
▲勾選「選擇所有章節」複選框渲染耗時勾選「選擇所有章節」複選框只作了一個事情,遍歷全部 Checkbox 改變 UI 狀態並獲取屬性。雖然 LULU UI 是用 CSS3 屬性來繪製 Checkbox ,也不至於會產生這麼嚴重的性能問題啊?爲了排除 CSS 的嫌疑,對比了 IE8 和 Chrome ,結果發現 IE8 比 Chrome 在勾選 Checkbox 的反饋上快不少!IE8 不支持一些 CSS3 屬性,所以優雅降級採用了背景圖實現 Checkbox UI 。按道理來講,性能問題多數是腳本搞的鬼,難道 CSS 也對性能有這麼大的負面影響?
id(#myid)
class(.myclass)
tag(div)
adjacent sibling(h1 + p)
child(ul > li)
descendent(li a)
universal(*)
attribute(a[rel=」external」])
pseudo-class and pseudo element(a:hover li:first)
複製代碼
▲CSS 選擇器效率權威排序
LULU UI 在繪製 Checkbox 時,使用了例如描邊、陰影、過渡等 CSS3 高級屬性,其勾選、禁用等樣式也是經過效率相對低的僞類選擇器和相鄰兄弟選擇器實現的。3000 個以上的 Checkbox 意味着 3000 次以上的 CSS3 選擇器篩選,由此猜測多是選擇器帶來的性能問題。
.ui-checkbox{
…
box-sizing: border-box;
box-shadow: inset 0 1px, inset 1px 0, inset -1px 0, inset 0 -1px;
-webkit-transition: color .2s, background-color .1s;
transition: color .2s, background-color .1s;
…
}
:checked + .ui-checkbox, :checked + .ui-checkbox: hover{
color: $borderFocus;
background-color: $backgroundRed;
}
複製代碼
▲ LULU UI 複選框樣式代碼
爲了確認這是 CSS3 選擇器帶來的渲染問題,嘗試改成類選擇器來從新定義 Checkbox 的樣式,代碼以下。
.ui-checkbox{
…
background: url(….);
….
}
.ui-checkbox-checked {
background-position: 0 -40px;
}
複製代碼
▲LULU UI 複選框優化後樣式代碼
結果勾選「選擇所有章節」卡頓消失了,幾乎是即時響應。
順便看了下性能數據,渲染只需 1777.7ms。
至此,問題 2 的 Checkbox 勾選產生的性能問題已經獲得解決,但在頁面加載過程當中的卡頓現象依然存在,章節數若是太多,滾動條几乎處於假死狀態。
這並不難理解,頁面上的章節數越多,意味着有更多的 DOM 節點須要一次性渲染,即便僅操做 1 次 DOM 短期內插入如此多的節點,仍是會致使耗時較長。一般作法是使用分頁、下拉加載等經常使用策略來解決這個問題。
▲頁面 Onload 過程上圖能夠看到,頁面 Onload 過程當中,Recalcualte Style(從新計算樣式)的時間達到了 1.08s,這是什麼概念?即便是 QQ 空間這樣集聚圖片、輸入框、操做按鈕的多節點網頁,在 Onload 過程當中運用在 Recalculate Style 上的時間也只有 293.4ms 左右。
▲ QQ 空間 Onload 過程內容分段加載勢在必行,咱們的分段加載策略:數據依舊一次性拉取,前端來進行分段渲染並分段插入 DOM 中。
最優的方案固然是後端改造接口來支持前端屢次拉取章節數據,但時間緊迫,後端改造接口也須要一些時間,所以並無作到請求的分段。前端分段數據結果以下表,Loading 時長差異不大。
章節數 | 分段前 Loading 時長 | 分段後 Loading 時長 |
---|---|---|
3000 | 0.366s | 0.391s |
5000 | 0.712s | 0.699s |
7000 | 0.938s | 0.936s |
▲前端分段數據結果
▲分段加載結果對比上面 4 組圖,Rendering(渲染)和 Painting(重繪)在分段後有所減小,但隨着分段的數量加大,差異逐漸縮小。多是因爲第一次將數據插入 DOM 還伴隨着頁面其餘元素及資源的載入,所以第一次分段效果顯著。但接近 1.5s 的渲染時長,仍是存在問題。
▲分段加載 Checkbox 先後對比理論上來看分段應該是有效的,但實踐來看,分段後勾選「選擇所有章節」在渲染、腳本、重繪方面相對減少,差異卻並不明顯。猜測:其依次插入 DOM 太過連續、沒有間隙,所以使用計時器來進一步優化。
計時器可使 JS 延遲一段時間執行,此事件排在當前線程中事件隊列的最後。這裏咱們可使用計時器的 setTimeout 方法,並設置延遲時間爲 0ms 。雖然咱們設定的時間爲 0ms,但瀏覽器的延遲時間最小爲平均 16ms 。 這 16ms 對用戶來講無感知,但瀏覽器卻能夠趁此機會喘口氣。
「計時器能夠防止在生成大量DOM元素的狀況下瀏覽器hanging(絞死) 。
— Javascript 忍者祕籍
關於 16.66ms 的解釋:
「咱們的目標是保證頁面要有高於每秒 60fps (幀) 的刷新頻率,這和目前大多數顯示器的刷新率相吻合 ( 60Hz )。若是網頁動畫可以作到每秒 60fps(幀),就會跟顯示器同步刷新,達到最佳的視覺效果。這意味着,秒以內進行 60 次從新渲染,每次從新渲染的時間不能超過 16.66 毫秒。」
— From horve‘s article
嘗試在分段加載的基礎上加入計時器的使用,咱們來看看性能數據的對比:
▲Mock 7000章 Loading 時長,左圖不使用計時器,右圖使用計時器
顯然 Rendering( 渲染 )方面有了明顯的降低(降低約 1300ms),數據已接近咱們的指望值( 368ms )!但對於勾選「選擇所有章節」並無太大影響,數據對好比下:
▲勾選 Checkbox,左圖不使用計時器,右圖使用計時器到此前 3 個性能問題都達到了我所指望的結果,但我居然選擇性遺忘了 IE7 瀏覽器 。在 IE7 下,勾選「選擇所有章節」依然響應延遲。是的,目前起點 PC 平臺須要支持到 IE7 及以上。
打印時間戳看了下,發現獲取並更新當前已選章節的總數量、總價格、總字數操做過於消耗時間。這是因爲每次勾選及取消勾選 Checkbox ,都會實時從 DOM 獲取數量相關元素的內容,在渲染性能偏低的 IE7 下,瓶頸會比較明顯。所以咱們將當前總章節數、總價格、總字數、及展現這些數字的元素自己緩存於對象中,再也不實時讀取,看下數據:
章節數 | 優化前(ms) | 優化後(ms) |
---|---|---|
3000 | 864 | 177 |
5000 | 1317 | 169 |
7000 | 1774 | 180 |
▲ 取10次測試數據平均值
以上 4 個體驗問題到這裏已基本解決,如今再來看看近 7000 章的「帶着農場混異界」:
▲ 7000 章的「帶着農場混異界」性能優化的結果已經很完美了,但如今全部用戶每次訪問訂閱頁,不論章節多少必然能看到 Loading ,章節數多還能忍,章節數少的狀況下,仍是給人不友好的感受。畢竟頁面數據直出的加載體驗纔是最佳實踐。
章節數 | 數量(本) | 百分比 |
---|---|---|
大於5000 | 19 | 0.003% |
大於2000 && 小於5000 | 260 | 0.045% |
▲ 書庫數據
數據顯示,章節數大於 2000 的書籍是少數,僅僅只有 200 多本,因少許書籍(佔0.045% )而損耗的加載體驗沒法使人滿意!
爲了讓 99% 以上的用戶更快看到內容,咱們大多數書籍的訂閱頁徹底能夠作到數據直出!所以和後端同窗約定了簡單的標識,章節數小於等於 2000 的時候直出數據,大於 2000 的時候,AJAX 拉取數據後分段插入頁面。到此爲止暴風世界總算平淨了 ^_^。
▲ 無 Loading 體驗爲了作更好的咱們,一些壓縮 AJAX 數據量的小優化也是不可以放棄噠,例如壓縮字段、減小多餘字段等。即便在 5000 章以上的數據量下,這些優化也能聚沙成塔地減小傳輸量。
這段性能風暴已經平息些許時日了,此次分享出來,既是本身作總結,也是協助一樣遇到性能瓶頸的童鞋。其實除此以外,還能想到一些可行的方案能夠繼續提高頁面的性能,例如:使用 requestIdleCallback 來充分利用瀏覽器的空閒時間作一些事情,提高性能;針對節點作 timechunk 的算法來精確規劃每秒鐘建立多少節點;上文中所說的接口分段等等。性能優化永無止境,後續會找機會把更多方案應用到訂閱頁及其餘須要優化的頁面,共勉。
PS:想更多的瞭解LULU UI,能夠訪問此連接:https://l-ui.com/