echarts的亿级渲染性能优化

236 阅读6分钟

由于业务每秒极限能生成数十万的数据,echarts的单条曲线就可能就有千万到十亿级的数据量。即使echarts本身具有采样能力,但这个量级的数据js内存本身就承载不了。

为了提升整体性能,我们进行了以下技术改进:

一、流式传输或websocket

全量查询即便后端也会因构建的json过大而出现内存溢出,而且大数据量一次性下载也需要等待很久。因此我们前后端采用了流式传输,基于SSE的协议进行数据解析,边传输边渲染,可以很好地减少页面loading时间。

image.png

不过这里更推荐使用websocket,直接传输二进制数据,避免构建json,也方便前端后续处理。

二、采样

最关键的部分就是采样。比如按最常见的分辨率1920,dpr为2来计算,曲线图即便最大,其像素点数量也远远小于需要渲染的数据,因此在拿到数据后可以进行二次采样。采样算法根据不同场景,可以选择固定步长的系统采样或尽可能保留形状的lttb(Largest-Triangle-Three-Bucket)算法。

结合前面的流式传输,在接收到数据片段时就进行采样并渲染,并在数据全部接收完成后,针对已有全量数据再次采样,并渲染完整图形。

每根曲线采用后的数据量,建议不超过200万——后文会详细解释200万数据的内存大小。

引申:lttb算法

lttb(Largest Triangle Three Buckets)算法是一种高保真的折线数据降采样算法,它通过在数据分桶后选择能最大化三角形面积的点,在大幅减少数据量的同时,最大程度保留原始数据的趋势特征(峰值、谷值、拐点)。

lttb算法大致原理如下图,首先根据采样率分桶,比如10采1则可以每10个点分一个桶,每个桶内取一个点。根据上一个保留点和当前桶内的候选点、下一个桶的中点计算三角形面积,取能形成最大面积的点作为这个桶的保留点。(此处面积计算可以使用向量叉积)

image.png

三、内容裁剪

内容裁剪主要分为两个部分,冗余字段删除和区间查询。

曲线需要的数据只有time和value,因此在后端传输时就将额外字段全部剔除。

同时前端查询时,根据当前曲线图的缩放范围进行查询,比如当前窗口的x轴范围是100 ~ 200,则查询80 ~ 220区间。这里为了避免echarts缩放失效,要额外增加一定的冗余区间。

image.png

四、IDB存储

全量数据的传输和采样成本很高,因此每当曲线全量采样完成后,可以针对采样数据存储到本地IndexedDB中,当二次查询该曲线时可以直接使用本地的。

这里有两个难点:存储时机和清理时机的选择。

IDB的存储和清理时机

存储时机的最大问题在于,如何判断数据已经传输完毕。

一方面流式传输过程中用户可能关闭曲线而取消后续传输,另一方面如果数据还在持续入库就查看曲线,也会导致数据传输不完整。前者比较简单,通过AbortController取消传输时就可以判断;而后者最简单粗暴的方法是,如果最后一条数据的时间小于查询区间,就认为数据尚未入库完成。当然,如果能拿到当前数据消费状态,或者能够根据业务规则判断数据完整性就更好。

清理时机的可以根据业务需求,可以按天清理或按项目维度清理或者按使用量清理(navigator.storage.estimate()可以获取IndexedDB占用存储大小)。

image.png

虽然IndexedDB可以持久化存储,但曲线查看操作往往是临时性的,因此我的决策更激进:每次页面打开(或刷新)就清理。

五、采用TypedArray而非Array

前文有提到1亿条数据建议采样到200万条,那么200万条数据实际占用内存多少呢?

    const array = [];

      const step = 0.01;
      let x = 0;
      while (x < 20000) {
        const y = Math.sin(x);
        array.push([x, y]);
        x = Number((x + step).toFixed(2));
      }

image.png 如果是单一的200万元素二维数组,占用91738kB,也就是100M左右。 然而如果将其传给echarts,数组会被拷贝和包装——下图中三个数组都有200万个元素。

image.png

整个曲线图仅一根曲线就占用内存达到了624M! 但如果改用Float64Array,内存降到了142MB,明显更能接受(js内存限制为4G)。同样200万条数据,Float64Array结构占用内存约为30M,只有普通Array的30%!(如果只看浅层大小,两者差距更明显,引申中有解释浅层大小差距)

image.png

引申1: js内存大小限制

js内存可以通过window.performance.memory查看 image.png

关键指标说明

  • usedJSHeapSize:当前 JS 堆已使用的内存;
  • totalJSHeapSize:浏览器为 JS 堆分配的总内存;
  • jsHeapSizeLimit:浏览器允许 JS 堆使用的最大内存(我的是4G)。

引申2: 堆快照中的名词解释

  • 距离:对象到垃圾回收根(GC Roots)的最短路径长度,数字越大嵌套越深。如果是-,要么已经被gc回收了,要么将在下一次gc会被回收。
  • 浅层大小:对象本身占用的内存
  • 保留的大小:如果删除该对象,能释放的总内存量,包括对象本身以及它唯一引用的其他对象

引申3: 从backing_store到 Native Heap 与 JS Heap

TypedArray的内存机制和普通Array不一样,简单来说TypedArray仅仅是一个视图对象,具体内容存在ArrayBuffer中,而ArrayBuffer的内容存在Native Heap中,并不占用JS Heap。相关具体解释可见juejin.cn/post/759705…

六、增量渲染

echarts的文档也指出在百万级以上数据量时,最好使用增量渲染 image.png

参考

大数据的渲染其实有很成熟的方案了,有需要的可以直接使用perspective: perspective-dev.github.io/examples/