You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Web Worker+Canvas绘制瀑布谱图时的内存泄漏问题排查

Web Worker中fillRect引发内存泄漏的排查与分析

问题背景

为避免瀑布谱图绘制阻塞主线程,将绘制逻辑迁移至Web Worker:通过postMessage传递Canvas和绘制数据,Worker中调用fillRect逐行绘制。但出现严重内存泄漏,10秒后页面卡顿;Chrome内存工具定位泄漏源于Worker的fillRect调用,主线程使用该函数无此问题,手动删除数据仅能缓解,无法根治。

相关代码

React组件(waterfall.tsx)

let Waterfall = (...) => {  // react component - worker and time received as props

  ...

  useEffect(() => {  // time-controlled

    let workerRequest: {
      data: FFTSample[], 
      lineWidth: number, 
      lineHeight: number, 
      dy?: number
    }[] = [];

    // data: array of server messages containing fft information for one point in time
    for (const sample of data) {
      if (sample.time < minTime) {
        // skip frames that run off screen
        continue;
      }

      if ((sample.time - lastTime) > TIME_BETWEEN_LINES) {  // sync with desired number of frames
        let dy = y(sample.time);
        let offset = dy - (lastDy + LINE_HEIGHT);  // compensate for jitters in the time math calculation

        if (dy <= 0 && sample.time != minTime) {
          // if frame moves all the way off screen, record its time
          // this is used to sync samples that are displayed across renders,
          // since not all frames are displayed
          setMinTime(sample.time);
        }

        lastTime = sample.time;
        lastDy = dy;

        // add row, with compensations applied
        workerRequest.push({
          data: sample.payload,
          lineWidth: width,
          lineHeight: (LINE_HEIGHT + 0.5) + offset,
          dy: (dy - LINE_HEIGHT) - offset
        });
      }
    }

    worker.postMessage({ draw: workerRequest });

  }, [time]);

  ...

}

Web Worker(worker.ts)

import { FFTSample } from "../typings";
import * as d3 from "d3";

let paintWaterfallRow = (
  ctx: OffscreenCanvasRenderingContext2D, data: FFTSample[], lineWidth: number, lineHeight: number, 
  z: d3.ScaleSequential<string>, dy?: number
) => {
  let chunkWidth = Math.ceil(lineWidth / data.length) + 1;
  dy = dy || 0;
  for (let i = 0; i < data.length; i++) {
    let point = data[i];
    let color = z(point.magnitude);
    ctx.fillStyle = color;
    ctx.fillRect((i / data.length) * lineWidth, dy, chunkWidth, lineHeight);
  }
}

let canvas: OffscreenCanvas;
let context: OffscreenCanvasRenderingContext2D;
let zScale = d3.scaleSequential(d3.interpolateViridis);

self.onmessage = (msg: MessageEvent) => {
  let data = msg.data;

  // store canvas
  if ("canvas" in data) {
    canvas = data.canvas;
    context = canvas.getContext("2d")!;
    context.imageSmoothingEnabled = false;
    if ("scale" in data) {
      context.scale(data.scale, data.scale);
    }
    console.log("worker: canvas set")
  }
  
  // store domain details for z scale (color information)
  if ("z" in data) {
    let zDomain = data.z;
    zScale.domain([zDomain[0], zDomain[1]]);
    console.log("worker: z-scale set");
  }

  // draw chart
  if ("draw" in data) {
    context.clearRect(0, 0, canvas.width, canvas.height);
    for (const line of data.draw) {
      paintWaterfallRow(
        context,
        line.data,  // FFTSample[]
        line.lineWidth,
        line.lineHeight,
        zScale,
        line.dy
      );
    }
  }
}

深入分析方向

1. OffscreenCanvas上下文的隐式引用

Worker中的OffscreenCanvasRenderingContext2D可能持有未被释放的绘制状态:

  • 每次调用fillStyle设置颜色字符串,高频调用下大量临时字符串对象未被GC回收;
  • 上下文内部的绘制命令队列在Worker中缺乏主线程的优化机制,命令对象堆积无法及时清理。

2. 数据传递的内存开销

  • worker.postMessage默认使用结构化克隆,FFTSample数组每次传递都会复制大量内存,Worker侧未在绘制完成后主动释放克隆后的引用;
  • React组件的useEffect依赖time高频触发,导致workerRequest数组频繁创建,传递给Worker后旧数据未被及时回收。

3. 绘制逻辑的性能缺陷

  • 逐行逐块调用fillRect的方式,在Worker中会触发大量独立的绘制命令,每个命令都可能分配内存,而主线程的Canvas有浏览器优化的内存池,不会出现此类问题;
  • chunkWidth计算中+1导致绘制区域超出画布,可能触发浏览器离屏渲染缓存溢出,未被及时清理。

4. D3 Scale的内存堆积

  • 全局的zScale每次更新domain时,可能保留旧的内部插值缓存,高频调用下堆积内存;
  • z(point.magnitude)每次生成新的颜色字符串,若未被及时回收,会在Worker内存中累积。

排查与验证步骤

  • 使用Chrome DevTools的Memory面板,对Worker单独进行内存快照:对比绘制前后的内存占用,定位未被回收的对象类型(如字符串、Canvas命令对象);
  • 注释fillRect调用,验证内存泄漏是否消失,确认泄漏点是否真的来自该API;
  • 修改绘制逻辑,改用ImageData一次性填充像素,替代逐次fillRect,对比内存变化;
  • 在Worker的draw处理完成后,手动清理data.draw数组(如data.draw = []),或使用Transferable对象传递TypedArray格式的FFT数据;
  • 移除D3 Scale,改用原生颜色计算逻辑,排除D3的内存问题。

内容的提问来源于stack exchange,提问作者moltarze

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 08:06:04