使用js在桌面上写一个倒计时器_论一个倒计时器的性能优化之路

原文发表于 2018.05.25,搬运自个人博客。

引子

回顾这半年,扛需求能力越来越强,业务代码也是越写越多。但稍一认真看看这些当时为了满足快速上线所码的东西,问题其实还是不少。这次就从一个简单的计时器说起。

现状

问题很明显

倒计时器组件在一个活动列表页面里被使用,列表中每一项都是一个促销活动入口。倒计时器位于每个活动区块的左上方,提醒用户该活动还有多久结束,如下动图所示(测试设备 SONY E5663,后同)。

87e90321858dac9c17c71fa974661396.gif

当页面滑动时,可以明显看到计时器停止,这意味着页面并没有刷新。直到松手后一两秒才恢复计时,且不稳定,又卡顿了一到两秒。

如此明显的问题吓得笔者赶紧去后台查阅了该页面 PV 和 UV 数据,虽说不多,但还是有一批忠实用户每天访问,这可怎么对得起我们的衣食父母…!即便测试用的设备性能羸弱,更换 Chrome 模拟器以及 17 年的安卓旗舰机再次测试并未出现如此卡顿现象,但我们无法挑选客户使用的设备,只能从技术角度解决问题,尽量提升用户体验。BTW,这台 SONY 测试机就是由东南亚的业务方同学提供,应该是当地用户的常用机型之一。

打脸与自我打脸

倒计时器组件的更新逻辑抽象如下,简单概括就是使用 setInterval 定时更新 React 组件的状态以实现倒数时间的更新:

6ee0dc272774fad86e296d66d7c63513.png

不得不说,贴出这么一段槽点满满的代码是极其需要勇气的,这… 居然是我写的?

fdfe8b7da356e79b4e93337179203342.png

那么开始分(tu)析(cao)吧,让我们自上而下依次盘点:

  1. 这段逻辑代码放在 componentWillMount 生命周期钩子里执行并不合适,其因有二:在 componentWillMount 阶段还未加载真实的 DOM 节点,此时就开始更新数据没有什么意义;React 的 Reconciliation 算法以及目前最新的 Fiber 调度器算法会对渲染的开始或停止过程进行优化,例如合并几次渲染过程为一次,这可能会导致 componentWillMount 被频繁调用。
  2. 每次更新数据后都将触发一次渲染 SOP,这无疑加大了性能开销。当动画刷新遇上大量运算,一首《凉凉》送给低端手机。
  3. 这样计时方式真的准吗?例如 setInterval 的精准性,又例如 setState 方法的使用。

顺着这个思路,赶紧来改代码吧!

提升更新效率

更新速度有多慢?

首先花几秒钟把这段代码挪到 componentDidMount() 钩子里。

接下来,既然页面在 MBP 的 Chrome 模拟器上访问没有问题,那么可以做个简单的对比实验,看看手机与笔记本模拟器的性能差距。使用 performance.now 测量更新一次所花费的时间,示例代码如下:

92359ca7afba940903c9bed90e94206b.png

从下方两张截图可以看到测试机与模拟器的性能相差十倍左右,且测试机的运算时间波动较大(下方上图为模拟器数据,下图为测试机数据):

46862bbf278135a073059f24ee9688f3.png

121df19a50ca98fe84ad52afcd5c9129.png

其实上面的埋点代码添加在 setState 的回调函数里,就明显能说明一个问题:setState 方法并不保证同步渲染更新,尽管截图中的时序看上去是同步的。

重点是,整个更新渲染的周期非常长,即使降低至 30Hz 的流畅画面要求,一帧可用的渲染时间也只有不到 34 毫秒,还不是业务代码独享! 之所以渲染速度慢,是因为调用一次 setState 方法会依次执行 React 生命周期中的 4 个函数:shouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate (如下图所示)。

018fe49cebb4c39852d1119731321f32.png
Source: https://bit.ly/2Pb6sn5

直接撸 DOM,要啥 jQuery

为了性能,这里采用最为简单粗暴的方法,直接更新 DOM 节点的 HTML 值:

9bc20e83504f0dc161209c168bbc9179.png

让我们来看看效果如何:模拟器上的更新时间缩短至 0.3 毫秒,比之前快了十几到二十几倍;测试机的数据也漂亮多了(如下图),再滑几下试试… 美滋滋!

f67d52021f95e0077b40be55b69998f7.png

c7a4e3ffac18d2415b66b1fac273b7e5.gif

更好的更新策略

定时器最重要的功能就是确保时间准确,如果时间都不准了,那也就该洗洗睡了。除去与服务端同步校时之类的方案,还是继续讨论如何在 Web 前端领域力求计时准确。

并不精准的 setInterval

在修复前文提到的 setState 缺陷之后,最明显的问题莫过于 setInterval 的使用。写一个定时任务,不少小伙伴第一反应想到的也是 setTimeoutsetInterval 函数,但是它们真的足够精确吗?这就要从 JS 的任务队列微任务队列(也有称 macrotask queuemicrotask queue)说起了…

咳咳,我们言简意赅总结下:JS 主线程执行时有一个栈存储运行时的函数相关变量,遇到函数时会先入栈执行完后再出栈(废话)。当遇到 setTimeout setInterval requestAnimationFrame 以及 I/O 操作时,这些函数会立刻返回一个值(如 setInterval 返回一个 intervalID )保证主线程继续执行,而异步操作则由浏览器的其它线程维护。当异步操作完成时,浏览器会将其回调函数插入主线程的任务队列中,当主线程执行完当前栈的逻辑后,才会依次执行任务队列中的任务。

但是在每个任务之间,还有一个微任务队列的存在。在当前任务执行完后,将先执行微任务队列中的所有任务,例如 Promise process.nextTick 等操作。也就是说当 setInterval(fn, 1000) 等待 1 秒钟后,fn 函数会被插入任务队列中,但并不一定会立刻执行,还需要等待当前任务以及微任务队列中的所有任务执行完。长此以往,使用 setInterval 的计时器超时将越来越严重。

如果有毅力的朋友推荐看看权威的 HTML 标准文档,没耐心的就看看这个动图简单感受一下原理吧。

673234d8f065bedd38ab3b9cac69aca4.gif

所以回归正题,不用 setInterval 那用啥?

天王盖地虎,我有 rAF

解铃还须系铃人,既然我们的代码执行时间在主线程中无法得到保证,那么还是要从更高抽象层级的浏览器中寻求办法。好在目前主流浏览器都已提供一个在重绘前执行动画相关函数的接口 requestAnimationFrame,用来更新计时器再合适不过。改造如下:

ecd3aae91b20e9ddefa9d18be08684f3.png

那么这样实现足够精准了吗?打印出每次更新的时间戳瞅瞅(上图为模拟器数据,下图为测试机数据)。

99b1a3aa979d26f1aebd950d136e06b7.png

008dd6d2f6ba963117b7552097a85e93.png

可以看到模拟器上已经相当精准,每秒的误差在 +0.15 毫秒左右,也就是运行将近 2 小时会有 1 秒的误差,笔者觉得完全可以接受。不过测试机上的误差就有点大了,每秒的误差在 +10 毫秒左右,虽然笔者觉得也可以接受(很少有人会在活动页停留很久),但本着工(tai)匠(gang)精神,想想是否还能优化呢?

正向反馈拯救采样频率

好奇心使笔者打印出了测试机调用 rAF 的时间间隔,绝大多数间隔在 16.6 毫秒左右,意味着手机 webview 也是 60Hz 的刷新频率;不过也存在少数间隔时间远超正常刷新时间,达到了 30 ~ 70 毫秒,如果触发滑动操作可能会超过 100 毫秒。不得不说,测试机就要挑这么烂的 Orz…

仔细想想,测试机上的计时误差本质是采样频率并未一直满足 60Hz,当某一次采样时间超过 16.6 毫秒且刚好需要刷新动画时,就会产生误差。同时每次误差都是超时而非提前,这样就在延时的道路上越走越远了。

那么反向思考,每当触发更新事件时,超时时段(超过 1 秒的时间)是已知的。如果将其补偿到下一次计时中,应该能减缓误差的扩大速度。代码如下:

1035ab97a05debe15f38d17db0c1bd81.png

观察测试手机打印的时间,发现此法完全是可行的。每当超时间隔超过正常的刷新频率 16.6 毫秒时,相当于赶上了下一次采样窗口的伊始,因此会被校正。相比手机上每隔两三秒校正一次,PC 模拟器的采样时间变化显得尤为明显。

Reference

  • Tasks, microtasks, queues and schedules
  • How does a single thread handle asynchronous code in JavaScript?
  • HTML Living Standard — Last Updated 25 May 2018
  • Window.requestAnimationFrame()
  • 本文作者: John Chou
  • 本文链接: https://blog.joouis.com/2018/05/25/optimization-road-of-count-down-timer/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-ND 许可协议。转载请注明出处!

相关文章

  • Javascript 简洁之道:如何使用类重构
  • JavaScript 性能优化概观
  • Weex Android 发车指南(已弃车)
  • 十分钟带你了解国产自制开源插件 structure-view
  • 小议 Javascript 数组去重

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

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

相关文章

Vue 导入文件import、路径@和.的区别

***import: html文件中,通过script标签引入js文件。而vue中,通过import xxx from xxx路径的方式导入文件,不光可以导入js文件。 from前的:“xxx”指的是为导入的文件起一个名称,不是指导入的文件的名称&…

python split()

python函数:split() Python中有split()和os.path.split()两个函数,具体作用如下: split():拆分字符串。通过指定分隔符对字符串进行切片,并返回分割后的字符串列表(list) os.pa…

windows聚焦图片为什么不更新了_为什么年轻明星都不愿意接周星驰的戏? 林更新道出了事情的真相|周星驰|林更新|喜剧之王|演员...

要说华语电影史上最有成就的喜剧之王,那么大家脑中一定会闪过周星驰的名字,相信在不少60后、70后心目中,都有周星驰的一席之地。《大话西游》、《逃学威龙》、《九品芝麻官》等一系列作品都是非常经典的作品,就算是拿到当下来看都…

多线程多进程解析:Python、os、sys、Queue、multiprocessing、threading

当涉及到操作系统的时候,免不了要使用os模块,有时还要用到sys模块。 设计到并行程序,一般开单独的进程,而不是线程,原因是python解释器的全局解释器锁GIL(global interpreter lock),…

练习1

方法一: 统计在一个队列中的数字,有多少个正数,多少个负数,如[1, 3, 5, 7, 0, -1, -9, -4, -5, 8] lists [1, 3, 5, 7, 0, -1, -9, -4, -5, 8]positive 0 negative 0for number in lists:if number > 0:positive 1elif nu…

python sort()、sorted()

python sort、sorted排序 这篇文章主要介绍了python sort、sorted高级排序技巧,本文讲解了基础排序、升序和降序、排序的稳定性和复杂排序、cmp函数排序法等内容. python list内置sort()方法用来排序,也可以用python内置的全局sorted()方法来对可迭代的序列排…

电脑内存占用莫名很高_CPU占用高,电脑莫名卡顿?万能的重启拯救不了就用这3招,妥了!...

大家还记得windows 10 1903推送时发生的大翻车事件吗?那次的更新主要是修复早期版本的Visual Basic 6、VBA和VBScript无反应、远端桌面出现当机黑屏幕等问题,但win10的更新总是“捡了芝麻,丢了西瓜”,解决早期问题而又出现新的问题…

5. 多线程程序如何让 IO 和“计算”相互重叠,降低 latency?

基本思路是,把 IO 操作(通常是写操作)通过 BlockingQueue 交给别的线程去做,自己不必等待。 例1: logging 在多线程服务器程序中,日志 (logging) 至关重要,本例仅考虑写 log file 的情况,不考…

tomcat web应用_具有可执行Tomcat的独立Web应用程序

tomcat web应用在部署应用程序时,简单性是最大的优势。 您将了解到,尤其是在项目发展且需要在环境中进行某些更改时。 将您的整个应用程序打包到一个独立且自足的JAR中似乎是个好主意,特别是与在目标环境中安装和升级Tomcat相比。 过去&#…

anaconda在ubuntu中添加环境变量

在ubuntu上安装好anaconda后,如果输入conda命令报错,很有可能需要以下修改注册表(相当于windows中将路径添加到系统环境变量) ~ /anaconda3/bin为.Sh所在home目录路径 在终端输入:sudo gedit ~/.bashrc 打开注册表后…

webpackjsonp 还原_具有催化CO2还原性能的非贵金属配合物的配体设计

Non-noble metal-based molecular complexes for CO2 reduction: From the ligand design perspectiveDong-Cheng Liu, Di-Chang Zhong, Tong-Bu LuEneryChem, 2, 100034 (2020).DOI: https://doi.org/10.1016/j.enchem.2020.100034全文链接https://www.sciencedirect.com/jour…

【数据库系统概论】第3章-关系数据库标准语言SQL(1)

文章目录 3.1 SQL概述3.2 学生-课程数据库3.3 数据定义3.3.1 数据库定义3.3.2 模式的定义3.3.3 基本表的定义3.3.4 索引的建立与删除3.3.5 数据字典 3.1 SQL概述 动词 分类 三级模式 3.2 学生-课程数据库 3.3 数据定义 3.3.1 数据库定义 创建数据库 tips:[ ]表…

适用于Java开发人员的Elasticsearch教程

课程大纲 Elasticsearch是基于Lucene的搜索引擎。 它提供了具有HTTP Web界面和无模式JSON文档的分布式多租户全文搜索引擎。 Elasticsearch是用Java开发的,并根据Apache许可的条款作为开源发布。 Elasticsearch是最受欢迎的企业搜索引擎,紧随其后的也是基…

shell实战之tomcat看门狗

1、脚本简介 tomcat看门狗,在tomcat进程异常退出时会自动拉起tomcat进程并记录tomcat运行的日志。 1 函数说明: 2 log_info:打印日志的函数,入参为需要在日志中打印的msg 3 start_tom:启动tomcat的函数…

tensorflow tf.train.batch()

tf.train.batch([example, label],batch_sizebatch_size, capacitycapacity) [example, label]表示样本和样本标签,这个可以是一个样本和一个样本标签,batch_size是返回的一个batch样本集的样本个数。capacity是队列中的容量。这主要是按顺序组合成一个b…

苹果6s上市时间_iPhone7的A10处理器还能战多长时间?2-3年不成问题!

iPhone 7采用A10 Fusion处理器,简称A10处理器,在2018年依然是处于高端处理器,再加上苹果自己的系统优化和资源调度,流畅度甚至超过其他安卓835机子。16年上市的iPhone7的A10还能再战多长时间?小编今天来分析一下。A10处…

tf.summary.FileWriter

ummary_waiter tf.summary.FileWriter("log",tf.get_default_graph()) log是事件文件所在的目录,这里是工程目录下的log目录。第二个参数是事件文件要记录的图,也就是tensorflow默认的图。

83998 连接服务器出错_服务端 TCP 连接的 TIME_WAIT 问题分析与解决

民工哥技术之路 写在开头,大概 4 年前,听到运维同学提到 TIME_WAIT 状态的 TCP 连接过多的问题,但是当时没有去细琢磨;最近又听人说起,是一个新手进行压测过程中,遇到的问题,因此,花…

SqlServer 时间格式化

select GETDATE() as 当前日期, DateName(year,GetDate()) as 年,DateName(month,GetDate()) as 月,DateName(day,GetDate()) as 日,DateName(dw,GetDate()) as 星期,DateName(week,GetDate()) as 周数,DateName(hour,GetDate()) as 时,DateName(minute,GetDate()) as 分,DateN…

[EBOOK]十大Java性能问题

有兴趣了解更多吗? 然后,您应该在此处下载相关的电子书。 Java中的大多数性能问题都可以归因于一些根本原因。 当然,偶尔会有一些奇怪的极端情况突然出现,并在应用程序中造成了严重破坏,但是在大多数情况下&#xff0…