導讀:BigPipe是服務器chunked輸出html內容,BigRender是服務器一次性輸出的,究竟淘寶商品詳情頁的BigRender方式是如何效仿Facebook的BigPipe方式的?存放大塊HTML代碼的最佳方式又是什么?文中為您揭曉答案。
內容如下:
對于復雜頁面,為了將用戶關注的內容盡可能快渲染出來,至少有兩種方式:
一、Facebook的BigPipe方式。
先輸出頁面整體布局,然后逐步輸出腳本塊,一邊輸出一邊執行,將內容渲染回頁面布局中。這樣可以讓服務端的運算、網絡傳輸和瀏覽器端的渲染變成并行。BigPipe最主要解決的問題是服務端的運算時間,當服務端的運算時間大于300~500ms時才能體現出優勢。當服務端響應非常快(小于100ms),BigPipe退化為下面要講的BigRender.
二、淘寶商品詳情頁的BigRender方式。
淘寶的商品詳情頁,服務端平均響應時間為52ms,采用BigPipe chunked輸出意義不大。這次優化主要在瀏覽器端。頁面下載完畢后,要經過Tokenization---Tree Construction—Rendering。要讓首屏盡快出來,得給瀏覽器減輕渲染首屏的工作量。可以從兩方面入手:
減少DOM節點數。節點數越少,意味著Tokenization, Rendering等操作耗費的時間越少。(對于典型的淘寶商品詳情頁,經測試發現,每增加一個DOM節點,會導致首屏渲染時間延遲約0.5ms.)
減少腳本執行時間。腳本執行和UI Update共享一個thread,腳本耗的時間越少,UI Update就能越發提前。
減少首屏DOM節點數
對于BigPipe來說,初始輸出的只有頁面布局,DOM節點數不多。首屏的DOM節點數主要取決于首屏腳本塊中,字符串化的html代碼:
- big_pipe.onPageletArrive({ "content": { /* data */ } })
這種方式下,頁面中的DOM節點是逐步增加的。尚未渲染的DOM節點,不會影響TTI區域。
對于BigRender來說,減少DOM節點數的方式有:
和Facebook的BigPipe一樣,調整頁面代碼為頁面布局+腳本塊。BigPipe是服務器chunked輸出html內容,BigRender是服務器一次性輸出,其他都是一樣的。
盡量少調整頁面代碼,但通過某種方式,將首屏不需要的html代碼先存放起來。渲染好首屏后,再將存儲好的html代碼逐步渲染出來。
用js字符串來存放html代碼
最容易想到的一種方式是學習Facebook好榜樣,用js字符串來存放:
- <script>
- var data = "<p>some data</p>...";
- </script>
這種方式對于BigRender來說,并不是很好:
1.由于存放在js字符串變量中,需要對雙引號或單引號轉義。
2.由于script是內嵌的,需要對script ETAGO轉義。
3.服務器端需要將html代碼轉化為一行。(也可以不轉成一行,用續行符來做。)
4.當html代碼中含有script時,需要先去除script中的單行注釋,否則轉化成一行時,會出問題。這一步,看似簡單,實際上很不容易,特別是對于淘寶旺鋪這種有第三方代碼的情況。(移除注釋的方法可以參考:Simple but Safe Comment Removal, 使用正則的方式很難做到0 bug,不用正則的話,需要引入html parser和javascript parser,效率更低。)
把代碼規范做好,把校驗工作做好,再加上預處理和緩存,js字符串的方式也是非常不錯的。但對于淘寶詳情頁來說,目前用js字符串的方式需要做的改動比較多,增加的服務器消耗不少,不是很合適。
我們這次優化的目標是:
1.大幅度減少首屏渲染時間。
2.盡量不改變原有開發習慣。
3.用盡量少的代碼做盡量多的優化。
用注釋來存放html代碼
為了便于獲取注釋內容,添加一層包裹:
- <div id="comment-data"><!--
- html code
- --></div>
這樣,獲取代碼很簡單:
- var htmlCode = document.getElementById('comment-data').
- childNodes[0].nodeValue;
缺點是:
1.服務端,html中的-->要替換為某種特殊標記。(不能簡單轉義為-->)
2.服務端,html中的--也要替換為某種特殊標記。否則在Firefox低版本中存在bug.
3.瀏覽器端,得到html Code后,要將上面的特殊標記替換回原值。
當html code很大時,替換的效率不高。依賴特殊標記的替換理論上也不完美。
還有什么存放方式呢?
HTML的元素類型
HTML元素分為五大類:
顯然,Void elements和Foreign elements不適合用來存放html代碼。
對于Normal elements,里面的<字符會被當做tag open來解析,有一個方式是通過display:none來避免渲染。
- <div style="display:none">
- html code
- </div>
這樣做,減少的只是可見的DOM節點數,DOM總數依舊不變。Tokenization — Tree Construction等操作的耗時并沒減少。
我們將重點放到Raw text elements和RCDATA elements上來。
CDATA,PCDATA和RCDATA
先了解下CDATA(Character Data) 的相關知識點。
在XML中,不包含子元素的元素的內容默認必須是PCDATA(Parsed Character Data):
- <data><p>some text</p></data>
“Parsed”是指<和&字符要轉換成<和&實體字符形式。如果不想寫一大堆&xx;,可以直接標記為CDATA:
- <data><![CDATA[<p>some text</p>]]></data>
這是XML的習慣,很嚴格,但對用戶并不友好。在HTML中,如果要兼容XML,得像如下一樣:
- <script>
- //<![CDATA[
- var t = "<p>";
- //]]>
- </script>
增加的<![CDATA]很無聊。script中本就是CDATA.
為了讓用戶更舒心,讓代碼更自然,HTML將script和style定義為Raw text elements。也就是說,這兩個元素里面的內容是raw text,里面出現的>就表示>字符本身,不會被當作tag open來解析;>也不會根據實體字符來轉義,就表示>字串自身。這就是CDATA.
Raw text elements有一個限制:里面的內容不能有自身的ETAGO標記,也就是說,script里的內容不能含有</script(\s|\\|>),否則就會導致script提前結束:
- <script>
- document.write('<script>alert("O HAI")</script>');
- </script>
上面的代碼會出錯,必須打破</script組合:
- 1 <script>
- 2 // Using string concatenation:
- 3 document.write('<script>alert("heh")<' + '/script>'); // Lame.
- 4 // Using a string literal escape:
- 5 document.write('<script>alert("huh")<\x3Cscript>'); // Lame.
- 6 // Simply escaping the solidus character with a reverse solidus (\):
- 7 document.write('<script>alert("O HAI")<\/script>'); // Awesome!
- 8 </script>
style也類似,不多說。
除了Raw text elements,還有RCDATA elements。我們來看看。
RCDATA(Replaceable Character Data)表示里面可以有&xx;等實體字符,也可以包含<字符而不會被當作tag open來解析。比如:
- 1<textarea><p><</p></textarea>
在RCDATA里,<可替換為<(Replaceable的含義),拿到值(比如textarea.value)后,是無從得知源碼里是否有<等實體字符的。
用script來存放html代碼
回到正題。在Raw text elements里,可以用script來存放數據:
- 1 <script type="text/html" id="script-data">
- 2 <p>some text</p>
- 3 </script>
獲取也很簡單:
- var htmlCode = document.getElementById('script-data').innerHTML;
這個方案比用注釋來存放的方案更好,但依舊存在以下缺點:
服務端,要將script里html中的</script替換為某種特殊標記。
瀏覽器端,得到htmlCode后,要將上面的特殊標記替換回原值。
注意:特殊標記不能是<\/script,因為有可能存在以下代碼:
- 1 <script type="text/html" id="script-data">
- 2 <script>
- 3 var str = '<\/script>';
- 4 <\/script>
- 5 </script>
這樣替換回原值時,會誤傷str字符串。
用textarea來存放html代碼
textarea中的內容會按照RCDATA規則來解析:
遇到&時,會盡可能得到實體字符。
遇到</textarea(\s|\\|>)時,會結束解析。
其他都直接作為textarea的內容。
- 1 <textarea id="area-data">
- 2 <p>some text</p>
- 3 </textarea>
獲取非常簡單:
- 1 var htmlCode = document.getElementById('area-data').value;
缺點:
服務端,要將html中的&轉義成&
服務端,要打破ETAGO,將</textarea轉義成</textarea
優點很明顯,在瀏覽器端,只需通過textarea.value取值即可,無需進行任何轉義替換操作。
并且理論上不會出現任何bug.
存放大塊HTML代碼的最佳方式
經過上面的分析,結果已經很明顯,用RCDATA elements來存放數據是最妥當的。title元素明顯不合適,因此最后的選擇就剩下一個了:textarea并且從語義上講,用 text area來存放html text也說得過去。
回到首屏渲染優化
可以根據實際情況,將頁面劃分成幾大區域。非首屏區域,簡單轉義后,直接用textarea包裹起來。這樣,DOM數立刻就減少了。瀏覽器在拿到html代碼時,首次 Tokenization — Tree Construction的速度就會大大加快。
完整的優化,還需要:
給瀏覽器合理的喘息(UI Update)時間,等首屏真正在顯示器上繪制出來后,再進行下一步操作。
得到textarea.value,填充回DOM樹時,得妥善處理內嵌的script代碼。
對內嵌script代碼中的document.write要妥善處理。
通過textarea回填,里面的非defer和async腳本會從同步變成異步。要妥善處理依賴關系,不破壞原有腳本邏輯。
對于優化項目來說,完備的測試和監控非常重要。
這次還做了AssetsTransfer。用戶第一次訪問時,會將首屏相關的腳本和樣式內嵌,并做預加載。用戶再次訪問時,則改成外鏈方式,這樣能充分利用瀏覽器緩存,并減少 html傳輸量。
最后,給一張優化成果圖:
這是一個典型的淘寶詳情頁的首屏時間趨勢圖。可看出,首屏時間從優化前的3s降低到了優化后的1.5s左右,快了一倍!
更深度的優化需要對頁面內容(包括腳本)做進一步的細粒度模塊化,區分出優先級,然后根據需求,靈活自由地控制各個模塊的下載和執行等等。
這篇博客寫得比較雜,關于BigRender優化的更多細節,以后有機會再細說。歡迎反饋、拍磚。歡迎業界各位朋友嘗試BigRender優化,希望國內的站點速度都越來越快!
文章轉載于:http://www.yixieshi.com/80147.html
聲明:本網部份文章為轉載文章,在每篇文章底部有說明,文章的觀點和立場僅代表作者個人立場,不代表微網立場,若是文章轉載中有侵范您的權益,請發郵件到 493149@qq.com或致電13922854199通知刪除,謝謝!