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
相关产品推荐
相关产品推荐

