Highcharts 股票图看板实时更新时内存占用大问题

目录

问题背景

原因分析

1. Highcharts的新增点的方法

2. Highcharts的极速渲染模块 Boost

ES模块引入Boost

配置选项 

 配置数据选项

序列提升 VS 图表提升

注意事项

优化技巧

获取时间信息

3. 定时刷新页面

4. Chrome Dev Tools - Performance 性能分析

5. 优化代码

5.1 代码关注点:

5.2 如何处理

5.3 在 vue 组件中处理 addEventListener

5.4 观察者模式引起的内存泄漏

5.5 异步操作未正确清除

6. Highcharts-Vue 选项

Component Properties

7.Vue响应式变量

7.1 ref和reactive两种定义响应式变量的方式有以下不同:

        1. 定义数据角度对比:

        2. 原理角度对比:

        3. 使用角度对比:

7.2 shallowRef和shallowReactive

7.3 减少大型不可变数据的响应性开销

8. 导读

解决方案


问题背景

我的数据可视化项目接入Web Socket,绘制实时股票数据,支持用户订阅特定的股票数据源或股票数据产品,针对每一份订阅Web Socket每10s推送数据时,实时补点。

原因分析

1. Highcharts的新增点的方法

addPoint(value, true, false),方法详细文档:Highcharts Class: Series

第二个布尔值是新增点之后是否重绘图表,第三个布尔值是新增点之后,是否删除之前的点,也就是说,是否要滚动显示一定量的点

https://www.highcharts.com/forum/viewtopic.php?t=41141

https://www.highcharts.com/forum/viewtopic.php?t=41141

Highcharts Demo - JSFiddle - Code Playground

2. Highcharts的极速渲染模块 Boost

boost | Highcharts JS API 文档

Boost module | Highcharts

Boost 模块为某些序列类型提供了一个选项,即使用 WebGL 而不是默认的 SVG 进行渲染。这种技术使得在毫秒内渲染数十万甚至数百万的数据点成为可能。除了 WebGL 渲染外,它还在可能的情况下跳过数据处理和检查,从而进一步节省时间。然而,这种优化也引入了一些限制,即在 Boost 模式下可用的功能会有所不同。具体详情,请查阅官方文档。

除了全局的 Boost 选项外,每个序列还有一个 boostThreshold 设定,这个设定定义了何时应该启用 Boost 功能。通过调整这个阈值,你可以控制何时切换到 WebGL 渲染,以适应不同数量级的数据点和性能需求。

在构建数据可视化项目时,特别是在处理大量数据时,Boost 模块可以成为一个强大的工具,帮助你提高渲染速度,改善用户体验。不过,在使用前请务必了解它的限制和注意事项,以确保你的项目能够充分利用其优势。

ES模块引入Boost
import Highcharts from "highcharts";
import HighchartsBoost from "highcharts/modules/boost";
// Import order is important !
HighchartsBoost(Highcharts);
// Then you can use your Highcharts as usual
配置选项 

在Chart Options添加如下Boost选项

{
    boost: {
        useGPUTranslations: true,
        // Chart-level boost when there are more than 5 series in the chart
        seriesThreshold: 5
    },
    title: {
        text: 'Highcharts Boost'
    },
    series: [{
        boostThreshold: 1,  // Boost when there are more than 1
                            // point in the series.
        data: [ [0, 1], [1, 2], [2, 3] ]
    }]
};
 配置数据选项

在 Boost 模式下,Turbo 模式总是开启的。这意味着所有的数据点都应该被配置为数字数组(例如:[1, 2, 3])或二维数字数组(例如:[[1, 2], [2, 3], [3, 4]])。

请注意,当数据分组(dataGrouping)功能启用时(在 stockChart 中默认为启用),Boost 模式将不会启动。


序列提升 VS 图表提升

有两种不同的方式可以提升图表的性能——基于系列级别的提升(series.boostThreshold)和整个图表级别的提升(boost.seriesThreshold)。

前者在大多数情况下工作得很好,而后者则是为包含大量序列的图表(如监控服务器集群)而设计的。

在图表级别进行提升时,所有可提升的序列都会渲染到同一个画布元素上,而在系列级别进行提升时,每个系列都有自己的最终渲染目标。因此,结合使用可提升和不可提升序列类型的组合图表应该始终在系列级别进行提升,以确保绘制顺序符合预期。

这两种模式各有自己的阈值。对于图表级别的提升,阈值是图表中必须存在的序列数量,以触发提升。对于系列级别的提升,阈值是特定系列中必须存在的点数,以使该系列进入提升模式。

注意事项

Boost 模块包含一个 WebGL 渲染器,它替换了 SVG 渲染器的一些部分。此外,它绕过并简化了一些处理大数据时的资源密集型方面。因此,对于提升后的图表,某些功能是不可用的。这些功能大多与交互性有关,如动画支持。但也有一些与视觉效果相关。

最大的注意事项是,柱状图和条形图的矩形总是被绘制为单条 1 像素宽的线。当放大到每个柱子/条形都可以作为独立实体看到的级别时,这可能不是期望的结果。因此,柱状图和条形图更适合于系列级别的提升。

区域图和区域样条图的区域被绘制为 1px 的列。这与提升模块预期的使用方式(即在数据点数超过提升阈值时触发)配合得很好。但是,如果提升阈值设置得太低,区域图和区域样条图将看起来像柱状图。这是我们正在考虑修复的一个限制。此外,区域图和区域样条图的线条本身不会被渲染。

除了圆形以外的标记形状都不被支持
线条的虚线样式不被支持
堆叠和负颜色不被支持
线宽限制为 1px

使用该模块的预期方式是设置阈值,以便在放大时由 SVG 渲染器“接管”渲染。这种方法在点不太密集时提供预期的交互性,同时在点密度高时提供高性能。

优化技巧

为了让 Highcharts 避免计算极值,可以明确地设置 xAxis 和 yAxis 上的极值(最小值和最大值)。在一个拥有 100 万个数据点的散点图中,这样做可能会将渲染时间减少约 10%。

如果 X 轴和 Y 轴上的值增量都不小,可以考虑将 useGPUTranslations 设置为 true。但是,如果你这样做而增量又很小(例如,具有很小时间增量的日期时间轴),它可能会因为浮点数的舍入误差而导致渲染问题,因此这应该根据具体情况来考虑。

获取时间信息

Boost 模块内置了时间测量功能,用于查看 Boost 渲染器不同方面的性能。它使用 console.time 和 console.endTime 来探测执行时间。结果将输出到开发者控制台。

可以激活以下五个不同的探测点:

WebGL 初始化(timeSetup)
系列处理(timeSeriesProcessing)
K-d 树处理(timeKDTree)
缓冲区复制(timeBufferCopy)
渲染(timeRendering)

以上所有设置都是在 Boost 属性的 debug 对象中设置的布尔值。

请注意,K-d 树是异步构建的,这意味着在构建过程中它不会锁定浏览器中的 UI 线程。它也在图表渲染之后发生。因此,在图表渲染之后和悬停提示激活之前会有一小段延迟。

3. 定时刷新页面

尝试写代码定时刷新浏览器页面,比如定时在每天的某个时间点,避免由于标签页长期不活跃,而持续接受web socket补点数据而造成的内存占用

4. Chrome Dev Tools - Performance 性能分析

使用performance标签分析,究竟是什么占用了这么大的内存

5. 优化代码

代码层面的内存和性能优化,可以从这些出发点思考:是否存在闭包等情况 ,没有及时释放变量占用的内存空间,没有及时销毁的定时器函数,或者没有销毁的事件监听回调函数,var定义的变量引起的变量提升导致的全局变量的污染等问题

5.1 代码关注点:

  1. DOM 中的 addEventLisner 函数及派生的事件监听,比如 Jquery 中的 on 函数,Vue 组件实例的 $on 函数;

  2. 其它 BOM 对象的事件监听, 比如 websocket 实例的 on 函数;

  3. 避免不必要的函数引用;

  4. 如果使用 render 函数,避免在 HTML 标签中绑定 DOM/BOM 事件;

5.2 如何处理

  1. 如果在 mounted/created 钩子中使用 JS 绑定了 DOM/BOM 对象中的事件,需要在 beforeDestroy 中做对应解绑处理;

  2. 如果在 mounted/created 钩子中使用了第三方库初始化,需要在 beforeDestroy 中做对应销毁处理(一般用不到,因为很多时候都是直接全局 Vue.use);

  3. 如果组件中使用了 setInterval,需要在 beforeDestroy 中做对应销毁处理;

5.3 在 vue 组件中处理 addEventListener

调用 addEventListener 添加事件监听后在 beforeDestroy 中调用 removeEventListener 移除对应的事件监听。为了准确移除监听,尽量不要使用匿名函数或者已有的函数的绑定来直接作为事件监听函数。

mounted() {
    const box = document.getElementById('time-line')
    this.width = box.offsetWidth
    this.resizefun = () => {
      this.width = box.offsetWidth
    }
    window.addEventListener('resize', this.resizefun)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resizefun)
    this.resizefun = null
  }

5.4 观察者模式引起的内存泄漏

在 spa 应用中使用观察者模式的时候如果给观察者注册了被观察的方法,而没有在离开组件的时候及时移除,可能造成重复注册而内存泄漏;

举个栗子:进入组件的时候 ob.addListener("enter",_func),如果离开组件 beforeDestroy 的时候没有 ob.removeListener("enter",_func),就会导致内存泄漏。

5.5 异步操作未正确清除

在组件中存在异步操作时(如Ajax请求),在组建销毁前,需要及时取消这些未完成的异步操作,以免造成内存泄漏,以下是解决该问题的步骤:

1. 在组件的`beforeUnmount`生命周期钩子中,取消未完成的异步操作,例如:

beforeUnmount() {
    this.cancelAsyncOperation()
}

6. Highcharts-Vue 选项

在图表库(如 Chart.js、ECharts 等)中,我们经常需要使用 Chart.update() 函数来更新图表的数据或其他属性。然而,在调用这个函数时,我们经常会面临一个问题:是否需要对传递给 Chart.update() 的对象进行深拷贝。

深拷贝意味着创建一个对象的完整副本,包括其所有属性和嵌套的对象。这样做的主要目的是避免在更新图表时意外修改原始数据。特别是当我们传递数组或对象作为图表的数据源时,如果不进行深拷贝,我们可能会不小心修改到传递给图表的原始数据,从而导致不可预见的问题。

为了解决这个问题,并防止传递数组或对象的引用,许多图表库默认将深拷贝选项设置为 true。这意味着当你调用 Chart.update() 时,库会自动为你创建一个传入对象的深拷贝,从而确保原始数据不会被修改。

然而,这种方法有一个明显的缺点:性能。当处理大量数据时,深拷贝操作会变得非常昂贵,因为它需要遍历对象的每个属性,并递归地复制嵌套的对象或数组。这可能导致图表更新变得缓慢,影响用户体验。

因此,如果你确定在更新图表时不会修改原始数据,或者你的应用可以容忍对原始数据的修改,那么将深拷贝选项设置为 false 是一个提高性能的好方法。这样,图表库就不会再为你创建深拷贝,从而加快图表更新的速度。

总之,在决定是否为 Chart.update() 中的对象进行深拷贝时,需要权衡数据完整性和性能之间的关系。在大多数情况下,默认开启深拷贝是一个好的起点,但如果你发现性能成为问题,或者你确定不会修改原始数据,那么关闭深拷贝可能是一个更好的选择。

参考Highcharts-Vue的配置选项

GitHub - highcharts/highcharts-vue

Component Properties

Here is the list of all available props allowed to pass directly to your <highcharts> component instance, which this integration is able to handle.

ParameterTypeRequiredDescription
:optionsObjectyesHighcharts chart configuration object
:constructor-typeStringnoChart constructor type using to init specific chart. Allowed options: 'chart''stockChart''mapChart'. First one is set for default.
:callbackFunctionnoFunction passed as a callback during chart init, and triggered when chart is loaded.
:updateArgsArraynoArray of update()'s function optional arguments. Parameters should be defined in the same order like in native Highcharts function: [redraw, oneToOne, animation]Here is a more specific description of the parameters.
:highchartsObjectnoA specific Highcharts instance. It's useful when required to build charts using different Highcharts versions.
:deepCopyOnUpdateBooleannoWhether to make a deep copy of object passed to Chart.update() function. In order to avoid passing references of arrays, it's set to true by default.

NOTE: That can cause a decrease of performance while processing a big amount of data, because copying source data is much expensive, and then it's recommended to disable that option by setting it to false.

 我们可以在初始化highcharts组件时,将deepCopyOnUpdate设置为false,来避免因深拷贝引起的性能问题

7.Vue响应式变量

7.1 ref和reactive两种定义响应式变量的方式有以下不同:

        1. 定义数据角度对比:

ref 用来定义:基本类型数据
reactive 用来定义:对象、或数组类型的数据
备注:ref也可以用来定义对象或数组类型数据,它内部会自动通过 reactive 转为代理对象

        2. 原理角度对比:

ref 通过 Object.defineProperty() 的 get 与 set 来实现响应式的(数据劫持)
reactive 通过使用 Proxy 来实现响应式(数据劫持),并通过Reflect 操作源对象内部的数据。

        3. 使用角度对比:

ref 定义的数据:操作数据需要 .value,读取数据时模版中直接读取不需要 .value

reactive 定义的数据:操作数据与读取数据,均不需要 .value

7.2 shallowRef和shallowReactive

性能优化 Vue.js

  shallowRef() 

ref() 的浅层作用形式,和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。

shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

  shallowReactive() 

reactive() 的浅层作用形式,和 reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。

7.3 减少大型不可变数据的响应性开销

Vue 的响应性系统默认是深度的。虽然这让状态管理变得更直观,但在数据量巨大时,深度响应性也会导致不小的性能负担,因为每个属性访问都将触发代理的依赖追踪。好在这种性能负担通常只有在处理超大型数组或层级很深的对象时,例如一次渲染需要访问 100,000+ 个属性时,才会变得比较明显。因此,它只会影响少数特定的场景。

Vue 确实也为此提供了一种解决方案,通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理。这使得对深层级属性的访问变得更快,但代价是,我们现在必须将所有深层级对象视为不可变的,并且只能通过替换整个根状态来触发更新:

const shallowArray = shallowRef([
  /* 巨大的列表,里面包含深层的对象 */
])

// 这不会触发更新...
shallowArray.value.push(newObject)
// 这才会触发更新
shallowArray.value = [...shallowArray.value, newObject]

// 这不会触发更新...
shallowArray.value[0].foo = 1
// 这才会触发更新
shallowArray.value = [
  {
    ...shallowArray.value[0],
    foo: 1
  },
  ...shallowArray.value.slice(1)
]

8. 导读

从源码中来,到业务中去,VUE终极性能优化指南

解决方案

采取以上6和7,减少了40%的内存消耗

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值