背景

公司项目最近用到甘特图功能,于是集成了一款开源的甘特图插件。

甘特图的主要作用是项目管理,可以用图示的方式通过活动列表和时间刻度形象地表示出任何特定项目的活动顺序与持续时间,如下图

image.png

玩过甘特图的同学都知道,甘特图的前端实现基本靠绘画。而绘画是对前端的开发和性能要求非常高的一项技术。而频繁的交互操作,也会导致开发的性能要求进一步严格。

现象

其基础现象很简单。当我拖动甘特图的视图区域时,明显感受到卡顿和拖影。各位同学都明白,涉及到绘画相关的动画操作,要60fps才能够到顺滑的阶段。30fps勉强卡顿,20fps就卡顿拖影严重了。

于是我采用了 Frame Rendering Stats 工具先进行肉眼观察帧率数值。他的主要作用除了观察当前页面操作的fps数值外,还可以监控gpu的内存用量。当然这个工具的位置也很容易找。就在 Chrome Devtools 的 Rendering 选项中,勾选开启即可

image.png

当我使用工具进行 fps 的观察,同时视图区域进行稳定匀速的滑动时,能够感受到明显的卡顿和拖影。其检测数值最高仅有31fps,最低有26fps,卡顿的级别基本上属于严重卡顿。如果换一台低端一点的设备,那么其展示效果肯定无法想象。

image.png

分析

既然我们发现了问题,那么就分析下问题到底出在哪里。接着打开 Performance 工具并开始录制,录制的同时对视图区域进行稳定匀速的滑动,滑动几秒后停止录制,拿到一份这样的分析报告:

image.png

甘特图插件和主要技术栈都是react。在 react 16最新的fiber架构中,为了让响应可以更快的得到反馈,拆分了子任务。而根据一般的显示器刷新率(60hz)和目前的浏览器所支持的最高刷新率来算,平均下来每一帧的任务时长一般只有16.6ms。当单帧任务时长超过16.6ms时,就会产生卡顿和掉帧。

但是根据分析情况来看,上图滚动时产生的任务绝大多数都大于40ms,甚至还会产生longtask(Chrome官方对longtask的定义是大于50ms,即20fps)。所以接着展开来看,看看单任务中到底是哪些事件导致的执行时间长。

接着点开其中一个任务,放大详情。可以看到selftime(自身执行时间)排名第一的是一个匿名函数。继续点开右侧的代码堆栈,去看看哪行代码执行时间比较长。

image.png

点开后,会自动帮我们跳转到 Devtools 中的 source 模块,还会将代码的执行时间标在函数的左侧。从下文可以分析,第74行的 toLocaleDateString 的耗时非常严重。因为函数组件/类组件的渲染生成是同步的,所以耗时长会拖慢 render 的效率,进而拖慢整体的帧率。

image.png

时区转换的锅

Date.prototype.toLocaleDateString() 的作用是对不同语言的时间文本进行转换。例如

const event = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
console.log(event.toLocaleDateString('de-DE', options));
// expected output: Donnerstag, 20. Dezember 2012
console.log(event.toLocaleDateString('ar-EG', options));
// expected output: الخميس، ٢٠ ديسمبر، ٢٠١٢
console.log(event.toLocaleDateString(undefined, options));
// expected output: Thursday, December 20, 2012 (varies according to default locale)

但是这样一个看起来人畜无害的方法,怎么会耗时这么长呢?

鉴于直接翻看v8的这部分源码比较硬核,我们选择去查看 toLocaleDateString 的 polyfill —— formatjs。这个库一直作为 Date 方面的国际化polyfill存在着,包括时区国际化和时间文本国际化。

我们找到 formatjs 中的 packages/intl-datetimeformat/src/to_locale_string.ts 中的 toLocaleString 方法。这个方法创造了一个时间格式化对象:

image.png

继续跟踪 DateTimeFormat 类的实现,可以看到有一个叫做 localeData 的变量。这个变量就是我们做国际化时的各国语言文本内容。同样上面有一个叫 tzData 变量,是时区数据库的内容:

image.png

接着一路跟踪,会发现 ResolveLocale 方法是处理当前选中时区的核心方法。在那里面,所有的国际化文本都会经过运算筛选,再与当前选中的语言文本进行匹配(尤其是下图当中这种 indexOf 高耗时方法)

image.png

解决方案

对于此类调用耗时问题,唯一的解决就是对现有的执行结果加缓存memo。方案很简单:将时间转化为时间戳作为缓存的key,存入缓存,后续直接从缓存读取即可:

image.png

优化后,我们再次用performance进行分析。发现不仅fps有肉眼和数值的显著提升,且longtask也不再存在,平均任务耗时被压缩到了23ms。基本上实现了流畅,解决了卡顿问题。

image.png

但,我们还要继续解决 toLocaleDateString 的兄弟api:Intl.DateTimeFormat

关于 Intl.DateTimeFormat

Intl.DateTimeFormat 是一个比较新的时间格式化api。他与 toLocaleDateString 在使用上最大的不同时,支持对任意的date对象进行format,api设计上偏向构造器,更加利于缓存设计。例如用法:

console.log(new Intl.DateTimeFormat('en-US').format(date));
// expected output: "12/20/2020"
console.log(new Intl.DateTimeFormat('en-GB', { dateStyle: 'full', timeStyle: 'long' }).format(date));
// Expected output "Sunday, 20 December 2020 at 14:23:16 GMT+11"

同样,在对上面的 toLocaleDateString 进行性能优化完毕后,排在react耗时后面的 Intl.DateTimeFormat 也值得处理。继续查看代码耗时:

image.png

image.png

发现此方法的耗时也不低:7.1ms,有提升空间。而此方法的polyfill实现,也和上面的 toLocaleDateString 一致,都是实例化 DateTimeFormat 对象才可以用。只不过区别是一个手动实例化,一个帮你实例化:

image.png

那我们就继续对 Intl.DateTimeFormat 增加缓存。

最终优化结果

按照对 toLocaleDateString 的优化思路,我们只需要对 Intl.DateTimeFormat 实例进行优化即可。依然是做缓存,只不过 key 换成了地区 + 转化选项这唯一的参数:

image.png

优化完毕后,我们再次采集一份performance样本。

通过检测,fps已经达到了最低45,最高50的数据。基本上实现流畅(因Devtools开启状态下也耗性能,实际使用帧率比这个高)。相比优化前,提升了61%。long task消失不存在

image.png

结尾

当然,这份优化历程只是个初步优化。可以看到,虽然单个任务的耗时有所大幅度下降,但是还有提升空间存在。要尽量低于16.6ms才能够实现完全流畅。

总结一下:尽量采用 Intl.DateTimeFormat 来替代 toLocaleDateString,并对构造器进行缓存提升性能。在其他国际化的场景(例如数字等)也要注意这一点

此外,这份性能优化的方案已经提交给了上游开源项目,并在8.15日已经合并进仓库:https://github.com/MaTeMaTuK/gantt-task-react/pull/19

image

记一次 React 开源甘特图组件的性能优化,已合入 PR!相关推荐

  1. Twproject Gantt – 开源的 JavaScript 甘特图组件

    Twproject Gantt 是一款基于 jQuery 开发的甘特图组件,也可以创建其它图表,例如任务树(Task Trees).内置编辑.缩放和 CSS 皮肤等功能.更重要的是, 它是免费开源的. ...

  2. 前端甘特图组件开发(一)

    背景 工作中需要在网页上实现甘特图,以展示进度数据.通过网上调研相关项目,找到一款 dhtmlx-gantt 组件,在低程度上满足项目需求,但在部分定制功能(如时间轴自定义.编辑弹窗样式风格等)并不能 ...

  3. vue 改变domclass_基于 vue 开发甘特图组件的心路历程(兼设计分享)

    语雀原文 有更好的排版体验~ 这篇文章主要讲述笔者开发 v-gantt 甘特图组件的经过. 起源 公司项目有个甘特图的需求. 笔者考察了世面上 常见的甘特图组件 后,本着 我上我也行 的心态,以及考虑 ...

  4. JavaScript多功能甘特图组件 - jsGantt

    jsGantt 是一个可定制的.灵活的.多语言的甘特图组件,由原生 JavaScript 构建.它使用客户端渲染以获得快速的性能和动态的交互性.非常适用于任何需要交互式时间线或时间表显示的项目. 更多 ...

  5. 强大js web甘特图制作之甘特图组件和数据对象

    引用CSS和JS 使用EdoGantt是一件简单轻松的事,首先我们在HTML页面内引用CSS和JS: <!--edo css--><link href="http://ww ...

  6. 前端甘特图组件开发(二)

    自定义时间轴功能 由于常见的甘特图时间轴只支持按照天/周/月/年或者固定的时间单位进行划分,但实际使用场景下有时候会需要按照特定且不规则的时间段对进度数据进行直观划分.因此本组件在原时间轴的基础上添加 ...

  7. AnyGantt Flash甘特图组件免费下载及使用教程

    原文来自龙博方案网http://www.fanganwang.com/product/1455转载请注明出处 AnyGantt是一个强大的,基于Flash的数据可视化解决方案.允许任何人利用有力的动画 ...

  8. 我在大厂写React学到了什么?性能优化篇

    前言 我工作中的技术栈主要是 React + TypeScript,这篇文章我想总结一下如何在项目中运用 React 的一些技巧去进行性能优化,或者更好的代码组织. 性能优化的重要性不用多说,谷歌发布 ...

  9. 从工具到社区,美图秀秀大规模性能优化实践

    导读:本文由演讲整理而成.美图秀秀社区自上线以来已经有近一年时间,不管是秀秀海量的用户还是图片社区特有的形态都给性能优化提出了巨大的挑战.本文将会结合这一年内我们遇到的具体案例和大家分享下美图秀秀社区 ...

最新文章

  1. Apache Ant安装 验证
  2. 自定义元类控制类的实例化行为
  3. C语言面试高频问题:自己代码实现字符串相关的常用API
  4. note05-计算机网络
  5. Linux 系统应用编程——多线程经典问题(生产者-消费者)
  6. 两种实现简单cp的方法
  7. 惠新宸php教程_百度PHP高级顾问惠新宸:PHP在百度的发展历程
  8. UISC-User Interface States Control ;Murphy 用户界面状态控制(Beta)
  9. 移动Web—CSS为Retina屏幕替换更高质量的图片
  10. Windows 用户怒了!系统漏洞简直泛滥成灾
  11. .net core 部署应用程序注意事项
  12. 中琛物联‘连接+云+数据’服务助阵
  13. Spring中的AOP(二)——AOP基本概念和Spring对AOP的支持
  14. unity3d发布linux版本_密码管理器 1Password 发布第一个 Linux 测试版本
  15. Texar安装、Textgenrnn安装
  16. es6之模块化(module)--绝对能看懂
  17. [软件启动就报错退出]的问题解决
  18. 发现美,欣赏美,美之我见
  19. h2ouve下载 insyde_神舟tx6zx6gx9tx9蓝天模具解锁bios高级菜单
  20. MFC控件响应鼠标中键OnMouseWheel

热门文章

  1. 游戏背后的设计之路——3D建模
  2. 小米林斌:印度粉丝比国内还疯狂
  3. 创业就是和靠谱的人一起做热爱的事 印象笔记CEO谈创业
  4. python入门:NO.9 input函数
  5. 【软件定义汽车】【服务篇】SOA的核心----服务化
  6. JKTD-1000型铁电材料特性测试系统(铁电材料综合测试)
  7. android 开发百度地图问题集锦
  8. 二维数组或三维数组转换为一维数组
  9. python写超市管理系统_控制台超市系统(Python)
  10. Oracle日期常用函数(SYSDATE)