「起点订阅页」Checkbox 美化引起的蝴蝶效应

本文做者:任家乐css

原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 (id: yuewen_YFE) 获取受权,并注明做者、出处和连接。前端

性能风暴

「听说亚马逊雨林的一只蝴蝶偶尔扇动几下翅膀,能够在两周之后引发美国得克萨斯州的一场龙卷风」曾经起点的订阅页也经历了相似的龙卷风袭击事件,那只蝴蝶即是咱们加工过的 Checkbox(复选框)。咱们对无公害的 Checkbox 究竟作了什么,才引起了这场性能风暴?c++

风暴前夕

起点订阅页是龙卷风袭击的现场,也是本文不得不小刀的对象。还记得当时「望向远方的回忆脸」开发、联调一切顺利,仿佛能立立刻线  「惊喜」。但提测后,画风突变,测试同窗扔过来几个重磅炸弹,订阅页章节数目超过 3000 章时,会出现反应迟钝、页面假死、浏览器奔溃等现象。看到这几个 Bug 内心一沉,3000 章以上的超长做品场景在联调过程当中确实被忽略了,无论怎样,赶忙搬砖 「奔溃脸」web

在此顺便提下订阅页的主打功能:陈列书籍全部收费章节,经过勾选复选框来进行单章、单卷、多卷订阅,在发起订阅操做时,也提供了余额支付、快捷支付、以及风险控制等功能。算法

风暴现场

抓紧时间体验了下起点近 7000 章的「带着农场混异界)」的订阅页,嘴里念叨着「谁会看这么长的文」,内心却很诚实的总结出 3 个明显的问题:后端

1. 内容区域 Loading 时间过长

章节数 Loading 耗时 (s)
3000 6.519
5000 14.732
7000 24.753

▲ 打印时间戳取 10 次实验平均数获得浏览器

2. 勾选 Checkbox 响应慢

3. 页面拖动滚动条卡顿明显

问题很严重,性能优化势在必行!缓存

灾情分析与解决

1、老版本 UI 控件惹的祸

7000 章数据 Loading 24s 「 excuse me? 」按照老习惯,直奔代码循环体找缘由,在此以前先讲一下订阅页的渲染逻辑:性能优化

  1. 页面 DOM Ready 后请求章节数据
  2. 请求 EJS 模板并渲染 EJS 模板
  3. 将渲染好的模板插入页面 DOM 中
  4. 初始化 Checkbox 实例

抓了下请求数据包,250kb 耗时 493ms ,可见瓶颈不在数据请求,因而用本地数据对步骤 二、三、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 volume
  for(var c = 0; c < volChapters.length; c++){
    //初始化章节相关checkbox
    chapter = 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!

2、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。

3、前端分段加载

至此,问题 2 的 Checkbox 勾选产生的性能问题已经获得解决,但在页面加载过程当中的卡顿现象依然存在,章节数若是太多,滚动条几乎处于假死状态。

这并不难理解,页面上的章节数越多,意味着有更多的 DOM 节点须要一次性渲染,即便仅操做 1 次 DOM 短期内插入如此多的节点,仍是会致使耗时较长。一般作法是使用分页、下拉加载等经常使用策略来解决这个问题。

页面 Onload 过程
▲页面 Onload 过程

上图能够看到,页面 Onload 过程当中,Recalcualte Style(从新计算样式)的时间达到了 1.08s,这是什么概念?即便是 QQ 空间这样集聚图片、输入框、操做按钮的多节点网页,在 Onload 过程当中运用在 Recalculate Style 上的时间也只有 293.4ms 左右。

QQ 空间 Onload 过
▲ 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 先后对比
▲分段加载 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,左图不使用计时器,右图使用计时器

4、减小 DOM 操做与访问

到此前 3 个性能问题都达到了我所指望的结果,但我居然选择性遗忘了 IE7 浏览器 。在 IE7 下,勾选「选择所有章节」依然响应延迟。是的,目前起点 PC 平台须要支持到 IE7 及以上。

打印时间戳看了下,发现获取并更新当前已选章节的总数量、总价格、总字数操做过于消耗时间。这是因为每次勾选及取消勾选 Checkbox ,都会实时从 DOM 获取数量相关元素的内容,在渲染性能偏低的 IE7 下,瓶颈会比较明显。所以咱们将当前总章节数、总价格、总字数、及展现这些数字的元素自己缓存于对象中,再也不实时读取,看下数据:

章节数 优化前(ms) 优化后(ms)
3000 864 177
5000 1317 169
7000 1774 180

▲ 取10次测试数据平均值

5、基于业务特性的体验优化

以上 4 个体验问题到这里已基本解决,如今再来看看近 7000 章的「带着农场混异界」:

7000 章的「带着农场混异界」
▲ 7000 章的「带着农场混异界」

性能优化的结果已经很完美了,但如今全部用户每次访问订阅页,不论章节多少必然能看到 Loading ,章节数多还能忍,章节数少的状况下,仍是给人不友好的感受。毕竟页面数据直出的加载体验才是最佳实践。

章节数 数量(本) 百分比
大于5000 19 0.003%
大于2000 && 小于5000 260 0.045%

▲ 书库数据

数据显示,章节数大于 2000 的书籍是少数,仅仅只有 200 多本,因少许书籍(占0.045% )而损耗的加载体验没法使人满意!

为了让 99% 以上的用户更快看到内容,咱们大多数书籍的订阅页彻底能够作到数据直出!所以和后端同窗约定了简单的标识,章节数小于等于 2000 的时候直出数据,大于 2000 的时候,AJAX 拉取数据后分段插入页面。到此为止暴风世界总算平净了 ^_^。

无 Loading 体验
▲ 无 Loading 体验

为了作更好的咱们,一些压缩 AJAX 数据量的小优化也是不可以放弃哒,例如压缩字段、减小多余字段等。即便在 5000 章以上的数据量下,这些优化也能聚沙成塔地减小传输量。

风暴平息

这段性能风暴已经平息些许时日了,此次分享出来,既是本身作总结,也是协助一样遇到性能瓶颈的童鞋。其实除此以外,还能想到一些可行的方案能够继续提高页面的性能,例如:使用 requestIdleCallback 来充分利用浏览器的空闲时间作一些事情,提高性能;针对节点作 timechunk 的算法来精确规划每秒钟建立多少节点;上文中所说的接口分段等等。性能优化永无止境,后续会找机会把更多方案应用到订阅页及其余须要优化的页面,共勉。

PS:想更多的了解LULU UI,能够访问此连接:https://l-ui.com/

更多分享,请关注YFE:

相关文章
相关标签/搜索