【数据可视化】使用OffscreenCanvas批量离屏绘制Sparkline迷你图表

Sparkline 是一种迷你图表,具有信息体积小、数据密度高的特点,常被用于在表格单元格、仪表板或其他小型区域内直观地显示数据的变化趋势。Sparkline 通常很简洁,没有坐标轴或图例,主要用于展示一段时间内的数据波动,可以应用于销售、财务、价格、进度、预测、指标跟踪等场景中。本文将探讨在网页前端大量生成这种迷你图表的性能问题及解决方案。

传统实现方式及问题

首先,我们看下迷你图表应用的鲜明特点。

  1. 数量多:常被嵌入到表格单元格中,因此一个页面上往往同时出现大量迷你图表。
  2. 交互需求低:由于表格单元格的空间限制,嵌入的迷你图表主要用来展示数据趋势,一般来说并不需要复杂的交互功能,甚至连坐标轴、图例都不需要。
  3. 动画需求低:通常情况下,迷你图表并无动画效果需求。

考虑到这些特点,网页上的迷你图表似乎更适合使用图片而非图表呈现。如果采用图片形式展示,可以在服务端绘制,类似于动态水印图片,前端页面发起图片请求时,携带相关数据,服务端拿到数据后绘制迷你图图像,并将图像的二进制数据放在响应体中返回给前端直接展示。

这种方式也不是没有缺点,除了增加服务器压力、后端研发和前后端联调工作量之外,还可能增加大量的 HTTP 请求。如果你的站点仍在使用 HTTP/2 之前的协议,可能会受到浏览器并发请求数量限制的影响。尽管这些问题并非不能解决,但实践中实施这一方案的主要障碍可能来自于传统的认知------数据可视化图表被认为是前端的工作范畴。

前端实现迷你图绘制是可行的,多数情况下,前端各种图表库可以轻松绘制出迷你图,避免了造轮子。然而,主要问题在于一次性绘制大量迷你图会导致浏览器性能下降,尤其是在复杂的页面场景下。因为这需要同时实例化大量的图表组件,有时候还需要将 Canvas 元素进一步转换为图片(如在树形图表的节点中插入迷你图,主流图表组件自定义节点通常不支持直接插入 Canvas 元素),这无疑会给页面性能带来挑战。

尽管如此,也不是没有可能找到解决方案。经过这些年的高速发展,现代浏览器的渲染、计算和扩展能力得到了极大提升,以往很多在前端实现比较困难的工作,如今已可以胜任,Web APP 可不是吹出来的。在进行充分的调研和实践之后,我决定在前端采用一套高性能的实现方案。

解题

项目架构

方案的核心思想是充分利用 Web WorkerOffscreenCanvas ^1^技术,将迷你图的绘制工作放在后台线程中进行,从而避免阻塞主线程,提高页面性能。

OffscreenCanvas 是一个用于离屏渲染的 API,它允许在不直接显示在屏幕上的画布上绘制图形。OffscreenCanvas 的主要优点是可以在 Web Worker 线程中使用,从而将图形绘制任务从主线程移到后台线程,避免阻塞主线程并提高页面性能。

以下是该方案的核心步骤:

  1. 在主线程中,收集当次需要绘制的所有的迷你图数据。

  2. 在主线程中创建一个 Web Worker,然后将收集到的迷你图数据通过 postMessage 方法统一发送给 Web Worker 线程。

  3. Web Worker 中,使用 OffscreenCanvas 技术创建一个离屏画布。然后,使用接收到的数据在离屏画布上逐一绘制迷你图,并将每次绘制结果转换为 DataURL 按顺序暂存。

  4. 当所有迷你图绘制完毕并转换为 DataURL 时,通过 postMessage 方法将所有的 DataURL 统一发送回主线程。

  5. 在主线程中,接收到 Web Worker 发送回来的 DataURL,将它们依次设置为相应的 img 标签的 src 属性值,从而在页面上展示出迷你图。

  6. 终止 Web Worker,释放相关资源。

整个设计的关键点如下:

  • 使用 Web Worker 线程Web Worker 可以在后台线程中运行 JavaScript 脚本,与主线程并行执行,不会影响主线程的性能。通过将迷你图的绘制工作移到 Web Worker 线程,可减少主线程的压力,从而提高页面的响应速度。

  • 利用 OffscreenCanvas 进行离屏渲染OffscreenCanvas 是一个离屏画布,允许在后台线程中绘制图形。使得我们可以在 Web Worker 线程中绘制迷你图,绘制不影响主线程,也不受主线程任务影响。

  • 集中交换输入输出数据 :将当前页面需要绘制的所有迷你图数据汇总到一个数组中发送到 Web Worker 线程,绘制完成的结果也放在一个数组中统一发送给主线程。这样做的主要优势包括:

    1. 减少跨线程交换数据的性能损耗:浏览器在跨线程通信前后会自动对要发送的数据进行序列化和反序列化。通过集中交换数据,可以降低跨线程通信次数,从而减少性能开销。

    2. 有助于在 Web Worker 线程中集中利用一块画布进行绘制,以节约系统资源,提高绘制效率。

    3. 有序的输入输出可以节约匹配成本,有助于更快地将绘制完成的迷你图与对应的数据进行匹配。

  • 避免多次实例化带来的性能开销 :只实例化一次,在同一个 OffscreenCanvas 画布上逐一绘制所有迷你图,将绘制结果转成 DataURL 按输入顺序存入结果数组,绘制完成后统一发送给主线程。这样可以减少实例化过程中的性能损耗。

浏览器兼容问题

这个架构里的一个薄弱环节是 OffscreenCanvas 的浏览器兼容问题。事实上,这两年主流浏览器都已基本实现了对 OffscreenCanvas 的支持,最新版的 Safari 浏览器也已经提供了支持(虽然看起来还不太完善)。

在很多项目中,对低版本浏览器的兼容仍是必选项。此时,如果不愿在项目核心代码中掺杂大量兼容逻辑,可以考虑把对低版本浏览器的兼容代码设计为更低一层的、可插拔的、相对独立的模块。等未来浏览器兼容情况更好了,甚至可以移除这个模块。

这个降级模块可以通过以下思路实现:在不支持或未启用 OffscreenCanvas 特性的浏览器中使用 Canvas 来绘制,CanvasOffscreenCanvas 的绘图相关 API 基本一致,使用 CanvasOffscreenCanvas 图形绘制方面的降级是可行的。但挑战在于 Canvas 并不能像 OffscreenCanvas 那样工作在 Web Worker 线程,所以这种兼容模式下的绘制只能在主线程进行。同时,为了给上层逻辑提供统一的接口,我们需要模拟一个 Web Worker,当然并不用新开线程,重点要实现的是 Web Worker 的通信机制。这才是这个降级模块真正有意思的地方,看似在模拟 OffscreenCanvas,其实主要代码都在模拟 Web Worker

在主线程模拟 Web Worker 通信,关键是要实现一个"基于发布订阅机制的异步消息传递系统"。早年 Web Worker 特性刚推出时,许多浏览器尚不支持,业界针对 Web Worker 有不少降级方案,多基于 Event Bus 实现。而如今,利用 MessageChannel ^2^特性进行模拟就更加简单了。MessageChannel 天生就具备一个消息通道,支持 postMessageonmessage API,自带两个通信端口,可以把其中一个视为主线程,另一个视为 Web Worker 线程,简直好似"量身定做"的,利用这个特性来模拟 Web Worker 通信再合适不过了。

js 复制代码
const { port1: mainSide, port2: workerSide } = new MessageChannel();

图形绘制

如上文所述,OffscreenCanvasCanvas 绘图相关的 API 基本一致。经常与数据可视化打交道的前端同学对使用 Canvas 绘制图表应该不陌生。而迷你图的绘制相对来说难度并不算大,因为通常不需要绘制坐标轴、图例等元素,也没有交互和动画等功能。

以最常见的迷你趋势图为例,我们只需要在笛卡尔坐标系(Cartesian coordinate system)内计算出需要绘制的数据点的位置,然后使用线条将它们连接起来。在这个过程中,可以使用贝塞尔曲线(Bézier curve)来使线条更加平滑。最后,在封闭路径内填充颜色即可完成绘制。具体细节这里不再赘述。

总结

本文介绍了在前端大量生成迷你图表的性能问题,并提出了一种解决方案,可以有效地提高页面性能和响应速度,为用户带来更好的使用体验。

Footnotes

  1. OffscreenCanvas 提供了一个可以脱离屏幕渲染的 canvas 对象。它在窗口环境和web worker环境均有效。developer.mozilla.org/zh-CN/docs/...

  2. Channel Messaging API 的 MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据。 developer.mozilla.org/zh-CN/docs/...

相关推荐
余生H14 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍17 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai21 分钟前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默33 分钟前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_8572979143 分钟前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_1 小时前
meta标签作用/SEO优化
前端·javascript·html
Jason不在家1 小时前
Flink 本地 idea 调试开启 WebUI
大数据·flink·intellij-idea
Ink1 小时前
从底层看 path.resolve 实现
前端·node.js
金灰1 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
茶卡盐佑星_1 小时前
说说你对es6中promise的理解?
前端·ecmascript·es6