AntV G6 是一款图可视化与剖析开源引擎。《AntV G6 的坑之——XXX》系列文章继续更新中,总结常见问题及神坑解决方案。任何问题可在 GitHub Issue 中发问,求 GitHub Star ⭐️https://github.com/antvis/g6
原文链接:http://g6.antv.antgroup.com/m...
简介
在面对简单数据的图可视剖析,你的 G6 利用是否呈现了卡顿、掉帧、不晦涩景象?跟着本文的 tips 排查和优化,晋升你的图可视化利用的性能。G6 的性能瓶颈次要在两个方面:渲染、计算。本大节介绍性能瓶颈的一些原理,对实践不感兴趣只想间接优化代码的小伙伴能够间接跳到解决方案章节
性能瓶颈 — 渲染
在渲染方面,性能次要取决于以后画布上形态元素的个数,e.g. 一个节点上有矩形、文本、图片三个图形,一条边上有门路、文本两个图形,那么一份 100 个节点、50 条边的图数据,将渲染出 100 * 3 + 50 * 2 = 400 个图形。然而,开发者经常自定义非常复杂的节点,一个节点上可能有 10~20 个图形,那么画布上的图形数量将陡增。因而,尽可能地缩小不必要的图形绘制,是晋升渲染性能的次要伎俩。
性能瓶颈 — 计算
计算方面,次要包含节点布局计算、折线主动寻径算法等。
解决方案
G6 外部代码,咱们在继续迭代其性能。而基于 G6 的图利用,则须要利用的开发者关注实现形式,不合理的实现形式很可能导致性能的额定开销。
定义正当的画布大小
一般来说,咱们该当依据浏览器中容器的大小设置图画布的大小,即在图实例上配置的 width
与 height
。目前支流显示器的分辨率来看,浏览器中搁置图的容器长个别都不会超过 2500,高个别不会超过 2000。之前已经遇到过开发者将图的 width
和 height
设置到几万,这造成了 <canvas />
标签的宽高十分大。这齐全没有必要,因为大部分超出了浏览器视口。实际上,咱们绘制的节点即便坐标达到了上万,咱们依然能够通过 drag-canvas
、zoom-canvas
等交互滚动查看,没有必要设置微小的图宽高。
尽可能抉择 Canvas 渲染
相比于 Canvas,可能局部开发者更相熟 DOM/SVG 的定义,毕竟 SVG 渲染进去之后能够审查元素,更合乎咱们的日常调试习惯。比方当你在自定义节点中应用 group.addShape('dom', {...})
这种 'dom' 图形时,就必须要应用 SVG 渲染,即在图实例上配置 renderer: 'svg'
。但 SVG 的性能比 Canvas 差得多。 在数据较大、节点比较复杂的状况下,咱们强烈推荐你应用 Canvas 进行渲染。Canvas 定义图形的形式也非常灵活,齐全能够笼罩 SVG 的能力,或任何看起来像 DOM 定义的卡片款式的节点。比方上面这两个例子,都是应用 Canvas 渲染和定义。
<img src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*3cRGRb5nB_UAAAAAAAAAAABkARQnAQ" width=300 style="display: inline-flex" alt='' /><img src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*b-g0RoOpI3sAAAAAAAAAAABkARQnAQ" width=300 style="display: inline-flex" alt='' />
- http://g6.antv.antgroup.com/e...
- http://g6.antv.antgroup.com/e...
回到 SVG 容易审查这个劣势,尽管 Canvas 上没有方法审查每一个图形,但咱们能够通过上面形式打印图形的属性,进行调试:
// 整个图const graphGroup = graph.getGroup(); // 整个图的根图形分组const graphGroups = graphGroup.get('children'); // 个别会有 -node -edge -delegate 几个分组// 单个节点(单个边/ combo 也相似)const node = graph.findById('node1'); // 找到某个节点对象const nodeShapeGroup = node.getContainer(); // 获取该节点的图形分组const nodeShapes = nodeShapeGroup.get('children'); // 获取改节点中的所有图形const keyShape = node.getKeyShape(); // 获取该节点的要害图形,keyShape 在 nodeShapes 中const labelShape = nodeShapeGroup.find(ele => ele.get('name') === 'label-shape'); // 获取 name 为 'label-shape' 的图形(name 在 addShape 时指定)。labelShape 在 nodeShapes 中console.log(nodeShapes[0].attr(), keyShape.attr(), labelShape.attr()); // 获取并打印图形的属性
除了应用 Canvas 渲染,在定义如此简单的节点时,同时倡议尽可能管制图形的数量,见下文 缩小自定义元素的图形数量 一节。
缩小自定义元素的图形数量
图的渲染性能很大水平取决于画布上图形的数量。有时尽管数据层面只有 100 个节点,但因为自定义节点非常复杂,每个节点达到数十个图形,再加上简单的自定义边,可能图上图形也可能达到上万。比方上面这个节点上有二十七个图形(因为节点带滚动,局部文字、锚点被暗藏):
<img src="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*WUI9Sr9E5a0AAAAAAAAAAAAADmJ7AQ/original" width=300 alt='' />
- 缩小不必要的图形。例如,给矩形减少边框,不须要新增图形,只须要给矩形设置描边粗细
lineWidth
和描边色stroke
即可。 默认看不见的图形,设置
visible: false
(而不是opacity: 0
)进行暗藏。在自定义节点的update
办法或draw
办法中,依据状况再通过shape.show()
将其显示进去或shape.hide()
再次暗藏,e.g.const circleShape = group.addShape('circle', {attrs: {}, // 在 attrs 中设置 opacity: 0 也能达到看不见的目标,但实际上还是渲染了,更举荐应用 visible 管制name: 'custom-circle', // 在 G6 3.3 及之后的版本中,必须指定 name,能够是任意字符串,但须要在同一个自定义元素类型中放弃唯一性visible: false, // 默认暗藏。留神 visible 字段的地位。visible 为 false 时,图形不会被渲染});circleShape.show(); // 显示circleShape.hide(); // 暗藏
- 依据缩放等级,调整显示的图形。在小规模的图上,每个节点都有详细信息性能问题不大,且用户兴许须要在每个节点上看到如此具体的信息。但在较大规模的图上,概览时用户更关怀的是图的关系构造,此时咱们该当思考,依据状况调整自定义节点上图形的数量,暗藏不必要的信息。这样做一方面能够减小渲染的压力,另一方面能够让用户更高效地取得更清晰的信息。在官网案例决策树中,进行画布的缩放,能够看到详情(左)和缩略节点(右)的优雅切换。每个节点上图形显示的图形数量从 9 个(具体)升高到 2 个(缩略)。
<img src="https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*HS5gQ6yCiL4AAAAAAAAAAAAAARQnAQ" width=500 alt='' />
<img src="https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*b03ARph0fyUAAAAAAAAAAAAADmJ7AQ/original" width=500 alt='' />
为自定义元素实现 update 办法
初学者为了不便自定义节点/边/ combo,往往只定义 draw
或 drawShape
办法,咱们也激励在小规模图上这样做,能够缩小一些开发成本和学习老本。但这将带来额定的性能开销。以自定义节点为例,可能有以下几种状况:
G6.registerNode
第三个参数没有传入被继承的节点类型名,且没有定义update
办法(或update: undefined
),如下:
G6.registerNode('custom-node', { draw: (cfg, group) => {}, update: undefined, // 或没有实现 update}); // 没有第三个参数
那么,这个自定义节点将不继承任何内置节点,也没有本人的 update
办法。包含首次绘制,所有的更新,例如通过内部或内部调用的 graph.updateItem
、node.refresh
等办法,都将擦除该节点上的所有图形,并从新走一遍 draw
办法。这也意味着这个节点上的所有图形将被销毁和从新实例化。这就带来了大量耗费。
G6.registerNode
第三个参数指定了被继承的节点类型名,没有复写update
,如下:
G6.registerNode('custom-node', { draw: (cfg, group) => {},}, 'circle'); // 继承内置 circle 类型节点
此时,custom-node
将继承内置 circle
类型节点的 update
、setState
等办法。有时,可能发现节点更新时,仿佛有不合乎预期的图形呈现,这是因为 custom-node
的 draw
办法和 circle
类型节点的 draw
办法差别太大,以至于 circle
类型节点依照本人在 draw
办法中定义的图形进行更新,与 custom-node
逻辑不匹配。解决这一问题最简略的办法就是将 update
复写为 undefined
。此时,就带来了和第一种状况相似的、不实现 update
办法的性能开销。
G6.registerNode
第三个参数指定了被继承的节点类型名,复写update: undefined
,如下:
G6.registerNode('custom-node', { draw: (cfg, group) => {}, update: undefined, // 被复写}, 'circle'); // 继承内置 circle 类型节点
下面第二种状况所述的,更新时呈现不合乎预期的图形或款式问题在复写 update: undefined
后该当不复存在。但带来了不实现 update
办法的性能开销。即所有的更新,例如通过内部或内部调用的 graph.updateItem
、node.refresh
等办法,都将擦除该节点上的所有图形,并从新走一遍 draw
办法。这也意味着这个节点上的所有图形将被销毁和从新实例化。
因而,为了更好的渲染性能,最正当的实现是充分利用节点的生命周期,在不同生命周期给出不同的增量逻辑。如下:
G6.registerNode('custom-node', { draw: (cfg, group) => { group.addShape('circle', { attrs: {...}, // styles, name: 'xxx' // 在 G6 3.3 及之后的版本中,必须指定 name,能够是任意字符串,但须要在同一个自定义元素类型中放弃唯一性 }) // ... }, update: (cfg, group, item) => { // 依据 cfg,产生增量的响应 const someShape = group.find(ele => ele.get('name') === 'xxx'); // 拿到须要更新的图形 someShape.attr({ lineWidth: 2 }); // 批改图形款式 someShape.show(); // 管制显示和暗藏 },}, 'circle'); // 继承内置 circle 类型节点
当然,这要求开发者可能对节点上图形的更新有足够清晰治理逻辑。就像 React 的 componentDidMount
、componentDidUpdate
等生命周期函数一样,不同的 props 变更做出不同的响应。置信只有了解了这一原理,你也能轻松做到。
正当应用折线边 polyline
与其余类型的边不同,折线类型边(polyline)在未指定 controlPoints
(拐折点)时,将应用 A 主动寻径算法,依据终点和起点的地位,主动计算折线弯折的地位。这一计算的复杂度较高,特地是在拖拽节点的过程中,相干的边须要实时依据最新的端点地位,频繁地计算 A 算法。因而,当图上的 polyline 边比拟多时,可能呈现卡顿景象。依据你的理论状况,能够抉择如下办法进行防止:
参考官网案例自定义折线。大部分状况下,折线两个弯折地位别离在两端点(上面例子的
startPoint
、endPoint
)连线的 1/3 和 2/3 处,其实咱们能够轻易计算出简略的折线门路。[['M', startPoint.x, startPoint.y],['L', endPoint.x / 3 + (2 / 3) * startPoint.x, startPoint.y],['L', endPoint.x / 3 + (2 / 3) * startPoint.x, endPoint.y],['L', endPoint.x, endPoint.y],]
若你应用的布局算法是
dagre
,那么能够关上它的controlPoints
配置。dagre
算法将为 polyline 边计算控制点,有了控制点,polyline 将不再应用 A* 主动寻径算法,配置办法如下:const graph = new Graph({// ... 其余配置layout: { type: 'dagre', controlPoints: true, // ... 其余配置}})
关上交互的优化配置项
设置节点/边的状态款式、拖拽节点等,根本都是部分更新,即渲染器只会擦除更新前的“脏矩形”,绘制上更新后的图形。但平移画布、缩放画布,在 Canvas 层面上,实际上是整个画布的擦除和重绘,并且在平移/缩放的过程中,这一重绘是极其频繁地被执行的。因而在较大规模的图上,用户可能会显著感觉到平移、缩放画布时十分卡顿。内置的缩放画布 zoom-canvas
和拖拽平移画布 drag-canvas
交互反对配置项 enableOptimize
,设置为 true
时,在拖拽/缩放过程中,非关键图形(即 G6.registerNode
、G6.registerEdge
、G6.registerCombo
的 draw
返回的图形)将会被暗藏。拖拽/缩放完结后,哪些长期被暗藏的图形将复原显示。这样可能大大晋升拖拽/缩放过程中的帧率。
默认状况下 enableOptimize
是 false
,能够通过上面形式配置:
const graoh = new Graph({ // ...其余配置项 modes: { default: [{ type: 'drag-canvas', enableOptimize: true, // ... 其余配置 }, { type: 'zoom-canvas', enableOptimize: true, // ... 其余配置 }] }})
抉择适合的布局算法
G6 提供了多种布局算法,其中力导向布局还是受到大多数开发者的青眼。G6 的以下几种布局均是力导家族成员,但性能却有差别,咱们更举荐应用近期新增的 force2
算法:
- force2:新增的力导算法,性能优良,在经典力导向模型根底上,减少了更多对于离心力、聚类力的配置,可配置带动画或不带动画的布局 (
animate
); - force:援用自 d3 的力导向算法,暂不反对不带动画的布局;
- forceAtlas2:区别于经典的力导向模型,成果更紧凑,性能个别,实现自论文 ForceAtlas2, a Continuous Graph Layout Algorithm forHandy Network Visualization Designed for the GephiSoftware;
- fruchterman:另一种力导向模型,偏向于六边形的散布,性能较差,实现自论文 Fruchterman–Reingold Hexagon Empowered NodeDeployment in Wireless Sensor Network Application。
只无力导向家族的布局能够通过布局的 animate
配置管制是否在计算过程中一直渲染,从而展现出相似“粒子碰撞”、“力相互作用”的动画成果。其余布局只能在齐全计算实现之后进行绘制,在图实例上配置 animate: true
能够为这一类动态的布局,或力导向家族配置 animate: false
的状况下,布局实现之后进行节点地位的插值动画,挪动到对应的地位上。
在数据量较小时,力导向家族无论是否开启布局的 animate
,成果均不错。在较大数据集上,若敞开 animate
,则可能须要较长时间期待布局实现后,画布才会更新。若关上 animate
,在动画中期待布局的实现,一般来说更容易为终端用户所承受。当然,也有可能呈现布局的序幕,节点有“震荡”状况。倡议在监听节点或画布的点击事件,在用户点击时,进行布局。
数据增量 V.S. changeData
- 若干个元素的更新,更举荐应用
graph.updateItem
别离更新; - 新增若干个元素,更举荐应用
graph.addItem
。v4.6.6 起反对了graph.addItems
批量新增元素; - 移除若干个元素,更举荐应用
graph.removeItem
; - 大部分的数据变更,应用
graph.changeData
。该办法将做以后图数据和 changeData 传入的新数据的 diff,若发现 id 雷同的元素,将进行新旧数据的交融。
优化 Minimap 配置
Minimap 是 G6 的小地图插件,用作图的导览。它有三种类型:'default'
、'keyShape'
、'delegate'
。'default'
模式下,主图上的所有内容将被齐全拷贝一份到 Minimap 的画布上,在主图元素产生更新的时候,Minimap 的响应内容也须要从新拷贝,相当于两份图的开销,因而这种类型的 Minimap 性能最差。'keyShape'
模式下,Minimap 仅显示节点和边的次要图形、'delegate'
模式下,Minimap 仅显示代理图形(可通过 delegateStyle
配置),这两种模式的 Minimap 画布内容较为简化,因而有更好的性能。在较大规模的图上,咱们更举荐前面两种模式的 Minimap。
此外,思考到 Minimap 个别比拟小,元素比拟多时,边比拟细,在 Minimap 上也看不清。 v4.7.16 起,Minimap 反对了 hideEdge
配置(默认为 false
),可设置为 true
以暗藏 Minimap 上的边,从而更大程度地晋升 Minimap 的性能。
正当地应用动画
动画的性能开销个别比拟大,更倡议动画应用在部分的状态响应时。例如 hover 节点时的呼吸成果、相干上下游的边流动成果等。并及时地进行动画。