性能优化|冷门知识点 | 页面一有高频 Canvas 动画(粒子特效、图表实时刷新、视频处理),主线程就沦陷------点击没反应、滚动掉帧、输入卡顿。你以为加个
requestAnimationFrame就万事大吉?其实绘制本身还在主线程上抢时间。OffscreenCanvas能把整块绘制逻辑扔进 Worker,主线程彻底解放。
问题场景
一个实时折线图 + 粒子背景的监控大屏,数据每秒刷新 30 次:
js
// 主线程直接绘制------看似没问题?
function drawChart(ctx, data) {
ctx.clearRect(0, 0, width, height);
data.forEach((point, i) => {
// 几十个点的路径计算 + 绘制
ctx.lineTo(point.x, point.y);
});
ctx.stroke();
// 后面还有粒子系统、坐标轴、文字标注......
}
页面跑起来后:图表本身不卡,但页面上的按钮点击延迟 200ms+,滚动像幻灯片 。用 Performance 面板一看,主线程长期被 CanvasRenderingContext2D 的绘制调用占满,长任务(Long Task)一个接一个。
原因分析
很多人有个误解:「动画不卡 = 性能好」。实际上:
- Canvas 绘制是同步 CPU 操作 。
stroke()、fill()、drawImage()的路径光栅化、纹理上传都在调用线程上执行,高频调用会持续占用主线程。 - 主线程被占 = 一切交互都排队。事件处理、布局、样式计算、JS 任务共用一个线程,绘制吃满时间片,用户交互自然卡顿。
requestAnimationFrame只是调度器 。它保证绘制节奏跟刷新率对齐,但并不能把绘制本身挪走------每帧该干的活一点没少。
核心矛盾:高频绘制这种「重活」,根本不该和「交互响应」挤在同一条线程上。
解决方案:OffscreenCanvas + Worker
OffscreenCanvas 提供了两种脱离主线程绘制的姿势:
姿势一:transferControlToOffscreen()(推荐,性能最好)
把已有 canvas 的控制权转移给 Worker,主线程只剩一个「展示壳」,所有绘制逻辑在 Worker 里跑:
js
// main.js ------ 主线程
const canvas = document.getElementById('chart');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('chart-worker.js');
// 把离屏画布转交给 Worker(transfer 零拷贝,主线程立即失去控制权)
worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]);
js
// chart-worker.js ------ Worker 里全权绘制
let ctx;
self.onmessage = (e) => {
if (e.data.type === 'init') {
ctx = e.data.canvas.getContext('2d');
// 在这里想怎么画就怎么画,主线程完全不参与
loop();
}
};
function loop() {
// Worker 里同样可以用 rAF(基于独立时钟)
requestAnimationFrame(() => {
draw(ctx); // 粒子、折线、坐标轴全在这
loop();
});
}
⚠️ 控制权转移后,主线程不能再碰该 canvas 的 2d/3d context,否则直接报错。
姿势二:Worker 里自建离屏画布(适合纯后台处理)
不需要展示、只想在后台生成图片的场景(如批量截图、图片压缩预览):
js
// worker.js
const offscreen = new OffscreenCanvas(1920, 1080);
const ctx = offscreen.getContext('2d');
// ... 绘制 ...
const blob = await offscreen.convertToBlob({ type: 'image/png' });
self.postMessage({ blob });
实战注意点
- 尺寸同步 :Worker 不知道设备像素比,初始化时要把
dpr和宽高一起传过去,否则高清屏会糊:
js
// main.js
worker.postMessage({
type: 'init',
canvas: offscreen,
dpr: window.devicePixelRatio,
width: canvas.clientWidth,
height: canvas.clientHeight,
});
-
交互仍需主线程 :点击坐标、hover 高亮这类交互数据,通过
postMessage传给 Worker,Worker 负责重绘------事件监听留在主线程,绘制全在 Worker,各司其职。 -
兼容性兜底 :Safari 15.4+、Chrome 69+ 已支持;老浏览器用
'OffscreenCanvas' in window做特性检测,降级回主线程绘制:
js
const useWorker = 'OffscreenCanvas' in window && 'transferControlToOffscreen' in canvas;
要点总结
- 卡顿根因:Canvas 绘制是同步 CPU 密集操作,高频绘制会占满主线程,拖垮所有交互。
requestAnimationFrame只保证节奏,不转移负载------别把它当性能解药。transferControlToOffscreen()是主推方案:控制权移交 Worker,主线程零绘制开销,且 transfer 零拷贝。- Worker 里也能用 rAF,动画循环、批量渲染、图片处理都能整体搬走。
- 别忘了传 dpr 和尺寸,否则高分屏模糊;用特性检测做优雅降级。
💡 适用场景:实时图表、粒子/动效背景、WebGL 游戏、图像处理、录屏合成。一句话------凡是「高频重绘制」,都值得考虑搬出主线程。