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

本文作者:任家乐

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

性能风暴

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

风暴前夕

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

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

风暴现场

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

1. 内容区域 Loading 时间过长

章节数Loading 耗时 (s)
30006.519
500014.732
700024.753

▲ 打印时间戳取 10 次实验平均数得到

2. 勾选 Checkbox 响应慢

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

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

灾情分析与解决

一、老版本 UI 控件惹的祸

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

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

抓了下请求数据包,250kb 耗时 493ms ,可见瓶颈不在数据请求,于是用本地数据对步骤 2、3、4 做了性能测试,结果如下:

章节数渲染 EJS 模板DOM插入 EJS 模板初始化 Checkbox 实例
300092ms75ms6345ms
5000149ms133ms13948ms
7000278ms301ms23866ms

▲模拟次数: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 过程

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

QQ 空间 Onload 过
▲ QQ 空间 Onload 过程

内容分段加载势在必行,我们的分段加载策略:数据依旧一次性拉取,前端来进行分段渲染并分段插入 DOM 中。

最优的方案当然是后端改造接口来支持前端多次拉取章节数据,但时间紧迫,后端改造接口也需要一些时间,因此并没有做到请求的分段。前端分段数据结果如下表,Loading 时长差别不大。

章节数分段前 Loading 时长分段后 Loading 时长
30000.366s0.391s
50000.712s0.699s
70000.938s0.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,左图不使用计时器,右图使用计时器

四、减少 DOM 操作与访问

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

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

章节数优化前(ms)优化后(ms)
3000864177
50001317169
70001774180

▲ 取10次测试数据平均值

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

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

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

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

章节数数量(本)百分比
大于5000190.003%
大于2000 && 小于50002600.045%

▲ 书库数据

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

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

无 Loading 体验
▲ 无 Loading 体验

为了做更好的我们,一些压缩 AJAX 数据量的小优化也是不能够放弃哒,例如压缩字段、减少多余字段等。即使在 5000 章以上的数据量下,这些优化也能积少成多地减少传输量。

风暴平息

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

PS:想更多的了解LULU UI,可以访问此链接:https://l-ui.com/

更多分享,请关注YFE:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/367062.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python polar函数_Python可视化很简单,可是你会吗?python绘制饼图、极线图和气泡图,让我来教教你吧,一文教会!!!...

matplotlib库作为Python数据化可视化的最经典和最常用库&#xff0c;掌握了它就相当于学会了Python的数据化可视化&#xff0c;今天呢&#xff0c;咱们就一起来聊聊关于Python如何去绘制饼图、极线图和气泡图吧好啦&#xff0c;废话少说&#xff0c;咱们就开始吧&#xff01;用…

Java中的访问者设计模式–示例教程

访客模式是行为设计模式之一 。 当我们必须对一组相似类型的对象执行操作时&#xff0c;将使用访问者模式。 借助访问者模式&#xff0c;我们可以将操作逻辑从对象移动到另一个类。 例如&#xff0c;假设有一个购物车&#xff0c;我们可以在其中添加不同类型的项目&#xff08…

函数递归与二分法

1.什么是函数递归 函数的递归调用是函数嵌套调用的一种特殊形式&#xff0c; 特殊在调用一个函数的过程中又直接或者间接地调用了该函数本身 递归本质就是一个循环的过程&#xff0c; 但是递归必须满足两个原则&#xff1a; 1.每进入下一层递归&#xff0c;问题的规模必须有所减…

使用Oracle WebLogic创建部署计划

创建部署计划 部署计划是JSR-88部署标准的一部分&#xff0c;尽管在该标准中未明确说明。 部署计划是一个XML文档&#xff0c;用于定义自定义WebLogic Server部署环境。 此配置可用于覆盖在应用程序归档文件中定义的特定设置。 有许多原因可能导致您不希望修改应用程序存档的原…

记录一个前端架构的想法

前端&#xff0c;真的是让我哭笑不得的职业&#xff0c;从几年前作为打酱油的理想职业到现在的热门职业&#xff0c;无疑在这个过程中&#xff0c;门槛变高了&#xff0c;而且还是非常高。一大堆的框架和库&#xff0c;像什么vue啦、react啦、angular啦、webpack啦等等等等。让…

java 编程原理_Java网络编程 -- 网络编程基础原理

Hello&#xff0c;今天记录下 Java网络编程 --> 网络编程基础原理。一起学习&#xff0c;一起进步。继续沉淀&#xff0c;慢慢强大。希望这文章对您有帮助。若有写的不好的地方&#xff0c;欢迎评论给建议哈&#xff01;初写博客不久&#xff0c;我是杨展浩。这是我的第十五…

JavaScript基础之--- 深拷贝与浅拷贝

理解深拷贝和浅拷贝之前&#xff0c;先来看一下JavaScript的数据类型。 1、基本类型和引用类型 //案例1 var num1 1, num2 num1; console.log(num1) //1 console.log(num2) //1 num2 2; //修改num2 console.log(num1) //1 console.log(num2) //2 //案例2 var obj1 {x: 1, …

vue菜鸟从业记:完成项目最后一公里之真机测试和打包上线

最近我朋友王小闰他们公司的项目开发已经进入收尾阶段&#xff0c;前后端并行开发的差不多了&#xff0c;联调也调过了&#xff0c;上篇文章里也讲到了&#xff0c;所谓联调&#xff0c;就仿佛在说“我也不知道我的接口文档写的对不对&#xff0c;我们验证一下吧&#xff1f;我…

血淋淋的事实告诉你:你为什么不应该在JS文件中保存敏感信息

在JavaScript文件中存储敏感数据&#xff0c;不仅是一种错误的实践方式&#xff0c;而且还是一种非常危险的行为&#xff0c;长期以来大家都知道这一点。 而原因也非常简单&#xff0c;我们可以假设你为你的用户动态生成了一个包含API密钥的JavaScript文件&#xff1a; apiCall…

从零开始搭建一个vue.js的脚手架

在谷歌工作的时候&#xff0c;我们要做很多界面的原型&#xff0c;要求快速上手&#xff0c;灵活运用&#xff0c;当时用的一些现有框架&#xff0c;比如angular&#xff0c;太笨重了——尤雨溪&#xff08;Vue.js 作者&#xff09; vue.js是现在一个很火的前端框架&#xff0c…

更安全的Web通信HTTPS

1. HTTP协议存在的问题 阅读本篇需要对HTTP协议有最基本的了解。 借用《图解密码技术》里的图片&#xff0c;我们以如下一个购物场景开始介绍&#xff1a; 在网购过程中&#xff0c;如果使用纯粹的HTTP协议&#xff0c;那么用户的账号密码&#xff0c;信用卡&#xff0c;银行卡…

CSS入门指南——页面的水平居中

我们经常看到这样的网页&#xff0c;即内容水平居中在屏幕中间&#xff0c;左右留白。我们来给这样的布局起个名字——水平居中布局 其实要实现这样的布局十分简单&#xff0c;即给中间部分一个宽度&#xff0c;设置margin左右值为auto,如&#xff0c;中间部分class"main&…

mysql explain的使用

一、explain返回各列的含义&#xff1a; 1、table&#xff1a;显示这一行的数据是关于那张表的 2、type&#xff1a;重要的列&#xff0c;显示连接使用了何种类型&#xff0c;从最好到最差的连接类型为const、eq_reg、ref、range、index、ALL 3、possible_keys&#xff1a;显示…

使用 Canvas 生成公众号头图

熟悉“前端晚自修”的朋友们应该知道&#xff0c;我们每期的头图除了上面的文字随着每期变动以外&#xff0c;几乎是一模一样的&#xff08;因为太懒了~&#xff09;。这个头图虽然丑了一点&#xff0c;但是也还说的过去&#xff0c;毕竟是我倾尽毕生艺术细胞拼出来的&#xff…

Flask mysql 模版传参_Flask渲染Jinja2模板和传参

### Flask渲染Jinja2模板和传参&#xff1a;1. 如何渲染模板&#xff1a;* 模板放在templates文件夹下* 从flask中导入render_template函数。* 在视图函数中&#xff0c;使用render_template函数&#xff0c;渲染模板。注意&#xff1a;只需要填写模板的名字&#xff0c;不需要…

08 Spring框架 AOP (一)

首先我们先来介绍一下AOP&#xff1a; AOP&#xff08;Aspect Orient Programming&#xff09;&#xff0c;面向切面编程&#xff0c;是面向对象编程OOP的一种补充。 面向对象编程是从静态角度考虑程序的结构&#xff0c;面向切面编程是从动态的角度考虑程序运行过程。 AOP底层…

移动spa商城优化记(一)---首屏优化篇

背景 随着公司业务的不断壮大&#xff0c;最近老是有用户反应公司APP内的商城打开比较慢&#xff0c;这可不行啊&#xff0c;慢了容易流失用户&#xff0c;流失用户减少公司业绩&#xff0c;公司业绩少我的年终奖就少…………&#xff0c;所以为了公司&#xff0c;也为了自己&a…

hprose for java 教程_hprose for java源码分析-4

4.1 疑窦丛生书接上回。上回说到&#xff0c;从HproseClient.java ------------------------- (#0)invokeHandler.handle()开始&#xff0c;将经历一个漫长的调用过程&#xff0c;下面把整个调用链粘出来&#xff0c;先认识下这个庞然大物。( >>> 表示调用到&#xff…

java批量提取文件夹名称_bat 批量提取指定目录下的文件名

bat 批量提取指定目录下的文件名下面是批量获取指定目录下的文件名的核心代码echo offecho text inputset inputset /p input:echo %input% is inputcd %input%rem echo onfor %%a in (*) do (echo %%a is input)cd ..如下是sql server执行对应脚本文件sqlcmd -Spcserver -dmas…

埃及分数The Rotation Game骑士精神——IDA*

IDA*&#xff1a;非常好用的搜索&#xff0c;可以解决很多深度浅&#xff0c;但是规模大的搜索问题。 估价函数设计思路&#xff1a;观察一步最多能向答案靠近多少。 埃及分数 题目大意&#xff1a; 给出一个分数&#xff0c;由分子a 和分母b 构成&#xff0c;现在要你分解成一…