ResizeObserver 的 contentRect 返回 CSS 像素值,不能直接赋给 canvas.width/height,否则在 Retina 屏下因未乘 devicePixelRatio 导致渲染模糊;必须用 contentRect 尺寸 × dpr 设置 canvas 属性,并调用 ctx.scale(dpr, dpr) 重置坐标系。

ResizeObserver 能精准监听容器尺寸变化,但直接用 contentRect 设置 Canvas 像素值会导致 Retina 屏模糊——必须结合 window.devicePixelRatio 手动缩放 canvas 的 width/height 属性,否则图表线条发虚、文字锯齿。
为什么 contentRect 不能直接当 canvas 像素尺寸用
Canvas 渲染质量取决于其 DOM 属性 width 和 height(单位:CSS 像素),而非 CSS 样式宽高。Retina 屏下,1 个 CSS 像素可能对应 2 或 3 个物理像素。entry.contentRect.width 返回的是 CSS 像素值,若直接赋给 canvas.width,实际渲染分辨率就只有 1x,必然模糊。
-
contentRect.width是布局计算后的 CSS 像素,和设备无关 -
canvas.width必须是整数,且应等于cssWidth × devicePixelRatio - 忽略
devicePixelRatio是高清屏图表模糊最常见原因 - 设置
canvas.style.width后,还需同步调整canvas.width,否则会拉伸失真
如何在 ResizeObserver 回调里正确重设 canvas 分辨率
核心是三步:读 CSS 尺寸 → 计算物理像素 → 重设 canvas 属性 + 重绘坐标系。不能只改 style,也不能漏掉 ctx.scale()。
- 先获取
entry.contentRect的width和height - 用
const dpr = window.devicePixelRatio || 1获取设备比 - 设置
canvas.width = Math.floor(width * dpr),canvas.height = Math.floor(height * dpr) - 立刻执行
ctx.scale(dpr, dpr),让后续绘图坐标自动适配 - 最后调用你的重绘逻辑(如清空、重画柱状图、重设文字大小)
const canvas = document.getElementById('my-chart');
const ctx = canvas.getContext('2d');
const resizeObserver = new ResizeObserver(entries => {
const entry = entries[0];
const { width, height } = entry.contentRect;
const dpr = window.devicePixelRatio || 1;
<p>canvas.width = Math.floor(width <em> dpr);
canvas.height = Math.floor(height </em> dpr);
canvas.style.width = <code>${width}px</code>;
canvas.style.height = <code>${height}px</code>;</p><p>ctx.scale(dpr, dpr); // 关键:重置坐标系
redrawChart(); // 你自己的绘图函数
});
resizeObserver.observe(canvas.parentElement);监听父容器而非 canvas 自身更稳妥
直接观察 canvas 元素本身容易出问题:某些 CSS 布局(如 transform: scale())会改变渲染尺寸但不触发 ResizeObserver;而父容器(比如 div#chart-wrapper)的 contentRect 更稳定,且能覆盖侧边栏折叠、Flex 容器收缩等真实业务场景。
- 监听
canvas.parentElement是多数生产项目的默认做法 - 如果父容器有 padding/border,
contentRect已剔除它们,拿到的就是 content-box 尺寸,可直接用 - 避免监听
canvas自身 —— 它的尺寸常由父级控制,自身变化极少,且易受 CSStransform干扰 - 若需监听多个图表容器,复用一个
ResizeObserver实例并多次调用.observe()即可
防抖不是必须,但 disconnect() 一定不能漏
ResizeObserver 回调本身已做浏览器级合并(不会像 window.resize 那样每帧都触发),所以通常无需额外防抖。但组件卸载时若不手动断开,会造成内存泄漏和重复回调。
- React 中在
useEffect清理函数里调用observer.disconnect() - Vue 中在
onBeforeUnmount钩子中执行断开 - 原生 JS 也应在移除元素前显式调用,否则观察者持续持有 DOM 引用
- 不要依赖 GC 自动回收 —— ResizeObserver 的引用链不易被识别,泄漏风险明确
真正容易被忽略的,是 canvas 的 width/height 属性重设后,必须同步调用 ctx.scale()。漏掉这一步,所有路径、文字、阴影都会按 1x 渲染,再高清的屏幕也救不回模糊感。

















