如何讓頁面儘可能早地渲染頁面,頁面更早可見,讓白屏時間更短,尤其是無線環境下,一直是效能優化的話題。
頁面可見時間
頁面可見要經歷以下過程:
- 解析 HTML 為 DOM,解析 CSS 為 CSSOM(CSS Object Model)
- 將 DOM 和 CSSOM 合成一棵渲染樹(render tree)
- 完成渲染樹的佈局(layout)
- 將渲染樹繪製到螢幕
layout
由於 JS 可能隨時會改變 DOM
和 CSSOM
,當頁面中有大量的 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 地址
123function injectWrite(src){document.write('<script src="' + src + '"></sc' + 'ript>');} - D. getScript: 形如以下,也是 KISSY 內部的
getScript
函式的簡易實現:DEMO 地址
12345<script>var script = document.createElement('script');script.src = "//g.tbcdn.com/xx.js";document.getElementsByTagName('head')[0].appendChild(script);</script> - E. 加
async
屬性:DEMO 地址 - F. 加
defer
屬性:DEMO 地址 - G. 同時加
async
defer
屬性:DEMO 地址
測試結果
以下提到的 domReady
同 DOMContentLoaded
事件。
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 渲染完成,再執行指定指令碼。
1 |
<script defer src="xx.js"></script> |
- 瀏覽器開始解析 HTML 網頁
- 解析過程中,發現帶有 defer 屬性的 script 標籤
- 瀏覽器繼續往下解析 HTML 網頁,解析完就渲染到頁面上,同時並行下載 script 標籤中的外部指令碼
- 瀏覽器完成解析 HTML 網頁,此時再執行下載的指令碼,完成後觸發 DOMContentLoaded
下載的指令碼檔案在 DOMContentLoaded 事件觸發前執行(即剛剛讀取完標籤),而且可以保證執行順序就是它們在頁面上出現的順序。所以 新增 defer 屬性後,domReady 的時間並沒有提前,但它可以讓頁面更快顯示出來。
將放在頁面上方的 script 加 defer,在 PC Chrome 下其效果相當於 把這個 script 放在底部,頁面會先顯示。 但對 iOS Safari 和 iOS WebView 加 defer 和 script 放底部一樣都是長時間白屏。
async: 非同步
HTML 5 規範,其作用是,使用另一個程式下載指令碼,下載時不會阻塞渲染,並且下載完成後立刻執行。
1 |
<script async src="yy.js"></script> |
- 瀏覽器開始解析 HTML 網頁
- 解析過程中,發現帶有 async 屬性的 script 標籤
- 瀏覽器繼續往下解析 HTML 網頁,解析完先顯示頁面並觸發 DOMContentLoaded,同時並行下載 script 標籤中的外部指令碼
- 指令碼下載完成,瀏覽器暫停解析 HTML 網頁,開始執行下載的指令碼
- 指令碼執行完畢,瀏覽器恢復解析 HTML 網頁
async 屬性可以保證指令碼下載的同時,瀏覽器繼續渲染。但是 async 無法保證指令碼的執行順序。哪個指令碼先下載結束,就先執行那個指令碼。
如何選擇 async 和 defer
defer
可以保證執行順序,async
不行【注:hack】async
可以提前觸發domReady
,defer
不行【注:Firefox 的defer
也可以提前觸發domready
】defer
在 iOS 和部分 Android 下依然阻塞渲染,白屏時間長。- 當 script 同時加
async
和defer
屬性時,後者不起作用,瀏覽器行為由async
屬性決定。 async
和defer
的相容性不一致,好在async
和defer
無線端基本都支援,async
不支援 IE 9-。
附 async 相容性 defer 相容性
script inject 和 async
1 2 3 4 5 6 7 8 9 |
<!-- BAD --> <script src="//g.alicdn.com/large.js"></script> <!-- GOOD --> <script> var script = document.createElement('script'); script.src = "//g.alicdn.com/large.js"; document.getElementsByTagName('head')[0].appendChild(script); </script> |
我們通常用這種 inject script 的方式來非同步載入檔案,特別是以前 Sea.js
、KISSY
的盛行時,出現大量使用$.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,推薦以下用法。
1 2 |
<!-- 現代瀏覽器用 'async', ie9-用 'defer' --> <script src="//g.alicdn.com/alilog/mlog/aplus_wap.js" async defer></script> |
其實現在無線站點 aplus.js 可以完全用這種方式引入,既不會阻塞 DOM
和CSSOM
,也不會延長整個頁面 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 中一時無法挪動位置的類庫等。
參考資料
- http://javascript.ruanyifeng.com/bom/engine.html#toc5
- http://www.stevesouders.com/blog/2013/11/16/async-ads-with-html-imports/
- https://www.igvita.com/2014/05/20/script-injected-async-scripts-considered-harmful/
- https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction?hl=zh-cn