[浅析]淘宝详情页的BigRender优化的最佳方式

作者:微网 出处:微网 点击次数:1807 发布时间:2011/11/4 0:00:00

[浅析]淘宝详情页的BigRender优化的最佳方式

导读: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代码:

  1. big_pipe.onPageletArrive({ "content": { /* data */ } })   
  2.  

这种方式下,页面中的DOM节点是逐步增加的。尚未渲染的DOM节点,不会影响TTI区域。

对于BigRender来说,减少DOM节点数的方式有:

和Facebook的BigPipe一样,调整页面代码为页面布局+脚本块。BigPipe是服务器chunked输出html内容,BigRender是服务器一次性输出,其他都是一样的。

尽量少调整页面代码,但通过某种方式,将首屏不需要的html代码先存放起来。渲染好首屏后,再将存储好的html代码逐步渲染出来。

用js字符串来存放html代码

最容易想到的一种方式是学习Facebook好榜样,用js字符串来存放:

  1. <script>   
  2.  
  3. var data = "<p>some data</p>...";   
  4.  
  5. </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代码

为了便于获取注释内容,添加一层包裹:

  1. <div id="comment-data"><!--   
  2.  
  3. html code   
  4.  
  5. --></div>   

这样,获取代码很简单:

  1. var htmlCode = document.getElementById('comment-data').  
  2. childNodes[0].nodeValue;  
  3.  

缺点是:

1.服务端,html中的-->要替换为某种特殊标记。(不能简单转义为--&gt;)

2.服务端,html中的--也要替换为某种特殊标记。否则在Firefox低版本中存在bug.

3.浏览器端,得到html Code后,要将上面的特殊标记替换回原值。

当html code很大时,替换的效率不高。依赖特殊标记的替换理论上也不完美。

还有什么存放方式呢?

HTML的元素类型

HTML元素分为五大类:

  1. Void elements。像hr,br,base这种。
  2. Raw text elements。有两个:script和style.
  3. RCDATA elements。也有两个:textarea和title.
  4. Foreign elements。来自MATHML和SVG的元素。
  5. Normal elements。除了以上四种类型之外的所有元素,比如p,div,iframe等。

显然,Void elements和Foreign elements不适合用来存放html代码。

对于Normal elements,里面的<字符会被当做tag open来解析,有一个方式是通过display:none来避免渲染。

  1. <div style="display:none"> 
  2.  
  3. html code   
  4.  
  5. </div>   

这样做,减少的只是可见的DOM节点数,DOM总数依旧不变。Tokenization — Tree Construction等操作的耗时并没减少。

我们将重点放到Raw text elements和RCDATA elements上来。

CDATA,PCDATA和RCDATA

先了解下CDATA(Character Data) 的相关知识点。

在XML中,不包含子元素的元素的内容默认必须是PCDATA(Parsed Character Data):

  1. <data>&lt;p&gt;some text&lt;/p&gt;</data>   

“Parsed”是指<和&字符要转换成&lt;和&amp;实体字符形式。如果不想写一大堆&xx;,可以直接标记为CDATA:

  1. <data><![CDATA[<p>some text</p>]]></data>   

这是XML的习惯,很严格,但对用户并不友好。在HTML中,如果要兼容XML,得像如下一样:

  1.  <script>   
  2.  
  3.  //<![CDATA[   
  4.  
  5.  var t = "<p>";   
  6.  
  7.  //]]>   
  8.  
  9. </script>   
  10.  

增加的<![CDATA]很无聊。script中本就是CDATA.

为了让用户更舒心,让代码更自然,HTML将script和style定义为Raw text elements。也就是说,这两个元素里面的内容是raw text,里面出现的>就表示>字符本身,不会被当作tag open来解析;&gt;也不会根据实体字符来转义,就表示&gt;字串自身。这就是CDATA.

Raw text elements有一个限制:里面的内容不能有自身的ETAGO标记,也就是说,script里的内容不能含有</script(\s|\\|>),否则就会导致script提前结束:

  1. <script>   
  2.  
  3.  document.write('<script>alert("O HAI")</script>');   
  4.  
  5.  </script> 

上面的代码会出错,必须打破&lt/script组合:

  1. <script>   
  2.  
  3. 2 // Using string concatenation:   
  4.  
  5. 3 document.write('<script>alert("heh")<' + '/script>'); // Lame.   
  6.  
  7. 4 // Using a string literal escape:   
  8.  
  9. 5 document.write('<script>alert("huh")<\x3Cscript>'); // Lame.   
  10.  
  11. 6 // Simply escaping the solidus character with a reverse solidus (\):   
  12.  
  13. 7 document.write('<script>alert("O HAI")<\/script>'); // Awesome!   
  14.  
  15. </script>   
  16.  

style也类似,不多说。

除了Raw text elements,还有RCDATA elements。我们来看看。

RCDATA(Replaceable Character Data)表示里面可以有&xx;等实体字符,也可以包含<字符而不会被当作tag open来解析。比如:

  1. 1<textarea><p>&lt;</p></textarea> 
  2.  

在RCDATA里,&lt;可替换为<(Replaceable的含义),拿到值(比如textarea.value)后,是无从得知源码里是否有&lt;等实体字符的。

用script来存放html代码

回到正题。在Raw text elements里,可以用script来存放数据:

  1. <script type="text/html" id="script-data">   
  2.  
  3. <p>some text</p>   
  4.  
  5. </script>   
  6.  

获取也很简单:

  1. var htmlCode = document.getElementById('script-data').innerHTML;   

这个方案比用注释来存放的方案更好,但依旧存在以下缺点:

服务端,要将script里html中的</script替换为某种特殊标记。

浏览器端,得到htmlCode后,要将上面的特殊标记替换回原值。

注意:特殊标记不能是<\/script,因为有可能存在以下代码:

  1. <script type="text/html" id="script-data">   
  2.  
  3. <script>   
  4.  
  5. 3 var str = '<\/script>';   
  6.  
  7. <\/script>   
  8.  
  9. </script>   
  10.  

这样替换回原值时,会误伤str字符串。

用textarea来存放html代码

textarea中的内容会按照RCDATA规则来解析:

遇到&时,会尽可能得到实体字符。

遇到</textarea(\s|\\|>)时,会结束解析。

其他都直接作为textarea的内容。

  1. <textarea id="area-data">   
  2.  
  3. <p>some text</p>   
  4.  
  5. </textarea>   
  6.  

获取非常简单:

  1. 1 var htmlCode = document.getElementById('area-data').value;   
  2.  

缺点:

服务端,要将html中的&转义成&amp;

服务端,要打破ETAGO,将</textarea转义成&lt;/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优化,希望国内的站点速度都越来越快!


在线咨询
售前咨询热线
400-830-8248
上班时间:

周一至周六
9:00-18:00