淘宝详情页的 BigRender 优化与存放大块 HTML 内容的最佳方式

标签: Programming | 发表时间:2011-09-23 23:56 | 作者:lifesinger 蓝精灵
出处:http://lifesinger.wordpress.com

对于复杂页面,为了将用户关注的内容尽可能快渲染出来,至少有两种方式:

一、Facebook 的 BigPipe 方式。先输出页面整体布局,然后逐步输出脚本块,一边输出一边执行,将内容渲染回页面布局中。这样可以让服务端的运算、网络传输和浏览器端的渲染变成并行。BigPipe 最主要解决的问题是服务端的运算时间,当服务端的运算时间大于 300 ~ 500ms 时才能体现出优势。当服务端响应非常快(小于 100ms),BigPipe 退化为下面要讲的 BigRender.

二、淘宝商品详情页的 BigRender 方式。淘宝的商品详情页,服务端平均响应时间为 52ms, 采用 BigPipe chunked 输出意义不大。这次优化主要在浏览器端。页面下载完毕后,要经过 Tokenization — Tree Construction — Rendering. 要让首屏尽快出来,得给浏览器减轻渲染首屏的工作量。可以从两方面入手:

  1. 减少 DOM 节点数。节点数越少,意味着 Tokenization, Rendering 等操作耗费的时间越少。(对于典型的淘宝商品详情页,经测试发现,每增加一个 DOM 节点,会导致首屏渲染时间延迟约 0.5ms.)
  2. 减少脚本执行时间。脚本执行和 UI Update 共享一个 thread, 脚本耗的时间越少,UI Update 就能越发提前。

减少首屏 DOM 节点数

对于 BigPipe 来说,初始输出的只有页面布局,DOM 节点数不多。首屏的 DOM 节点数主要取决于首屏脚本块中,字符串化的 html 代码:

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

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

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

  1. 和 Facebook 的 BigPipe 一样,调整页面代码为 页面布局 + 脚本块。BigPipe 是服务器 chunked 输出 html 内容,BigRender 是服务器一次性输出,其他都是一样的。
  2. 尽量少调整页面代码,但通过某种方式,将首屏不需要的 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 中的 --> 要替换为某种特殊标记。(不能简单转义为 --&gt;
  2. 服务端,html 中的 -- 也要替换为某种特殊标记。否则在 Firefox 低版本中存在 bug.
  3. 浏览器端,得到 htmlCode 后,要将上面的特殊标记替换回原值。

当 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 来避免渲染:

<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>&lt;p&gt;some text&lt;/p&gt;</data>

“Parsed” 是指 < 和 & 字符要转换成 &lt; 和 &amp; 实体字符形式。如果不想写一大堆 &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 来解析;&gt; 也不会根据实体字符来转义,就表示 &gt; 字串自身。这就是 CDATA.

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

<script>
document.write('<script>alert("O HAI")</script>');
</script>

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

<script>
  // Using string concatenation:
  document.write('<script>alert("heh")<' + '/script>'); // Lame.
  // Using a string literal escape:
  document.write('<script>alert("huh")<\x3Cscript>'); // Lame.
  // Simply escaping the solidus character with a reverse solidus (\):
  document.write('<script>alert("O HAI")<\/script>'); // Awesome!
</script>

style 也类似,不多说。

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

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

<textarea><p>&lt;</p></textarea>

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

用 script 来存放 html 代码

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

<script type="text/html" id="script-data">
<p>some text</p>
</script>

获取也很简单:

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

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

  1. 服务端,要将 script 里 html 中的 </script 替换为某种特殊标记。
  2. 浏览器端,得到 htmlCode 后,要将上面的特殊标记替换回原值。

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

<script type="text/html" id="script-data">
<script>
var str = '<\/script>';
<\/script>
</script>

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

用 textarea 来存放 html 代码

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

  1. 遇到 & 时,会尽可能得到实体字符。
  2. 遇到 </textarea(\s|\\|>) 时,会结束解析。
  3. 其他都直接作为 textarea 的内容。
<textarea id="area-data">
<p>some text</p>
</textarea>

获取非常简单:

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

缺点:

  1. 服务端,要将 html 中的 & 转义成 &amp;
  2. 服务端,要打破 ETAGO, 将 </textarea 转义成 &lt;/textarea

优点很明显,在浏览器端,只需通过 textarea.value 取值即可,无需进行任何转义替换操作。
并且理论上不会出现任何 bug.

存放大块 HTML 代码的最佳方式

经过上面的分析,结果已经很明显,用 RCDATA elements 来存放数据是最妥当的。title 元素明显不合适,因此最后的选择就剩下一个了:textarea. 并且从语义上讲,用 text area 来存放 html text 也说得过去^o^

回到首屏渲染优化

可以根据实际情况,将页面划分成几大区域。非首屏区域,简单转义后,直接用 textarea 包裹起来。这样,DOM 数立刻就减少了。浏览器在拿到 html 代码时,首次 Tokenization — Tree Construction 的速度就会大大加快。

完整的优化,还需要:

  1. 给浏览器合理的喘息(UI Update)时间,等首屏真正在显示器上绘制出来后,再进行下一步操作。
  2. 得到 textarea.value, 填充回 DOM 树时,得妥善处理内嵌的 script 代码。
  3. 对内嵌 script 代码中的 document.write 要妥善处理。
  4. 通过 textarea 回填,里面的非 defer 和 async 脚本会从同步变成异步。要妥善处理依赖关系,不破坏原有脚本逻辑。
  5. 对于优化项目来说,完备的测试和监控非常重要。
  6. 这次还做了 AssetsTransfer. 用户第一次访问时,会将首屏相关的脚本和样式内嵌,并做预加载。用户再次访问时,则改成外链方式,这样能充分利用浏览器缓存,并减少 html 传输量。

最后,给一张优化成果图:

这是一个典型的淘宝详情页的首屏时间趋势图。可看出,首屏时间从优化前的 3s 降低到了优化后的 1.5s 左右,快了一倍!

更深度的优化需要对页面内容(包括脚本)做进一步的细粒度模块化,区分出优先级,然后根据需求,灵活自由地控制各个模块的下载和执行等等⋯⋯

这篇博客写得比较杂,关于 BigRender 优化的更多细节,以后有机会再细说。欢迎反馈、拍砖。欢迎业界各位朋友尝试 BigRender 优化,希望国内的站点速度都越来越快!

参考资料

  1. CDATA
  2. CDATA Confusion
  3. HTML5 Tokenization
  4. The end-tag open (ETAGO) delimiter
  5. tokenization of html
  6. html 实体字符值

补充说明

2011-09-23: 在业界,script 经常用来存放 template 数据:

<script type="text/template">
<h1>{{title}}</h1>
<p>I am {{name}}...</p>
</script>

绝大部分情况下,template 里不会出现 </script . 这样,服务端和浏览器端都无需做任何 replace, 是目前用来存放 template 的最佳实践。

2011-09-24: 举例说明下 textarea 中为何要转义 &. 假设原代码为:

<p>&amp;lt; represents &lt;</p>
<p>&lt;/p&gt;</p>

如果直接放到 textarea 中,

<textarea>
<p>&amp;lt; represents &lt;</p>
<p>&lt;/p&gt;</p>
</textarea>

由于 textarea 是 RCDATA 元素,上面的代码等价于:

<textarea>
<p>&lt; represents <</p>
<p></p></p>
</textarea>

获取 textarea.value, 回填到 DOM 树的代码为:

out.innerHTML = textarea.value;

这时页面中的显示效果明显和原来不一样了。
如果将所有 & 都转化成 &amp; 则可以保持原样。

注意:理论上,并不需要将所有 & 转化成 &amp;, 只需要将与 HTML 语法冲突的字符串 &lt; &gt; &amp; &nbsp; 中的 & 转化成 &amp; 即可。但这样做,还得处理 &#xx 等数值表示,比如 & 还有 &#38; &#0038; 两种表现形式,这样替换起来更麻烦,不如将所有 & 替换成 &amp; 来得快捷高效。

相关 [淘宝 bigrender 优化] 推荐:

淘宝详情页的 BigRender 优化与存放大块 HTML 内容的最佳方式

- 蓝精灵 - 岁月如歌
对于复杂页面,为了将用户关注的内容尽可能快渲染出来,至少有两种方式:. 一、Facebook 的 BigPipe 方式. 先输出页面整体布局,然后逐步输出脚本块,一边输出一边执行,将内容渲染回页面布局中. 这样可以让服务端的运算、网络传输和浏览器端的渲染变成并行. BigPipe 最主要解决的问题是服务端的运算时间,当服务端的运算时间大于 300 ~ 500ms 时才能体现出优势.

淘宝网前台应用性能优化实践

- - 淘宝中间件团队博客
本文曾发表于2013年4月的《程序员》杂志. 近年来,随着用户数和PV的增加,淘宝网的后端服务器数量增长很快;并且我们知道,Web页面延迟时间和转化率之间有着直接的关联. 出于提升系统吞吐量、降低成本、减少页面延迟、提升用户浏览体验、提高交易转化率的考虑,淘宝网在性能优化领域做了很多尝试. 本文将从应用性能分析、基础设施优化、应用自身优化、前端性能优化这四个方面,对淘宝网的优化尝试做一个总结.

HBase在淘宝的应用和优化小结

- - NoSQLFan
本文来自于NoSQLFan联合作者@ koven2049,他在淘宝从事Hadoop及HBase相关的应用和 优化. 对Hadoop、HBase都有深入的了解,本文就是其在工作中对HBase的应用优化小结,分享给大家. 原文地址: http://walkoven.com/?p=57. 文章PDF下载: http://walkoven.com/hbase:optimization and apply summary in taobao.pdf.

java 内存移到堆外!!! Jvm gcih 淘宝优化JVM实践

- - CSDN博客互联网推荐文章
出自Jvm  GC-Invisible Heap. GC-Invisible Heap,简称GCIH,是一种将Java对象从Java堆内移动到堆外,并且可以在JVM间共享这些对象的技术. GCIH顾名思义就是GC访问不到的堆,它是对JVM内存管理机制的一个有益的补充. 在某些特殊的应用中有大量生命周期很长的对象,在应用运行的整个过程中它们都存在,不需要被GC回收.

淘宝双十一架构优化及应急预案

- - InfoQ cn
 每年的双十一,在整体架构上最依仗的是过去几年持续建设的稳定的服务化技术体系和去IOE带来整体的扩展性和成本的控制能力. 今年在架构上做的比较大的优化有三点:. 第一是导购系统的静态化改造. 今年淘宝和天猫都分别根据自身的业务特性进行了Detail页面的静态化改造,但核心的思路都是通过CDN+nginx+JBoss+缓存的架构,将页面信息进行静态化缓存.

淘宝商品库MySQL优化实践的学习

- - 标点符
淘宝商品库是淘宝网最核心的数据库之一,采用MySQL主备集群的架构,特点是数据量大且增长速度快,读多写少,对安全性要求高,并发请求高. 由于MySQL最初的设计不是用来存储大规模数据的,但淘宝的数据量非常惊人,所以在I/O方面,尤其是CPU I/O层面会有很大瓶颈,因此淘宝的主要目标也是解决IO方面的瓶颈问题.

淘宝内部分享:MySQL & MariaDB性能优化

- - 极客521 | 极客521
MySQL是目前使用最多的开源数据库,但是MySQL数据库的默认设置性能非常的差,必须进行不断的优化,而优化是一个复杂的任务,本文描述淘宝数据库团队针对MySQL数据库Metadata Lock子系统的优化,hash_scan 算法的实现解析的性能优化,TokuDB·版本优化,以及MariaDB·的性能优化.

社区热议淘宝开源的优化定制JVM版本:Tabao JVM

- - InfoQ cn
9月18日,淘宝核心系统部专用计算组的 王峥(花名:长仁)在 微博上宣布:. jvm.taobao.org上线,开源基于OpenJDK vm的优化定制JVM版本:TaobaoJVM. 在 jvm.taobao.org上,介绍了项目的背景:. 淘宝有几万台Java应用服务器,上千名Java工程师、及上百个Java应用.

淘宝“伤”城

- 品味视界 - FT中文网_英国《金融时报》(Financial Times)
秦苏为英国《金融时报》中文网撰稿. 中国互联网的野蛮生长,再次震惊了电子商务市场. 10月11日晚间,为抗议淘宝商城大幅提高技术服务年费和保证金,约7000家中小卖家通过YY网络语音等组织方式,对韩都衣舍、欧莎、七格格、优衣库等大卖家进行攻击,包括利用规则进行购物、给差评、到货付款或申请退款等. 通过集中拍下某商品,导致这些商家的大部分商品下架“被拍死”.