無線效能優化:頁面可見時間與非同步載入

發表於2016-01-20

無線效能優化:頁面可見時間與非同步載入

如何讓頁面儘可能早地渲染頁面,頁面更早可見,讓白屏時間更短,尤其是無線環境下,一直是效能優化的話題。

頁面可見時間

頁面可見要經歷以下過程:

  • 解析 HTML 為 DOM,解析 CSS 為 CSSOM(CSS Object Model)
  • 將 DOM 和 CSSOM 合成一棵渲染樹(render tree
  • 完成渲染樹的佈局(layout)
  • 將渲染樹繪製到螢幕

layout

由於 JS 可能隨時會改變 DOMCSSOM,當頁面中有大量的 JS 想立刻執行時,瀏覽器下載並執行,直到完成 CSSOM 下載與構建,而在我們等待時,DOM 構建同樣被阻塞。為了 JS 不阻塞 DOM 和 CSSDOM 的構建,不影響首屏可見的時間,測試幾種 JS 載入策略對頁面可見的影響:

幾種非同步載入方式測試

  • A. head script: 即普通的將 JS 放在 head 中或放在 body 中間:DEMO 地址
  • B. bottom script: 即常規的優化策略,JS 放body的底部:DEMO 地址
  • C. document.write: 以前 PC 優化少用的一種非同步載入 JS 的策略:DEMO 地址
  • D. getScript: 形如以下,也是 KISSY 內部的getScript函式的簡易實現:DEMO 地址
  • E. 加 async 屬性:DEMO 地址
  • F. 加 defer 屬性:DEMO 地址
  • G. 同時加 async defer 屬性:DEMO 地址

測試結果

以下提到的 domReadyDOMContentLoaded 事件。

A (head script) B (bottom script) C (document.write) D (getScript) E (async) F (defer) G (async + defer)
1 PC Chrome 頁面白屏長、domReady:5902.545、onLoad:5931.48 頁面先顯示、domReady:5805.21、onLoad:5838.255 頁面先顯示、domReady:5917.95、onLoad:5949.30 頁面先顯示、domReady:244.41、onLoad:5857.645 頁面先顯示、domReady:567.01、onLoad:5709.33 頁面先顯示、domReady:5812.12、onLoad:5845.6 頁面先顯示、domReady:576.12、onLoad:5743.79
2 iOS Safari 頁面白屏長、domReady:6130、onLoad:6268.41 頁面白屏長、domReady:5175.80、onLoad:5182.75 頁面白屏長、domReady:5617.645、onLoad:5622.115 502s 白屏然後頁面顯示最後變更 load finish 時間、domReady:502.71、onLoad:6032.95 508s 白屏然後頁面顯示最後變更 load finish time domReady:508.95、onLoad:5538.135 頁面白屏長、domReady:5178.98、onLoad:5193.58 556s 白屏然後頁面顯示最後變更 load finish 時間、domReady:556、onLoad:5171.95
3 iOS 手淘 WebView 頁面白屏長、頁面出現 loading 消失、domReady: 5291.29、onLoad:5292.78 頁面白屏長、頁面未跳轉 loading 消失、domReady: 5123.46、onLoad:5127.85 頁面白屏長、頁面未跳轉 loading 消失、domReady: 5074.86、onLoad:5079.875 頁面可見快、loading 消失快在 domReady 稍後、domReady:14.06、load finish:5141.735 頁面可見快、loading 消失快在 domReady 稍後、domReady:13.89、load finish:5157.15 頁面白屏長、loading 先消失再出現頁面、domReady: 5132.395、onLoad:5137.52 頁面可見快、然後 loading 消失、domReady:13.49、load finish:5124.08
4 Android browser 頁面白屏長、domReady: 5097.29、onLoad:5100.37 頁面白屏長、domReady: 5177.48、onLoad:5193.66 頁面白屏長、domReady: 5125.96、onLoad:5165.06 頁面可見快、等 5s 後更新 load finish 時間 domReady:463.33、load finish:5092.90 頁面可見快、等 5s 後更新 load finish 時間 domReady:39.34、load finish:5136.55 頁面白屏長、domReady: 5092.45、onLoad:5119.81 頁面可見快、等 5s 後更新 load finish 時間 domReady:50.49、load finish:5507.668
5 Android 手淘 WebView 白屏時間長、一直 loading 直接頁面可見、domReady:5058.91、onLoad:5073.81 頁面立即可見、loading 消失快、等 5s 後更新 domReady 時間和 load 時間 domReady:4176.34、onLoad:4209.50 頁面立即可見、loading 消失快、domReady:6011.18、onLoad:6031.93 頁面可見快、loading 之後消失、等 5s 後更新 load finish 時間 domReady:36.31、load finish:5081.76 頁面可見快、loading 隨後消失、等 5s 後更新 load finish 時間 domReady:25.11、load finish:5113.81 頁面可見快、loading 隨後消失、等 5s 後更新 domReady 時間和 load 時間 domReady:5213.11、load finish:5312.19 頁面可見快、loading 隨後消失、等 5s 後更新 load finish 時間 domReady:89.67、load finish:5589.95

從以上測試結果可以看出以下結論:

  • 橫向看, iOS Safari 和 Android browser 的在頁面可見、domReady、onLoad 的時間表現一致。
  • 縱向看,bottom script、document.write 和 defer 三列,可知 document.write 和 defer 無任何非同步效果,可見時間、domReady、onLoad 的觸發時間和 bottom script 的情況一致。
  • 縱向看,async + defer 聯合用和 async 的表現一致,故合併為 async。
  • 縱向看,script 放頁頭(head script)和 script 放 body 底部(bottom script)。iOS Safari 、Android browser 和 iOS WebView 表現一致,即使 script 放在 body 的底部也無濟於事,頁面白屏時間長,要等到 domReady 5s 多後結束才顯示頁面;唯獨 Android WebView 的表現和 PC 的 Chrome 一致。
  • 單純看手淘 WebView 容器中 loading 消失的時間,這個時間點 iOS 和 Android 的表現一致,即都是在 UIWebView 的 didFinishLoad 事件觸發時消失。這個事件的觸發可能在 domReady 之前(如:A3、B3),也可能在 domReady 之後(如:D3、E3);這個事件觸發和 JS 中的 onLoad 觸發時機也沒有必然的聯絡,可能在 onLoad 之前(如:D3、E3)也可能在 onLoad 幾乎同時(如:A5)。 didiFinishLoad 到底是什麼時機觸發的呢,詳見下章。
  • 頁面可見時間,getScript 方式和 async 方式頁面可見都非常快,domReady 的時間觸發得也非常快,客戶端的 loading 在 domReady 稍後即消失。原因是因為最後耗時的 JS 請求非同步化了,沒有阻塞瀏覽器的 DOM + CSSOM 構建,頁面渲染完成就立刻可見了。整體看,如果 domReady 的時間快,則頁面可見快;反之如果頁面可見快,domReady 的時間不一定快,如 B5、B1、C1、C5、F1、F5。如果非同步化耗時長的 JS,domReady 和 onLoad 的時間差距是很大的,不做任何處理 onLoad 的時間 domReady 的時間差 30ms 左右。所以在非同步化的前提下,可以用 domReady 的時間作為頁面可見的時間。

didFinishLoad 到底什麼時候觸發

didFinishLoad 是 native 定義的事件,該事件觸發時手淘 loading 菊花消失,並且 windvane 中的發出請求不再收集,也就是 native 統計出的 pageLoad 時間。在使用者資料平臺看到的瀑布流請求,就是在 didFinishLoad 觸發前收集到的所有請求。

經過上方測試,客戶端的 didFinisheLoad 事件的觸發和 JS 中的 domReady(DOMContentLoaded)和 onLoad 觸發沒有任何關聯。可能在 domReady 之前或之後,也可能在 onLoad 之前或之後。

那它到底是什麼時候觸發呢? iOS 官方文件 是 Sent after a web view finishes loading a frame。 結合收集的使用者請求和測試,didFinishLoad 是在連續發起的請求結束之後觸發,監聽一段時間內無請求則觸發。

所以經常會看到 data_sufei 這個 JS 檔案,在有些使用者的瀑布流裡面有,在有些使用者的又沒有。原因是這個 JS 是 aplus_wap.js 故意 setTimeout 1s 後發出的,如果頁面在 1s 前所有的請求都發完了則觸發 didFinishLoad,後面的 data_sufei.js 的時間就不算到 pageLoad 的時間;反之如果接近 1s 頁面還有圖片等請求還在發,則 data_sufei.js 的時間也會被算到裡面。

因此在 JS 中用 setTimeout 來延遲傳送請求也有可能會影響 didFinishLoad 的時間,建議 setTimeout 的時間設定得更長一點,如 3s。

async 和 defer

script 標籤上可以新增 defer 和 async 屬性來優化此 script 的下載和執行。

defer :延遲

HTML 4.0 規範,其作用是,告訴瀏覽器,等到 DOM+CSSOM 渲染完成,再執行指定指令碼。

  • 瀏覽器開始解析 HTML 網頁
  • 解析過程中,發現帶有 defer 屬性的 script 標籤
  • 瀏覽器繼續往下解析 HTML 網頁,解析完就渲染到頁面上,同時並行下載 script 標籤中的外部指令碼
  • 瀏覽器完成解析 HTML 網頁,此時再執行下載的指令碼,完成後觸發 DOMContentLoaded

下載的指令碼檔案在 DOMContentLoaded 事件觸發前執行(即剛剛讀取完標籤),而且可以保證執行順序就是它們在頁面上出現的順序。所以 新增 defer 屬性後,domReady 的時間並沒有提前,但它可以讓頁面更快顯示出來。

將放在頁面上方的 script 加 defer,在 PC Chrome 下其效果相當於 把這個 script 放在底部,頁面會先顯示。 但對 iOS Safari 和 iOS WebView 加 defer 和 script 放底部一樣都是長時間白屏。

async: 非同步

HTML 5 規範,其作用是,使用另一個程式下載指令碼,下載時不會阻塞渲染,並且下載完成後立刻執行。

  • 瀏覽器開始解析 HTML 網頁
  • 解析過程中,發現帶有 async 屬性的 script 標籤
  • 瀏覽器繼續往下解析 HTML 網頁,解析完先顯示頁面並觸發 DOMContentLoaded,同時並行下載 script 標籤中的外部指令碼
  • 指令碼下載完成,瀏覽器暫停解析 HTML 網頁,開始執行下載的指令碼
  • 指令碼執行完畢,瀏覽器恢復解析 HTML 網頁

async 屬性可以保證指令碼下載的同時,瀏覽器繼續渲染。但是 async 無法保證指令碼的執行順序。哪個指令碼先下載結束,就先執行那個指令碼。

如何選擇 async 和 defer

  • defer 可以保證執行順序,async 不行【注:hack】
  • async 可以提前觸發 domReadydefer 不行【注:Firefox 的 defer 也可以提前觸發 domready
  • defer 在 iOS 和部分 Android 下依然阻塞渲染,白屏時間長。
  • 當 script 同時加 asyncdefer 屬性時,後者不起作用,瀏覽器行為由 async 屬性決定。
  • asyncdefer 的相容性不一致,好在 asyncdefer 無線端基本都支援,async 不支援 IE 9-。
    async 相容性 defer 相容性

script inject 和 async

我們通常用這種 inject script 的方式來非同步載入檔案,特別是以前 Sea.jsKISSY 的盛行時,出現大量使用$.use 來載入頁面入口檔案。這種方式和 async 的一樣都能非同步化 JS,不阻塞頁面渲染。但真的是最快的嗎?

一個常見的頁面如下:一個 CSS,兩個非同步的 JS

JS 使用 script inject 的方式測試結果如下,DEMO

JS 使用 async 的方式測試結果如下, DEMO

對比結果發現,通過 的方式的 JS 可以和 CSS 併發下載,這樣整個頁面 load 時間變得更短,JS 更快執行完,這樣頁面的互動或資料等可以更快更新。為什麼呢?因為瀏覽器有類似 ‘preload scanner’ 的功能,在 HTML 解析時就可以提前併發去下載 JS 檔案,如果把 JS 的檔案隱藏在 JS 邏輯中,瀏覽器就沒這麼智慧發現了。

也許大家會說,現在 CSS/JS 都預載入到客戶端了,怎麼載入不重要。但頁面有可能分享出去也有可能執行在瀏覽器中,也有可能預載入失效。

綜合上面 async 和 defer,推薦以下用法。

其實現在無線站點 aplus.js 可以完全用這種方式引入,既不會阻塞 DOMCSSOM,也不會延長整個頁面 onLoad 時間,而不是原來的 PC 上的 script inject方式。

如果 aplus.js 在 PC 上這麼用,IE 8/IE 9 應用的是 defer 屬性,不會阻塞頁面渲染,但是這個 JS 需要執行完後才觸發 domReady(DOMContentLoaded)事件,故在 IE 8/IE 9 下可能會影響 domReady 的時間。

最後建議

  • 業務 JS 儘量非同步,放 body 底部的 JS 在 iOS 上和部分 Android 是無效的,依然會阻塞首屏渲染。
  • 非同步的方式儘可能原生用async,容器(瀏覽器、webview 等)級別自帶優化,不要通過 JS 去模擬實現,如 getScript/ajax/KISSY.use/$.use 等。
  • 有順序依賴關係的 JS 可以加 defer,不改變執行順序,相當於放到頁面底部,如 TMS head 中一時無法挪動位置的類庫等。

參考資料

相關文章