如何正确结合ResizeObserver与requestAnimationFrame解决画布闪烁问题
解决ResizeObserver结合requestAnimationFrame时画布闪烁问题
先看正常运行的ResizeObserver用法,此时画布尺寸变化时能正常重绘:
<canvas></canvas>
html, body { height: 100%; margin: 0; } canvas { width: 100%; height: 100%; display: block; }
const canvas = document.querySelector('canvas'); const ctx = canvas.getContext('2d'); function draw() { const { width, height } = canvas; ctx.clearRect(0, 0, width, height); ctx.save(); ctx.translate(width / 2, height / 2); const size = Math.min(width, height); ctx.beginPath(); ctx.arc(0, 0, size / 3, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } const observer = new ResizeObserver(entries => { for (const entry of entries) { const canvas = entry.target; canvas.width = entry.contentBoxSize[0].inlineSize; canvas.height = entry.contentBoxSize[0].blockSize; draw(); } }) observer.observe(canvas);
但当把绘制逻辑移到requestAnimationFrame循环并添加动效后,调整窗口大小时画布会出现闪烁:
const canvas = document.querySelector('canvas'); const ctx = canvas.getContext('2d'); function draw() { const { width, height } = canvas; ctx.clearRect(0, 0, width, height); ctx.save(); ctx.translate(width / 2, height / 2); const size = Math.min(width, height); ctx.beginPath(); ctx.arc(0, 0, size / 3, 0, Math.PI * 2); ctx.fill(); ctx.rotate(performance.now() * 0.001); ctx.strokeStyle = 'red'; ctx.strokeRect(-5, -5, size / 4, 10); ctx.restore(); } function rAFLoop() { draw(); requestAnimationFrame(rAFLoop); } requestAnimationFrame(rAFLoop); const observer = new ResizeObserver(entries => { for (const entry of entries) { const canvas = entry.target; canvas.width = entry.contentBoxSize[0].inlineSize; canvas.height = entry.contentBoxSize[0].blockSize; } }) observer.observe(canvas);
问题原因
闪烁的核心是ResizeObserver回调的执行时机和requestAnimationFrame的帧循环不同步:
- 修改
canvas.width/height会直接清空画布内容 - 如果ResizeObserver回调在当前帧的draw执行之后触发,修改尺寸后要等到下一帧才会重新绘制,中间会出现空白帧,导致视觉闪烁
解决方法
把尺寸更新逻辑移到requestAnimationFrame的绘制循环里,确保尺寸修改和绘制在同一帧完成,避免空白帧出现。具体做法:
- 在ResizeObserver回调中仅记录最新的画布尺寸,不直接修改canvas属性
- 在draw函数中检查尺寸是否变化,若变化则更新canvas的宽高,再执行绘制
修改后的代码如下:
const canvas = document.querySelector('canvas'); const ctx = canvas.getContext('2d'); // 存储最新的目标尺寸 let targetWidth = canvas.offsetWidth; let targetHeight = canvas.offsetHeight; // 初始化画布尺寸 canvas.width = targetWidth; canvas.height = targetHeight; function draw() { // 检查尺寸是否变化,若变化则更新画布 if (canvas.width !== targetWidth || canvas.height !== targetHeight) { canvas.width = targetWidth; canvas.height = targetHeight; } const { width, height } = canvas; ctx.clearRect(0, 0, width, height); ctx.save(); ctx.translate(width / 2, height / 2); const size = Math.min(width, height); ctx.beginPath(); ctx.arc(0, 0, size / 3, 0, Math.PI * 2); ctx.fill(); ctx.rotate(performance.now() * 0.001); ctx.strokeStyle = 'red'; ctx.strokeRect(-5, -5, size / 4, 10); ctx.restore(); } function rAFLoop() { draw(); requestAnimationFrame(rAFLoop); } requestAnimationFrame(rAFLoop); const observer = new ResizeObserver(entries => { for (const entry of entries) { // 仅更新目标尺寸,不修改画布 targetWidth = entry.contentBoxSize[0].inlineSize; targetHeight = entry.contentBoxSize[0].blockSize; } }) observer.observe(canvas);
额外优化
还可以添加防抖逻辑,避免窗口调整过程中频繁触发尺寸判断,但上面的方法已经能解决核心的闪烁问题。
内容的提问来源于stack exchange,提问作者user128511
相关产品推荐
相关产品推荐

