無線性能優化:頁面可見時間與異步加載

jopen 8年前發布 | 12K 次閱讀 HTML 性能優化 前端技術

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

頁面可見時間

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

  • 解析 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 地址

    function injectWrite(src){
      document.write('<script src="' + src + '"></sc' + 'ript>');
    }
  • D. getScript: 形如以下,也是 KISSY 內部的 getScript 函數的簡易實現: DEMO 地址

    <script>
     var script = document.createElement('script');
     script.src = "http://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 渲染完成,再執行指定腳本。

<script defer src="xx.js"></script>
  • 瀏覽器開始解析 HTML 網頁
  • 解析過程中,發現帶有 defer 屬性的 script 標簽
  • 瀏覽器繼續往下解析 HTML 網頁,解析完就渲染到頁面上,同時并行下載 script 標簽中的外部腳本
  • 瀏覽器完成解析 HTML 網頁,此時再執行下載的腳本,完成后觸發 DOMContentLoaded

下載的腳本文件在 DOMContentLoaded 事件觸發前執行(即剛剛讀取完\<\/html>標簽),而且可以保證執行順序就是它們在頁面上出現的順序。所以 添加 defer 屬性后,domReady 的時間并沒有提前,但它可以讓頁面更快顯示出來。

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

async: 異步

HTML 5 規范,其作用是,使用另一個進程下載腳本,下載時不會阻塞渲染,并且下載完成后立刻執行。

<script async src="yy.js"></script>
  • 瀏覽器開始解析 HTML 網頁
  • 解析過程中,發現帶有 async 屬性的 script 標簽
  • 瀏覽器繼續往下解析 HTML 網頁,解析完先顯示頁面并觸發 DOMContentLoaded,同時并行下載 script 標簽中的外部腳本
  • 腳本下載完成,瀏覽器暫停解析 HTML 網頁,開始執行下載的腳本
  • 腳本執行完畢,瀏覽器恢復解析 HTML 網頁

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

如何選擇 async 和 defer

  • defer 可以保證執行順序, async 不行【注:<=IE 9 defer 執行順序有 bug,但可以 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

  <!-- BAD -->
<script src="http://g.alicdn.com/large.js"></script>

<!-- GOOD -->
<script>
 var script = document.createElement('script');
 script.src = "http://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

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

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

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

<!-- 現代瀏覽器用 'async', ie9-用 'defer' -->
<script src="http://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 的時間。

來自: http://taobaofed.org/blog/2016/01/20/mobile-wpo-pageshow-async/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!