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