ResizeObserver 本身不解决 Canvas 模糊,需配合 devicePixelRatio 适配:在回调中用 getBoundingClientRect() 获取容器尺寸,按 dpr 设置 canvas.width/height 和 ctx.scale(),并监听 dpr 动态变化。

ResizeObserver 本身不解决 Canvas 模糊,但它能触发正确的重适配逻辑
Canvas 模糊不是 ResizeObserver 的责任,而是你没在尺寸变化后重新做 devicePixelRatio 适配。ResizeObserver 只是帮你捕获「容器宽高变了」这个信号,后续必须手动调整 canvas.width/canvas.height 和 ctx.scale(),否则拉伸模糊照旧。
常见错误现象:用 resize 事件监听窗口,但容器是 flex/grid 内部自适应缩放时根本收不到;或用了 ResizeObserver 却只改了 style.width,没碰 canvas.width —— 这等于把高清图硬塞进小框里再拉伸,必然糊。
- ResizeObserver 回调中必须重新获取容器
getBoundingClientRect(),不能缓存旧尺寸 - 每次回调都要重设
canvas.width和canvas.height(物理像素),再调ctx.scale(dpr, dpr) - 如果 canvas 是响应式布局(如 width: 100%),需确保父容器有明确宽高,否则
getBoundingClientRect()返回 0
为什么不能只靠 CSS 宽高 + transform 缩放
CSS transform: scale() 或 width/height 样式只是视觉拉伸,不增加物理像素数。浏览器仍按原始 canvas.width 像素渲染,再用 GPU 插值放大 —— 这就是模糊根源。
使用场景:适合临时预览、动画过渡,但绝不能用于需要清晰线条/文字的正式渲染(比如图表坐标轴、UI 图标)。
-
transform: scale(2)在 dpr=2 设备上,实际是 4 倍插值,边缘发虚更明显 - 某些安卓 WebView 对
transform缩放支持差,可能直接丢帧或错位 - 若 canvas 同时启用了
imageSmoothingEnabled = false,CSS 缩放会强制启用平滑,导致设置失效
完整可复用的 ResizeObserver 适配函数
以下代码处理了 resize 频繁触发、dpr 变化(如设备旋转)、以及避免重复缩放 ctx 的坑:
function setupResponsiveCanvas(canvasId) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
let dpr = window.devicePixelRatio || 1;
<p>const updateCanvas = () => {
const rect = canvas.getBoundingClientRect();
const newDpr = window.devicePixelRatio || 1;</p><pre class="brush:php;toolbar:false;">// dpr 变了才重设,避免无谓重绘
if (newDpr !== dpr) {
dpr = newDpr;
ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换矩阵
}
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.style.width = `${rect.width}px`;
canvas.style.height = `${rect.height}px`;
ctx.scale(dpr, dpr);};
const ro = new ResizeObserver(updateCanvas); ro.observe(canvas.parentElement || canvas);
// 初始执行一次 updateCanvas();
return { canvas, ctx, destroy: () => ro.disconnect() }; }
// 使用 const { canvas, ctx } = setupResponsiveCanvas('myCanvas'); ctx.fillStyle = '#000'; ctx.fillRect(10, 10, 20, 20); // 坐标已按缩放后坐标系,写整数即可
容易被忽略的三个细节
很多方案跑起来“好像没问题”,但上线后在 iPad 或折叠屏上突然糊——往往栽在这三点:
- 没监听
window.devicePixelRatio动态变化:横竖屏切换时 dpr 可能从 2 变成 3(部分安卓机),仅靠 ResizeObserver 捕获不到 - 父容器用了
transform(比如scale(0.8)):此时getBoundingClientRect()返回的是变换后尺寸,但 canvas 物理像素仍要按真实视口比例算,需额外校正 - 离屏 canvas(
OffscreenCanvas)未同步 dpr:若在 Worker 中预渲染,window.devicePixelRatio不可用,得由主线程传入 dpr 值
真正稳定的方案,永远是「容器尺寸变化」+「dpr 变化」双信号驱动重适配,缺一不可。











