Sparkline
是一种迷你图表,具有信息体积小、数据密度高的特点,常被用于在表格单元格、仪表板或其他小型区域内直观地显示数据的变化趋势。Sparkline
通常很简洁,没有坐标轴或图例,主要用于展示一段时间内的数据波动,可以应用于销售、财务、价格、进度、预测、指标跟踪等场景中。本文将探讨在网页前端大量生成这种迷你图表的性能问题及解决方案。
传统实现方式及问题
首先,我们看下迷你图表应用的鲜明特点。
- 数量多:常被嵌入到表格单元格中,因此一个页面上往往同时出现大量迷你图表。
- 交互需求低:由于表格单元格的空间限制,嵌入的迷你图表主要用来展示数据趋势,一般来说并不需要复杂的交互功能,甚至连坐标轴、图例都不需要。
- 动画需求低:通常情况下,迷你图表并无动画效果需求。
考虑到这些特点,网页上的迷你图表似乎更适合使用图片而非图表呈现。如果采用图片形式展示,可以在服务端绘制,类似于动态水印图片,前端页面发起图片请求时,携带相关数据,服务端拿到数据后绘制迷你图图像,并将图像的二进制数据放在响应体中返回给前端直接展示。
这种方式也不是没有缺点,除了增加服务器压力、后端研发和前后端联调工作量之外,还可能增加大量的 HTTP 请求。如果你的站点仍在使用 HTTP/2 之前的协议,可能会受到浏览器并发请求数量限制的影响。尽管这些问题并非不能解决,但实践中实施这一方案的主要障碍可能来自于传统的认知 ------ 数据可视化图表被认为是前端的工作范畴。
前端实现迷你图绘制是可行的,多数情况下,前端各种图表库可以轻松绘制出迷你图,避免了造轮子。然而,主要问题在于一次性绘制大量迷你图会导致浏览器性能下降,尤其是在复杂的页面场景下。因为这需要同时实例化大量的图表组件,有时候还需要将 Canvas 元素进一步转换为图片(如在树形图表的节点中插入迷你图,主流图表组件自定义节点通常不支持直接插入 Canvas 元素),这无疑会给页面性能带来挑战。
尽管如此,也不是没有可能找到解决方案。经过这些年的高速发展,现代浏览器的渲染、计算和扩展能力得到了极大提升,以往很多在前端实现比较困难的工作,如今已可以胜任,Web APP
可不是吹出来的。在进行充分的调研和实践之后,我决定在前端采用一套高性能的实现方案。
解题
项目架构
方案的核心思想是充分利用 Web Worker
和 OffscreenCanvas
[^1] 技术,将迷你图的绘制工作放在后台线程中进行,从而避免阻塞主线程,提高页面性能。
OffscreenCanvas
是一个用于离屏渲染的 API,它允许在不直接显示在屏幕上的画布上绘制图形。OffscreenCanvas
的主要优点是可以在Web Worker
线程中使用,从而将图形绘制任务从主线程移到后台线程,避免阻塞主线程并提高页面性能。
以下是该方案的核心步骤:
- 在主线程中,收集当次需要绘制的所有的迷你图数据。
- 在主线程中创建一个
Web Worker
,然后将收集到的迷你图数据通过postMessage
方法统一发送给Web Worker
线程。 - 在
Web Worker
中,使用OffscreenCanvas
技术创建一个离屏画布。然后,使用接收到的数据在离屏画布上逐一绘制迷你图,并将每次绘制结果转换为DataURL
按顺序暂存。 - 当所有迷你图绘制完毕并转换为
DataURL
时,通过postMessage
方法将所有的DataURL
统一发送回主线程。 - 在主线程中,接收到
Web Worker
发送回来的DataURL
,将它们依次设置为相应的 img 标签的 src 属性值,从而在页面上展示出迷你图。 - 终止
Web Worker
,释放相关资源。
整个设计的关键点如下:
- 使用
Web Worker
线程:Web Worker
可以在后台线程中运行 JavaScript 脚本,与主线程并行执行,不会影响主线程的性能。通过将迷你图的绘制工作移到Web Worker
线程,可减少主线程的压力,从而提高页面的响应速度。 - 利用
OffscreenCanvas
进行离屏渲染:OffscreenCanvas
是一个离屏画布,允许在后台线程中绘制图形。使得我们可以在Web Worker
线程中绘制迷你图,绘制不影响主线程,也不受主线程任务影响。 - 集中交换输入输出数据:将当前页面需要绘制的所有迷你图数据汇总到一个数组中发送到
Web Worker
线程,绘制完成的结果也放在一个数组中统一发送给主线程。这样做的主要优势包括: -
- 减少跨线程交换数据的性能损耗:浏览器在跨线程通信前后会自动对要发送的数据进行序列化和反序列化。通过集中交换数据,可以降低跨线程通信次数,从而减少性能开销。
- 有助于在
Web Worker
线程中集中利用一块画布进行绘制,以节约系统资源,提高绘制效率。 - 有序的输入输出可以节约匹配成本,有助于更快地将绘制完成的迷你图与对应的数据进行匹配。
- 避免多次实例化带来的性能开销:只实例化一次,在同一个
OffscreenCanvas
画布上逐一绘制所有迷你图,将绘制结果转成DataURL
按输入顺序存入结果数组,绘制完成后统一发送给主线程。这样可以减少实例化过程中的性能损耗。
浏览器兼容问题
这个架构里的一个薄弱环节是 OffscreenCanvas
的浏览器兼容问题。事实上,这两年主流浏览器都已基本实现了对 OffscreenCanvas
的支持,最新版的 Safari
浏览器也已经提供了支持(虽然看起来还不太完善)。
在很多项目中,对低版本浏览器的兼容仍是必选项。此时,如果不愿在项目核心代码中掺杂大量兼容逻辑,可以考虑把对低版本浏览器的兼容代码设计为更低一层的、可插拔的、相对独立的模块。等未来浏览器兼容情况更好了,甚至可以移除这个模块。
这个降级模块可以通过以下思路实现:在不支持或未启用 OffscreenCanvas
特性的浏览器中使用 Canvas
来绘制,Canvas
和 OffscreenCanvas
的绘图相关 API 基本一致,使用 Canvas
做 OffscreenCanvas
图形绘制方面的降级是可行的。但挑战在于 Canvas
并不能像 OffscreenCanvas
那样工作在 Web Worker
线程,所以这种兼容模式下的绘制只能在主线程进行。同时,为了给上层逻辑提供统一的接口,我们需要模拟一个 Web Worker
,当然并不用新开线程,重点要实现的是 Web Worker
的通信机制。这才是这个降级模块真正有意思的地方,看似在模拟 OffscreenCanvas
,其实主要代码都在模拟 Web Worker
。
在主线程模拟 Web Worker
通信,关键是要实现一个" 基于发布订阅机制的异步消息传递系统 "。早年 Web Worker
特性刚推出时,许多浏览器尚不支持,业界针对 Web Worker
有不少降级方案,多基于 Event Bus
实现。而如今,利用 MessageChannel
[^2] 特性进行模拟就更加简单了。MessageChannel
天生就具备一个消息通道,支持 postMessage
和 onmessage
API,自带两个通信端口,可以把其中一个视为主线程,另一个视为 Web Worker
线程,简直好似 "量身定做" 的,利用这个特性来模拟 Web Worker
通信再合适不过了。
arduino
const { port1: mainSide, port2: workerSide } = new MessageChannel();
图形绘制
如上文所述,OffscreenCanvas
与 Canvas
绘图相关的 API 基本一致。经常与数据可视化打交道的前端同学对使用 Canvas
绘制图表应该不陌生。而迷你图的绘制相对来说难度并不算大,因为通常不需要绘制坐标轴、图例等元素,也没有交互和动画等功能。
以最常见的迷你趋势图为例,我们只需要在笛卡尔坐标系(Cartesian coordinate system)内计算出需要绘制的数据点的位置,然后使用线条将它们连接起来。在这个过程中,可以使用贝塞尔曲线(Bézier curve)来使线条更加平滑。最后,在封闭路径内填充颜色即可完成绘制。具体细节这里不再赘述。
总结
本文介绍了在前端大量生成迷你图表的性能问题,并提出了一种解决方案,可以有效地提高页面性能和响应速度,为用户带来更好的使用体验。
其他
- OffscreenCanvas 提供了一个可以脱离屏幕渲染的 canvas 对象。它在窗口环境和 web worker 环境均有效。developer.mozilla.org/zh-CN/docs/...
- Channel Messaging API 的
MessageChannel
接口允许我们创建一个新的消息通道,并通过它的两个MessagePort
属性发送数据。developer.mozilla.org/zh-CN/docs/...