本文作者:任家乐
原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 (id: yuewen_YFE) 获取授权,并注明作者、出处和链接。
性能风暴
「据说亚马逊雨林的一只蝴蝶偶尔扇动几下翅膀,可以在两周以后引起美国得克萨斯州的一场龙卷风」曾经起点的订阅页也经历了类似的龙卷风袭击事件,那只蝴蝶便是我们加工过的 Checkbox(复选框)。我们对无公害的 Checkbox 究竟做了什么,才引发了这场性能风暴?
风暴前夕
起点订阅页是龙卷风袭击的现场,也是本文不得不小刀的对象。还记得当时「望向远方的回忆脸」开发、联调一切顺利,仿佛能立马上线 「惊喜」。但提测后,画风突变,测试同学扔过来几个重磅炸弹,订阅页章节数目超过 3000 章时,会出现反应迟钝、页面假死、浏览器奔溃等现象。看到这几个 Bug 心里一沉,3000 章以上的超长作品场景在联调过程中确实被忽略了,不管怎样,赶紧搬砖 「奔溃脸」
在此顺便提下订阅页的主打功能:陈列书籍所有收费章节,通过勾选复选框来进行单章、单卷、多卷订阅,在发起订阅操作时,也提供了余额支付、快捷支付、以及风险控制等功能。
风暴现场
抓紧时间体验了下起点近 7000 章的「带着农场混异界)」的订阅页,嘴里念叨着「谁会看这么长的文」,心里却很诚实的总结出 3 个明显的问题:
1. 内容区域 Loading 时间过长
章节数 | Loading 耗时 (s) |
---|---|
3000 | 6.519 |
5000 | 14.732 |
7000 | 24.753 |
▲ 打印时间戳取 10 次实验平均数得到
2. 勾选 Checkbox 响应慢
3. 页面拖动滚动条卡顿明显
问题很严重,性能优化势在必行!
灾情分析与解决
一、老版本 UI 控件惹的祸
7000 章数据 Loading 24s 「 excuse me? 」按照老习惯,直奔代码循环体找原因,在此之前先讲一下订阅页的渲染逻辑:
- 页面 DOM Ready 后请求章节数据
- 请求 EJS 模板并渲染 EJS 模板
- 将渲染好的模板插入页面 DOM 中
- 初始化 Checkbox 实例
抓了下请求数据包,250kb 耗时 493ms ,可见瓶颈不在数据请求,于是用本地数据对步骤 2、3、4 做了性能测试,结果如下:
章节数 | 渲染 EJS 模板 | DOM插入 EJS 模板 | 初始化 Checkbox 实例 |
---|---|---|---|
3000 | 92ms | 75ms | 6345ms |
5000 | 149ms | 133ms | 13948ms |
7000 | 278ms | 301ms | 23866ms |
▲模拟次数:10
以上数据表明,实例化 Checkbox UI 组件是耗时最多的环节。由于数据是由书籍卷构成的基本结构,要实例化单个 Checkbox ,同时考虑到对整卷的操作需求,必先循环卷,再循环卷中的所有章节,这就有了双层循环。另外根据订阅页的视觉要求,兼容到 IE7 ,则不能使用原生的 Checkbox 组件,所以选择了兼容性更好的 Checkbox UI 组件。扒一下代码:
var chapter;
for (var v = 0; v < volumeNum; v ) {//do something with book volumefor(var c = 0; c < volChapters.length; c ){//初始化章节相关checkboxchapter = new Checkbox({selector: '#' volChapters[c].id});//do something with book chapter}//do something after init checkbox
}
打印时间戳发现 Checkbox 组件的初始化比较耗时,尝试去除初始化 Checkbox UI 组件的逻辑,改用我们为业务定制的,采用 CSS 渐进增减的 UI 组件 LULU UI 代替 Checkbox 的美化组件,情况果然有所好转,结果如下:
▲数据请求成功的回调总时间相比之前的 24s ,Loading 时间上已骤减至 1.925s(数据请求时长 上图成功逻辑时长),考虑到网络环境的不同,真实情况下应大于 2s 。同时拖动滚动条卡顿的情况有所缓解,但依然存在。
Let’s go on!
二、CSS3 的高级属性是把双刃剑
尝试勾选「选择全部章节」复选框,响应依旧延迟,渲染耗时竟然达到了 5840.9ms !页面卡顿也依旧明显 「奔溃脸」,继续填坑!
▲勾选「选择全部章节」复选框渲染耗时勾选「选择全部章节」复选框只做了一个事情,遍历所有 Checkbox 改变 UI 状态并获取属性。虽然 LULU UI 是用 CSS3 属性来绘制 Checkbox ,也不至于会产生这么严重的性能问题啊?为了排除 CSS 的嫌疑,对比了 IE8 和 Chrome ,结果发现 IE8 比 Chrome 在勾选 Checkbox 的反馈上快很多!IE8 不支持一些 CSS3 属性,因此优雅降级采用了背景图实现 Checkbox UI 。按道理来说,性能问题多数是脚本搞的鬼,难道 CSS 也对性能有这么大的负面影响?
id(#myid)
class(.myclass)
tag(div)
adjacent sibling(h1 p)
child(ul > li)
descendent(li a)
universal(*)
attribute(a[rel=”external”])
pseudo-class and pseudo element(a:hover li:first)
▲CSS 选择器效率权威排序
LULU UI 在绘制 Checkbox 时,使用了例如描边、阴影、过渡等 CSS3 高级属性,其勾选、禁用等样式也是通过效率相对低的伪类选择器和相邻兄弟选择器实现的。3000 个以上的 Checkbox 意味着 3000 次以上的 CSS3 选择器筛选,由此猜想可能是选择器带来的性能问题。
.ui-checkbox{ … box-sizing: border-box; box-shadow: inset 0 1px, inset 1px 0, inset -1px 0, inset 0 -1px; -webkit-transition: color .2s, background-color .1s; transition: color .2s, background-color .1s; …
}
:checked .ui-checkbox, :checked .ui-checkbox: hover{ color: $borderFocus; background-color: $backgroundRed;
}
▲ LULU UI 复选框样式代码
为了确认这是 CSS3 选择器带来的渲染问题,尝试改为类选择器来重新定义 Checkbox 的样式,代码如下。
.ui-checkbox{ … background: url(….); ….
}.ui-checkbox-checked { background-position: 0 -40px;
}
▲LULU UI 复选框优化后样式代码
结果勾选「选择全部章节」卡顿消失了,几乎是即时响应。
顺便看了下性能数据,渲染只需 1777.7ms。
三、前端分段加载
至此,问题 2 的 Checkbox 勾选产生的性能问题已经得到解决,但在页面加载过程中的卡顿现象依然存在,章节数如果太多,滚动条几乎处于假死状态。
这并不难理解,页面上的章节数越多,意味着有更多的 DOM 节点需要一次性渲染,即使仅操作 1 次 DOM 短时间内插入如此多的节点,还是会导致耗时较长。通常做法是使用分页、下拉加载等常用策略来解决这个问题。
▲页面 Onload 过程上图可以看到,页面 Onload 过程中,Recalcualte Style(重新计算样式)的时间达到了 1.08s,这是什么概念?即使是 QQ 空间这样集聚图片、输入框、操作按钮的多节点网页,在 Onload 过程中运用在 Recalculate Style 上的时间也只有 293.4ms 左右。
▲ QQ 空间 Onload 过程内容分段加载势在必行,我们的分段加载策略:数据依旧一次性拉取,前端来进行分段渲染并分段插入 DOM 中。
最优的方案当然是后端改造接口来支持前端多次拉取章节数据,但时间紧迫,后端改造接口也需要一些时间,因此并没有做到请求的分段。前端分段数据结果如下表,Loading 时长差别不大。
章节数 | 分段前 Loading 时长 | 分段后 Loading 时长 |
---|---|---|
3000 | 0.366s | 0.391s |
5000 | 0.712s | 0.699s |
7000 | 0.938s | 0.936s |
▲前端分段数据结果
▲分段加载结果对比上面 4 组图,Rendering(渲染)和 Painting(重绘)在分段后有所减少,但随着分段的数量加大,差别逐渐缩小。可能是由于第一次将数据插入 DOM 还伴随着页面其他元素及资源的载入,因此第一次分段效果显著。但接近 1.5s 的渲染时长,还是存在问题。
▲分段加载 Checkbox 前后对比理论上来看分段应该是有效的,但实践来看,分段后勾选「选择全部章节」在渲染、脚本、重绘方面相对减小,差别却并不明显。猜想:其依次插入 DOM 太过连续、没有间隙,因此使用计时器来进一步优化。
计时器的使用
计时器可以使 JS 延迟一段时间执行,此事件排在当前线程中事件队列的最后。这里我们可以使用计时器的 setTimeout 方法,并设置延迟时间为 0ms 。虽然我们设定的时间为 0ms,但浏览器的延迟时间最小为平均 16ms 。 这 16ms 对用户来说无感知,但浏览器却可以趁此机会喘口气。
「计时器可以防止在生成大量DOM元素的情况下浏览器hanging(绞死) 。
— Javascript 忍者秘籍
关于 16.66ms 的解释:
「我们的目标是保证页面要有高于每秒 60fps (帧) 的刷新频率,这和目前大多数显示器的刷新率相吻合 ( 60Hz )。如果网页动画能够做到每秒 60fps(帧),就会跟显示器同步刷新,达到最佳的视觉效果。这意味着,秒之内进行 60 次重新渲染,每次重新渲染的时间不能超过 16.66 毫秒。」
— From horve‘s article
尝试在分段加载的基础上加入计时器的使用,我们来看看性能数据的对比:
▲Mock 7000章 Loading 时长,左图不使用计时器,右图使用计时器
显然 Rendering( 渲染 )方面有了明显的下降(下降约 1300ms),数据已接近我们的期望值( 368ms )!但对于勾选「选择全部章节」并没有太大影响,数据对比如下:
▲勾选 Checkbox,左图不使用计时器,右图使用计时器四、减少 DOM 操作与访问
到此前 3 个性能问题都达到了我所期望的结果,但我竟然选择性遗忘了 IE7 浏览器 。在 IE7 下,勾选「选择全部章节」依然响应延迟。是的,目前起点 PC 平台需要支持到 IE7 及以上。
打印时间戳看了下,发现获取并更新当前已选章节的总数量、总价格、总字数操作过于消耗时间。这是由于每次勾选及取消勾选 Checkbox ,都会实时从 DOM 获取数量相关元素的内容,在渲染性能偏低的 IE7 下,瓶颈会比较明显。因此我们将当前总章节数、总价格、总字数、及展示这些数字的元素本身缓存于对象中,不再实时读取,看下数据:
章节数 | 优化前(ms) | 优化后(ms) |
---|---|---|
3000 | 864 | 177 |
5000 | 1317 | 169 |
7000 | 1774 | 180 |
▲ 取10次测试数据平均值
五、基于业务特性的体验优化
以上 4 个体验问题到这里已基本解决,现在再来看看近 7000 章的「带着农场混异界」:
▲ 7000 章的「带着农场混异界」性能优化的结果已经很完美了,但现在所有用户每次访问订阅页,不论章节多少必然能看到 Loading ,章节数多还能忍,章节数少的情况下,还是给人不友好的感觉。毕竟页面数据直出的加载体验才是最佳实践。
章节数 | 数量(本) | 百分比 |
---|---|---|
大于5000 | 19 | 0.003% |
大于2000 && 小于5000 | 260 | 0.045% |
▲ 书库数据
数据显示,章节数大于 2000 的书籍是少数,仅仅只有 200 多本,因少量书籍(占0.045% )而损耗的加载体验无法令人满意!
为了让 99% 以上的用户更快看到内容,我们大多数书籍的订阅页完全可以做到数据直出!因此和后端同学约定了简单的标识,章节数小于等于 2000 的时候直出数据,大于 2000 的时候,AJAX 拉取数据后分段插入页面。到此为止暴风世界总算平净了 ^_^。
▲ 无 Loading 体验为了做更好的我们,一些压缩 AJAX 数据量的小优化也是不能够放弃哒,例如压缩字段、减少多余字段等。即使在 5000 章以上的数据量下,这些优化也能积少成多地减少传输量。
风暴平息
这段性能风暴已经平息些许时日了,这次分享出来,既是自己做总结,也是协助同样遇到性能瓶颈的童鞋。其实除此之外,还能想到一些可行的方案可以继续提升页面的性能,例如:使用 requestIdleCallback 来充分利用浏览器的空闲时间做一些事情,提升性能;针对节点做 timechunk 的算法来精确规划每秒钟创建多少节点;上文中所说的接口分段等等。性能优化永无止境,后续会找机会把更多方案应用到订阅页及其他需要优化的页面,共勉。
PS:想更多的了解LULU UI,可以访问此链接:https://l-ui.com/