渲染卡死不再怕,offScreenCanvas来帮你

04/25/2024

背景

在我们的业务系统中,大量使用了 ECharts 来展示各类数据可视化图表。随着业务的发展,有些图表需要展示的数据量越来越大,导致在渲染过程中出现严重的性能问题,甚至造成页面卡死的情况。

为了解决这个问题,我们需要将复杂的计算和渲染过程从主线程中分离出去。Web Worker 是一个很好的选择,它可以在后台线程中进行复杂计算而不影响主线程的运行。而 OffscreenCanvas 则提供了在 Web Worker 中进行canvas渲染的能力,这正是我们需要的解决方案。

OffscreenCanvas 是什么?

OffscreenCanvas 是一个可以在非主线程(如 Web Worker)中使用的 Canvas API。它提供了与普通 Canvas 几乎相同的功能,但最大的区别是它可以在后台进行渲染,不会阻塞主线程的运行。

主要特点:

  • 可以在 Web Worker 中进行渲染操作
  • 支持 2D 和 WebGL 上下文
  • 通过 transferControlToOffscreen() 方法将 canvas 控制权转移到 Worker 线程
  • 渲染结果可以通过 transferToImageBitmap() 方法传回主线程

实现方案

1. 主线程代码

首先,我们需要创建一个 canvas 元素,并将其转换为 OffscreenCanvas:

// main.js
const canvas = document.querySelector('#myChart');
const offscreen = canvas.transferControlToOffscreen();
// 创建 Worker
const worker = new Worker('worker.js');
// 将 offscreenCanvas 传递给 Worker
worker.postMessage({
  type: 'init',
  canvas: offscreen,
  width: canvas.clientWidth,
  height: canvas.clientHeight
}, [offscreen]);
// 发送数据更新
function updateChart(data) {
  worker.postMessage({
    type: 'update',
    data: data
  });
}

2. Worker 线程代码

在 Worker 中,我们需要初始化 ECharts 并处理来自主线程的消息:

// worker.js
importScripts('https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js');
let myChart = null;
self.onmessage = function (evt) {
  const { type, canvas, width, height, data } = evt.data;
  if (type === 'init') {
    // 初始化 ECharts 实例
    myChart = echarts.init(canvas, null, {
      width: width,
      height: height
    });
    // 设置基础配置
    const option = {
      // ... 图表配置
    };
    myChart.setOption(option);
  }
  if (type === 'update') {
    // 更新数据
    myChart.setOption({
      series: [{
        data: data
      }]
    });
  }
};

3. 使用示例

// 在 React 组件中使用
import React, { useEffect, useRef } from 'react';
function EChartsOffscreen() {
  const canvasRef = useRef(null);
  const workerRef = useRef(null);
  useEffect(() => {
    const canvas = canvasRef.current;
    const offscreen = canvas.transferControlToOffscreen();
    workerRef.current = new Worker('/worker.js');
    workerRef.current.postMessage({
      type: 'init',
      canvas: offscreen,
      width: canvas.clientWidth,
      height: canvas.clientHeight
    }, [offscreen]);
    // 清理函数
    return () => {
      workerRef.current?.terminate();
    };
  }, []);
  return <canvas ref={canvasRef} style={{ width: '100%', height: '400px' }} />;
}
export default EChartsOffscreen;

注意事项

  1. 浏览器兼容性

    • OffscreenCanvas 目前主要在现代浏览器中支持
    • 建议添加降级处理方案
  2. 数据传输开销

    • Worker 线程与主线程之间的数据传输存在开销
    • 对于频繁更新的场景,需要考虑数据传输的性能影响
  3. 内存管理

    • 及时清理不再使用的 Worker 和资源
    • 注意避免内存泄漏

性能对比

在我们的实际项目中,使用 OffscreenCanvas 后的性能提升显著:

  • 渲染 10 万数据点的折线图:

    • 传统方案:主线程阻塞 2-3 秒
    • OffscreenCanvas:主线程无阻塞,总耗时减少 40%
  • 多图表同时渲染:

    • 传统方案:明显卡顿,页面响应迟缓
    • OffscreenCanvas:保持流畅,用户体验大幅提升

总结

通过使用 OffscreenCanvas 和 Web Worker,我们成功解决了大数据量图表渲染导致的性能问题。这个方案不仅提升了用户体验,还为后续更多数据可视化场景提供了可靠的技术支持。

虽然实现过程相对复杂一些,但带来的性能提升是值得的。建议在以下场景考虑使用 OffscreenCanvas:

  • 大数据量的图表渲染
  • 需要频繁更新的动态图表
  • 多个复杂图表同时展示的场景