在 G2 4.0 的重构和 G2Plot 的开发过程中,我们主要关注在架构、交互和图表的体验上;这次我们对 G2 的性能做一个整体的评估,分析其中的性能问题点,最终完成性能的优化;使得 G2 在大数据量情况下也拥有良好的性能。
文中描述的模块性能优化的方法,也可以借鉴到其他的地方。
性能评估
先评估下 G2 目前的性能数据:可以想象到的影响 G2 渲染性能的自变量包括:数据量、Geometry 类型、辅助组件(Label/Annotatio)使用情况,对这些自变量进行测量和验证。
数据量对初始渲染时间的影响
实验中数据量范围: [100, 20000],使用的 geometry 类型为 line,实验中的因变量为渲染时间即图表的初始渲染时间 chart.render()
实验数据如下
结论:数据量大小和渲染时间强正相关。
Geometry 类型对初始渲染时间的影响
这次实验针对大数据量情况下常用的 line/area/point 三种图形来进行测量
Geometry 类型说明:point、line、area、line+point、area+point
结论:Geometry 类型对初始渲染时间有影响,Geometry 数量对初始渲染时间强相关,在数据量大后渲染时间甚至和Geometry数量成线型关系了;同时 Geometry 数量对初始性能渲染时间影响大于 Geometry 类型对初始渲染时间影响。
Label 数据标签使用对初始渲染时间影响
这次实验以折线图、折线图开启数据标签label、折线图开启数据标签并配置 label layout 三组来进行对比
结论:label 数据标签的开启和 label layout 使用会明显的影响图形的初始渲染性能。
性能分析
在上面对几个自变量对性能影响的实验测量后,这里来针对一个具体的数据量来进行性能分析,定位造成性问题的瓶颈点。我们用 Chrome 的 performance 工具来对 10000 数据量的图形渲染进行 profile 分析:
首先我们对初始的渲染分成四个阶段,分别对应 G2 内部渲染的四个阶段,我们先看看这个阶段的分布:
比较容易注意到的点包括:
- Axis 轴渲染两次,init/update,分别耗时 607ms 和 472ms。init 过程中 scale.getTicks 耗时 287ms,processOverlap 也分别耗时 168ms 和 345ms
- PaintRecursive 中,Geometry.sort 耗时 510ms,这里的 sort 是 G2 为折线图/面积图加在 X 轴数据自动排序
预期优化
分析到这里,其实就可以有个优化的预期了,按照执行难易程度将可以优化的地方分成三个阶段:
这里优化的时间是针对10000数据量下的一次渲染时间为例来说明
第一阶段∫ _EASY_ 2.05s -> 1.15s 节省 898ms
- 去掉 geometry 的默认 sort,对业务来说会有自己的数据排序,sort 默认设置为 false,可以优化 510ms
axis render 过程的 processOvelap,其他的大头 getMaxLabelWidth,可以优化掉 128 + 140 = 268ms
- getMaxLabel 优化方法 目前是每个 label 都去拿 Bbox,可以先直接比较 label text 的中英文字符长度
第二阶段∫ _MIDDLE_ 1.15s -> 0.95s
axis processOverlap 另外两个 each,其中的大头 Element.remove 可以优化 67 + 53 = 120ms
- Element.remove 涉及了在 parent 中查找和array 删除元素的过程,可以 1)优化 g remove 的实现,,2)替换为 element.set('visible', false)
寻找整个阶段中的重复执行代码段:
- Category.translate: translate 涉及到数组 indexOf 查找,在数量较大的时候比较耗时,这个片段在整体初始 render 过程中重复了3+边,加上缓存避免重复的 indexOf,预计可以优化约 200ms
第三阶段∫ _HARD_ 0.95s -> 0.7s?
- axis overlap 在时机 overlap 处理之前可以进行 sample 抽样,降低实际处理的数据量,比如降低到把这里的10000数据量降低到500级别,预计节省 250ms
- 上面提到的 axis render 两遍问题是在 auto padding 中需求先 render 一遍参与 padding 的组件,确定他们的大小,有没有办法去掉这个,只 render 一遍?
执行验证
有了上面的实验和分析后,后续的事情只是执行和验证了。
1. 去掉 Geometry 的默认 sort ✅
设置 line/area geometry 的默认 sort 为 false,再次以相同的方法验证在 [100,20000] 数据量下的初始渲染时间,并和优化前的进行对比,优化效果符合预期,数据量越大效果越明显。
2. 优化 getMaxLabelWidth ✅
在 component 中 getMaxLabelWidth 中,使用中英文字符串长度比较,先找到最长的 label,再测量一起宽度,再次验证
3. 优化 processOverlap 中的 element.remove ❌
先直接去掉 axis label 在 processOverlap 中的 remove 操作,结果显示性能不升反降。。 这里的问题是如果不 remove 掉直接增加了后续遍历的成本。那么只能去优化 g-base remove 的实现方式。
方案:先考虑如何在 g-base 中 elemen.remove 的时候去掉 array的 indexOf 过程,可以在 container 的实现过程中,维护每个 child 的 index 信息,这样在 remove 的时候,可以避免 indexOf,直接从 array 中移除掉元素。更好的方式可能是改造 children 的储存数据结构,不是以 array 来作为 children 的数据结构。
验证下可以看到有效果,但影响非常小,这样标记 index 的问题在于 remove 之后还得去更新每个 child 的 index。
由于优化 3 在目前的方案下效果不明显,还需要在 G 层认真考虑下如何优化大数据量下的 container 和 element 的组织和数据结构,这里先去掉,后续在 G 层优化
4. 优化重复的 category.translate ✅
方案:在 category scale 的实现在,添加 tranalste 函数的 cache,在 cache 有效期间对同一 domain 多次 translate 的时候只会做一次计算
验证效果,可以看到直接添加一次 cache 就有明显的效果(黄色曲线)
5. 优化 axis processOverlap 添加数据抽样 ✅
方案:在 axis 实现中,新增大数据量的优化配置,对 ticks 数量超过配置的数据量时,在 render 前对 ticks 进行抽样。
验证效果:可以看到效果非常明显,随着数据量的增大,初始渲染时间增加明显比优先前降低很多。
总结
我们分别按步骤完成上述的各个阶段的优化,每次优化完成后重复验证性能数据,这里直接看下我们优化后的整体效果:
可以看到我们预期中每一步优化都有明显的效果,看下最好在各种数据量的下性能提升比例
可以看到,在测量的各种数据量下平均的性能提升比例达到了 300% 🎉🎉🎉。但是性能优化还需要一直持续下去,上述的只是挑了几个比较占比例的,在我们日常的开发迭代中,也需要对大数据性能的代码有一定的敬畏之心。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。