使用js在桌面上写一个倒计时器_论一个倒计时器的性能优化之路
原文发表于 2018.05.25,搬运自个人博客。
引子
回顾这半年,扛需求能力越来越强,业务代码也是越写越多。但稍一认真看看这些当时为了满足快速上线所码的东西,问题其实还是不少。这次就从一个简单的计时器说起。
现状
问题很明显
倒计时器组件在一个活动列表页面里被使用,列表中每一项都是一个促销活动入口。倒计时器位于每个活动区块的左上方,提醒用户该活动还有多久结束,如下动图所示(测试设备 SONY E5663,后同)。
当页面滑动时,可以明显看到计时器停止,这意味着页面并没有刷新。直到松手后一两秒才恢复计时,且不稳定,又卡顿了一到两秒。
如此明显的问题吓得笔者赶紧去后台查阅了该页面 PV 和 UV 数据,虽说不多,但还是有一批忠实用户每天访问,这可怎么对得起我们的衣食父母…!即便测试用的设备性能羸弱,更换 Chrome 模拟器以及 17 年的安卓旗舰机再次测试并未出现如此卡顿现象,但我们无法挑选客户使用的设备,只能从技术角度解决问题,尽量提升用户体验。BTW,这台 SONY 测试机就是由东南亚的业务方同学提供,应该是当地用户的常用机型之一。
打脸与自我打脸
倒计时器组件的更新逻辑抽象如下,简单概括就是使用 setInterval
定时更新 React 组件的状态以实现倒数时间的更新:
不得不说,贴出这么一段槽点满满的代码是极其需要勇气的,这… 居然是我写的?
那么开始分(tu)析(cao)吧,让我们自上而下依次盘点:
- 这段逻辑代码放在
componentWillMount
生命周期钩子里执行并不合适,其因有二:在componentWillMount
阶段还未加载真实的 DOM 节点,此时就开始更新数据没有什么意义;React 的 Reconciliation 算法以及目前最新的 Fiber 调度器算法会对渲染的开始或停止过程进行优化,例如合并几次渲染过程为一次,这可能会导致componentWillMount
被频繁调用。 - 每次更新数据后都将触发一次渲染 SOP,这无疑加大了性能开销。当动画刷新遇上大量运算,一首《凉凉》送给低端手机。
- 这样计时方式真的准吗?例如
setInterval
的精准性,又例如setState
方法的使用。
顺着这个思路,赶紧来改代码吧!
提升更新效率
更新速度有多慢?
首先花几秒钟把这段代码挪到 componentDidMount()
钩子里。
接下来,既然页面在 MBP 的 Chrome 模拟器上访问没有问题,那么可以做个简单的对比实验,看看手机与笔记本模拟器的性能差距。使用 performance.now
测量更新一次所花费的时间,示例代码如下:
从下方两张截图可以看到测试机与模拟器的性能相差十倍左右,且测试机的运算时间波动较大(下方上图为模拟器数据,下图为测试机数据):
其实上面的埋点代码添加在 setState
的回调函数里,就明显能说明一个问题:setState
方法并不保证同步渲染更新,尽管截图中的时序看上去是同步的。
重点是,整个更新渲染的周期非常长,即使降低至 30Hz 的流畅画面要求,一帧可用的渲染时间也只有不到 34 毫秒,还不是业务代码独享! 之所以渲染速度慢,是因为调用一次 setState
方法会依次执行 React 生命周期中的 4 个函数:shouldComponentUpdate
、componentWillUpdate
、render
和 componentDidUpdate
(如下图所示)。
直接撸 DOM,要啥 jQuery
为了性能,这里采用最为简单粗暴的方法,直接更新 DOM 节点的 HTML 值:
让我们来看看效果如何:模拟器上的更新时间缩短至 0.3 毫秒,比之前快了十几到二十几倍;测试机的数据也漂亮多了(如下图),再滑几下试试… 美滋滋!
更好的更新策略
定时器最重要的功能就是确保时间准确,如果时间都不准了,那也就该洗洗睡了。除去与服务端同步校时之类的方案,还是继续讨论如何在 Web 前端领域力求计时准确。
并不精准的 setInterval
在修复前文提到的 setState
缺陷之后,最明显的问题莫过于 setInterval
的使用。写一个定时任务,不少小伙伴第一反应想到的也是 setTimeout
和 setInterval
函数,但是它们真的足够精确吗?这就要从 JS 的任务队列及微任务队列(也有称 macrotask queue 和 microtask queue)说起了…
咳咳,我们言简意赅总结下:JS 主线程执行时有一个栈存储运行时的函数相关变量,遇到函数时会先入栈执行完后再出栈(废话)。当遇到 setTimeout
setInterval
requestAnimationFrame
以及 I/O 操作时,这些函数会立刻返回一个值(如 setInterval
返回一个 intervalID
)保证主线程继续执行,而异步操作则由浏览器的其它线程维护。当异步操作完成时,浏览器会将其回调函数插入主线程的任务队列中,当主线程执行完当前栈的逻辑后,才会依次执行任务队列中的任务。
但是在每个任务之间,还有一个微任务队列的存在。在当前任务执行完后,将先执行微任务队列中的所有任务,例如 Promise
process.nextTick
等操作。也就是说当 setInterval(fn, 1000)
等待 1 秒钟后,fn
函数会被插入任务队列中,但并不一定会立刻执行,还需要等待当前任务以及微任务队列中的所有任务执行完。长此以往,使用 setInterval
的计时器超时将越来越严重。
如果有毅力的朋友推荐看看权威的 HTML 标准文档,没耐心的就看看这个动图简单感受一下原理吧。
所以回归正题,不用 setInterval
那用啥?
天王盖地虎,我有 rAF
解铃还须系铃人,既然我们的代码执行时间在主线程中无法得到保证,那么还是要从更高抽象层级的浏览器中寻求办法。好在目前主流浏览器都已提供一个在重绘前执行动画相关函数的接口 requestAnimationFrame
,用来更新计时器再合适不过。改造如下:
那么这样实现足够精准了吗?打印出每次更新的时间戳瞅瞅(上图为模拟器数据,下图为测试机数据)。
可以看到模拟器上已经相当精准,每秒的误差在 +0.15 毫秒左右,也就是运行将近 2 小时会有 1 秒的误差,笔者觉得完全可以接受。不过测试机上的误差就有点大了,每秒的误差在 +10 毫秒左右,虽然笔者觉得也可以接受(很少有人会在活动页停留很久),但本着工(tai)匠(gang)精神,想想是否还能优化呢?
正向反馈拯救采样频率
好奇心使笔者打印出了测试机调用 rAF
的时间间隔,绝大多数间隔在 16.6 毫秒左右,意味着手机 webview 也是 60Hz 的刷新频率;不过也存在少数间隔时间远超正常刷新时间,达到了 30 ~ 70 毫秒,如果触发滑动操作可能会超过 100 毫秒。不得不说,测试机就要挑这么烂的 Orz…
仔细想想,测试机上的计时误差本质是采样频率并未一直满足 60Hz,当某一次采样时间超过 16.6 毫秒且刚好需要刷新动画时,就会产生误差。同时每次误差都是超时而非提前,这样就在延时的道路上越走越远了。
那么反向思考,每当触发更新事件时,超时时段(超过 1 秒的时间)是已知的。如果将其补偿到下一次计时中,应该能减缓误差的扩大速度。代码如下:
观察测试手机打印的时间,发现此法完全是可行的。每当超时间隔超过正常的刷新频率 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 数组去重
使用js在桌面上写一个倒计时器_论一个倒计时器的性能优化之路相关推荐
- 使用js在桌面上写一个倒计时器_现代课程网教学互动平台课件编辑器:计时器、倒计时器(图文版)...
现代课程网教学互动平台 课件编辑器 计时器使用指南 01 功能介绍 计时器插件就是可以从零开始计时的插件. 02 编辑插件 1 在顶部菜单栏里找到"工具控件",在下拉菜单中找到&q ...
- iphone桌面上的圆圈怎么设置_苹果手机桌面上找不到便签记事本怎么办?有办法添加吗...
原标题:苹果手机桌面上找不到便签记事本怎么办?有办法添加吗 习惯了使用安卓手机的小伙伴,可能很多人都会选择在便签或者记事本里记录待办事项.因为随着移动互联网的发展,现在很多安卓手机上都有系统自带的便签 ...
- python定义一个triangle类_设计一个程序,程序中有三个类,Triangle,Lader,Circle。...
//此程序写出三个类,triangle,lader,circle:其中triangle类具有类型为double的a,b,c边以及周长,面积属性, //具有周长,面积以及修改三边的功能,还有判断能否构成 ...
- nginx 上传 文件超时设置_Nginx在高并发下的性能优化点!有这篇就够了!
点击上方"java进阶架构师",选择右上角"置顶公众号" 20大进阶架构专题每日送达 前面几周,讲过Nginx的日志配置:Nginx | 超详细!Nginx 日 ...
- mysql数据库新建一个递增的_分享一个mysql实验—基于数据库事务隔离级别RR及RC的测试...
概述 今天主要分享一个最近做的实验,主要是测试mysql数据库在RR和RC不同事务隔离级别下的表现. MySQL使用可重复读来作为默认隔离级别的主要原因是语句级的Binlog.RR能提供SQL语句的写 ...
- vue 如何生成一个dom元素_通过一个简单的示例学习如何编写Vue组件
大家好,本篇文章我将带着大家一起学习如何编写自定义组件(Components),通过「vue基础」新手快速入门篇(一)这篇文章的学习,我们知道了 Vue 设计的目的就是为了方便我们创建基于组件UI的项 ...
- 将一个项目中的图片存到另一个项目中_复盘一个Smart200小项目(2)
这一部分回顾一下确认项目功能需求的过程. 硬件系统选择: 第一阶段,陆陆续续在微信和电话上扯了大约一个周,确定了采用高级语言开发上位机的方案,准备开始确定采用什么PLC和触摸屏了,哥们说客户对使用什么 ...
- 第一个python解释器_第一个python程序
回顾昨日重点(需记住5*) 三大核心: cpu--运行程序 内存--基于电,存取速度快,断电丢失数据 硬盘--基于磁,存取速度远远慢于内存,断电不丢失数据 操作系统 应用程序 操作系统 (window ...
- eclipse把tomcant用到一个项目里_聊一个镜头工艺里容易被忽略,但很重要的项目...
在不改换门庭的情况下,一颗镜头一般都会伴随大家使用很长一段时间,也相信大多数人都遇到过剐蹭镜头前组的情况,这时候最容易引发的担忧就是"伤着镀膜了么?会不会影响成像效果?" 其实换个 ...
最新文章
- html5 网页游戏 开源,HTML5 网页游戏,基于 WebGL 打造
- gitblit mysql_Gitblit服务器搭建及IDEA整合Git使用
- JSjQuery全选反选父项子项联动多选框
- android 模仿instagram的listview,实现Instagram的Material Design概念设计
- Hibernate查询缓存如何工作
- Java基础——类加载机制及原理
- maven错误解决:编码GBK的不可映射字符
- kaffe java_Java虚拟机Kaffe的研究与实现
- MAC-快捷键打开终端
- html messagebox确定取消,提示组件 - MessageBox 弹框 - 《ElementUI v2.15 使用手册》 - 书栈网 · BookStack...
- 数据分析之股票市场价格分析
- Git上传文件时报错:The authenticity of host xxx can‘t be established.
- 兔斯基QQ表情全集GIF篇下载(到8月2日)234整理
- 天下熙熙皆为利来 天下攘攘皆为利往
- 完美攻略心得之圣魔大战3(Castle Fantisia)艾伦希亚战记(艾伦西亚战记)包括重做版(即新艾伦希亚战记)
- python实现登录支付宝收能量_2019年Python常见面试题(北、上、广、深、杭、南)...
- json_tuple,get_json_object提取日志埋点信息
- C#构造函数和析构函数
- java基础-Objcet根父类
- JAVA--命令行窗口-java运行报错:Error: A JNI error has occurred,please check your installation and try again
热门文章
- 我改了500个Bug,但是!!
- win7系统如何访问xp系统的服务器,WIN7系统怎么让XP系统访问呢
- 企业联合体的形式_联合体如何联合?——总包新政下,联合体如何联合?系列三...
- vba 当前文件名_VBA代码解决方案第77讲内容:如何导出文件
- Terminal中输入一行命令快速移动光标至行首行尾
- 视频干扰的原因及解决方法
- 渝粤教育,我是客服,2022重返王者荣耀,再露凶残,欢迎约战
- 【渝粤题库】国家开放大学2021春1366英语教学理论与实践题目
- 【lora模块技术无线数传电台】E90-DTU产品高防护等级的体现
- android 自定义baseadapter listview,android之ListView和BaseAdapter的组合使用