通过前面的章节,想必大家已经熟悉了页面性能的几大要素,以及如何利用工具或者自建系统进行页面的性能分析。本章为大家介绍两个典型的优化实例。
2.1 延迟渲染
通常为了加快页面渲染的速度,基础的解决思路是,通过去除页面上除首屏以外的对于用户不可见的信息区块,让页面的DOM节点数更少、DOM树结构更简单,从而达到加快页面下载和渲染速度的目的。
然后使用懒加载异步化请求、BigPipe等方案,动态加载这些不可见的信息区块。
考虑 BigPipe 方案的实施成本,大部分情况下,笔者会选择使用懒加载异步化请求的方案对页面进行优化。
2.1.1 挑战和困难
在实际的页面优化过程中,并不是一切都如预期那样顺利。笔者曾遇到一个问题,即页面的主体部分不能再使用异步化请求的方式分割加载页面信息。
这个页面有什么特殊之处,为何它被拒绝使用异步化请求进行懒加载呢?因为它是网站重要的主流程页面,即承担搜索功能的搜索结果列表(List)页面。在很多电商网站中都可以发现,搜索页面的结构非常相似,通常会包含两大主体搜索结果内容,如图2-1所示。

图2-1
从如图2-1所示的页面结构可以发现,页面主体是典型的左右两栏布局。其中左栏一般为关联产品搜索结果的类目数据,右栏为搜索结果数据。所以对于页面的首屏来说它包含了两部分,通常这两部分数据都来自于两次独立的数据查询的结果。
如果使用异步化请求分割右栏的搜索结果列表,当单页请求结果数为50时,首屏展示区域为3条结果。抛开这种请求方式对于服务端搜索引擎分页处理系统的挑战不说,一次搜索结果分两次请求获取,这对于服务端的资源也是很大的浪费。我们都知道,在服务端数据查询请求的优化中,强调的是对于查询请求资源次数的优化,而不是查询结果的大小。在这样的请求中,查询结果一次返回3条数据还是50条数据,所消耗的时间几乎是一样的。
表面上页面通过这样的分割,可以达到减少 DOM 树节点数、简化 DOM 树结构的目的,但考虑到服务端所付出的代价,这无疑是得不偿失的。
所以在这样的情况下,我们必须寻找其他方式,来简化这个庞大的搜索结果列表页面。
2.1.2 解决方案
既然服务端的解决方案有诸多限制,那么将焦点转移到浏览器端。笔者希望在尽量不改动服务端在Web应用层的HTML Template输出逻辑的前提下,在浏览器端找到能够加快页面渲染速度的办法。
注:之所以要求尽量不改动Web应用层的HTML Template输出逻辑,是为了保障搜索页面模板代码的可维护性。
首先我们大概回顾下浏览器端的页面渲染过程,如图2-2所示。

图2-2
其中渲染树即之前提到过的 CSSOM 树的构建。基于这个渲染过程,我们通过各种测试来尝试找出在搜索页面中真正的渲染瓶颈在何处。
为了方便大家理解测试的过程和结果,我们假设一个前提,完整的搜索结果页面包含36个搜索结果。其中首屏展示的部分包含3个结果,首屏以下不可见部分则为剩下的33个结果。
图2-3是笔者汇总的测试报告。

图2-3
下面来详解每个测试结果。
1)将矛头指向“绘制Render树”
在测试过程中,逐步隐藏搜索结果,白屏等待的时间有所减少,但效果不明显。
2)测试“Render树构建”
在测试过程中,逐步删减页面CSS文件的样式代码直到没有。笔者观察到,每一次减少都能使StartRender时间缩短,直到最终页面无任何样式代码时,StartRender时间与原先相比大幅缩短。通过测试发现,基于DOM树和样式构建Render数据的过程也非常耗时,但是页面不可能做空版页面,CSS文件精简的程度有限,于是紧接着做第三个测试。
3)观察DOM树
在测试过程中,逐步减少搜索结果数量,直到剩下首屏展示的三个结果,得到的测试结果和上一个测试结果相似,而且StartRender的优化程度更明显。基于渲染过程,笔者对于结果的分析是,DOM树节点数量的减少不仅能够加快DOM树的构建过程,而且也能加快Render树的构建过程。
通过上面不同维度的测试,笔者发现在整个渲染过程中,主要的消耗还是集中在 DOM 树和Render树的构建上。其中DOM树的复杂程度对于整个渲染过程的复杂程度起到关键作用,浏览器解析文档构建DOM树,然后遍历DOM树,计算每个DOM树节点的样式,生成Render树。我们可以用一个简单但并非完全准确的公式来表现这个过程的复杂度。
在不考虑CSS Selector深度的情况下:
DOM树节点数×CSS Selector数=遍历循环次数
以我们的页面举例,大概有2500个DOM树节点和2000个CSS Selector:
2500 ×2000=5 000 000
最多可能会有500万次的遍历循环。
通过上面的测试分析,我们明白了浏览器在DOM树上构建Render树并不是非常轻松的任务。如果你希望开发一个高性能的 HTML 页面,在编写 HTML 和样式的时候,都应该加倍小心,尽量精简结构,并且要避免通过滥用样式选择器得到高效的CSS Selector。
回到我们的目标页面,在不影响业务功能展现的前提下,我们的页面已经采集了很多基础方案进行了优化。例如HTML结构精简、样式文件优化、CDN Cache、DNS Prefech等,但还是未能达到“1s内开始StartRender”的目标值。
基于上面的测试结果分析,可以判断出目标页面的渲染性能瓶颈在于,页面的DOM树节点数过多,导致在DOM树和Render树构建上所消耗的时间过多,从而使页面渲染性能不够高。
所以现在方案也非常明确了,就是要减小页面首次渲染时的 DOM 树节点数,并且在不修改服务端输出逻辑的前提下进行。
然后细化分解一下,希望提供的方案能够将首屏以下不可见的33个结果,在浏览器首次渲染的时候,在页面的 DOM 树中消失,达到加快首次渲染速度的目的。在其处于可见状态的时候,再将其恢复到DOM树中。
在考虑尽量不修改服务端逻辑的前提下(保障可维护性),笔者最终选择使用TextArea来存放首屏以下的33个搜索结果对应的HTML代码。存放在TextArea中的HTML代码,浏览器会解析识别为TextArea内容,而不会被当作DOM节点进行解析。所以通过这个办法,页面首次渲染的DOM树包含的节点数大幅减少,从而大幅提高首次渲染速度。
我们额外要做的事情是:在服务端,将一些特殊字符(比如&)进行HTMLEncode转义。而在浏览器端,则需要在首屏以下区域处于可见状态时,将TextArea中的HTML代码取出,将其恢复到DOM树中进行渲染。
在这里,笔者将这个方案称为“延迟渲染”。在实际运用过程中,根据不同情况,也可以选择使用Script Tag来存放HTML代码。
我们看一下使用“延迟渲染”给页面带来的变化,如图2-4所示。

图2-4
最终通过这个方案,我们解决了搜索页面的渲染性能瓶颈问题,将页面的StartRender稳定在1s内。