在后台管理系统、数据看板和可视化页面中,组件尺寸经常发生变化。例如,用户调整侧边栏宽度、切换布局模式或改变浏览器窗口大小时,图表容器的宽度都可能改变。
传统方案通常监听 window.resize 事件,但这种方式只能感知浏览器窗口变化,无法准确覆盖组件自身尺寸变化的所有场景。
现代浏览器提供了 ResizeObserver API,可以直接监听指定元素的尺寸变化,从而实现更加精确的自适应布局。
本文通过一个数据面板,介绍 ResizeObserver 的使用方式、性能优化方法和资源释放机制。
一、为什么不应该只依赖 window.resize?
假设一个管理后台采用左右两栏布局,左侧为导航菜单,右侧为数据图表。
当用户收起导航菜单时,右侧图表区域会变宽,但浏览器窗口的尺寸没有变化。
此时,单纯监听 window.resize 无法感知这一布局调整。
首先定义一个基础的数据面板:
<section class="dashboard" data-dashboard>
<header class="dashboard-header">
<h2>数据统计面板</h2>
<p>实时展示业务指标与趋势</p>
</header>
<div class="chart-container" data-chart-container>
<canvas data-chart></canvas>
</div>
</section>
<a href="https://www.yxtiktok.com/blog/guides/">
这里通过 data-chart-container 标记需要监听的容器。
后续 JavaScript 不需要依赖复杂的 CSS 选择器,而是直接通过稳定的属性选择目标元素。
二、创建 ResizeObserver 实例
ResizeObserver 的构造函数接收一个回调函数。
当被观察元素的尺寸发生变化时,浏览器会将相关信息传入回调。
const container = document.querySelector(
"[data-chart-container]"
);
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const {
width, height } = entry.contentRect;
console.log("容器宽度:", width);
console.log("容器高度:", height);
}
});
observer.observe(container);
contentRect 提供元素内容区域的宽高信息。
相比频繁调用 getBoundingClientRect(),ResizeObserver 更适合处理由布局变化引起的尺寸更新。
需要注意,ResizeObserver 不负责监听元素的位置变化。如果元素只是移动了位置,但尺寸没有变化,回调不一定执行。
三、根据容器宽度调整布局模式
在实际项目中,可以根据容器自身宽度决定展示方式,而不必完全依赖浏览器窗口断点。
例如,当图表区域较窄时,将统计卡片改为单列展示;当容器较宽时,恢复多列布局。
先定义统计卡片结构:
<div class="statistics" data-statistics>
<article class="stat-card">
<h3>访问次数</h3>
<strong>12,480</strong>
</article>
<article class="stat-card">
<h3>活跃用户</h3>
<strong>3,260</strong>
</article>
<article class="stat-card">
<h3>任务完成率</h3>
<strong>92.6%</strong>
</article>
</div>
<a href="https://www.yxtiktok.com/blog/guides/">
通过 JavaScript 根据宽度更新布局状态:
const statistics = document.querySelector(
"[data-statistics]"
);
const layoutObserver = new ResizeObserver(entries => {
const entry = entries[0];
if (!entry) {
return;
}
const width = entry.contentRect.width;
statistics.dataset.layout =
width < 480 ? "compact" : "expanded";
});
layoutObserver.observe(statistics);
对应的 CSS:
.statistics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
.statistics[data-layout="compact"] {
grid-template-columns: 1fr;
}
这里由 JavaScript 负责判断组件状态,由 CSS 负责具体的视觉布局。
对于纯粹的响应式样式需求,现代 CSS 的容器查询通常更加合适。ResizeObserver 的优势主要体现在尺寸变化还需要触发 JavaScript 业务行为的场景。
四、避免尺寸变化引发频繁计算
如果图表内部包含大量数据点,每次尺寸变化都立即执行完整绘制,可能造成不必要的计算。
可以利用 requestAnimationFrame() 合并同一渲染周期内的多次更新。
先增加图表展示区域:
<section class="performance-panel">
<h2>性能趋势</h2>
<div class="performance-content" data-performance>
<canvas data-performance-canvas></canvas>
</div>
<p class="performance-description">
图表根据容器实际宽度调整。
</p>
</section>
<a href="https://www.yxtiktok.com/blog/guides/">
然后实现绘制调度:
let frameId = null;
let pendingWidth = 0;
function scheduleRender(width) {
pendingWidth = width;
if (frameId !== null) {
return;
}
frameId = requestAnimationFrame(() => {
frameId = null;
renderChart(pendingWidth);
});
}
const chartObserver = new ResizeObserver(entries => {
for (const entry of entries) {
scheduleRender(entry.contentRect.width);
}
});
这种设计避免在短时间内重复提交绘制任务。
但是,requestAnimationFrame() 并不是万能的性能优化工具。如果绘制计算本身非常耗时,仍然可能阻塞主线程。
对于复杂的数据处理,还可以考虑将计算部分放入 Web Worker。
五、正确处理 Canvas 的高清屏绘制
Canvas 具有两个尺寸概念:CSS 显示尺寸和内部绘图缓冲区尺寸。
在高像素密度屏幕上,如果只设置 CSS 宽度,图表可能出现模糊。
定义 Canvas 容器:
<div class="chart-wrapper" data-chart-wrapper>
<canvas
data-responsive-canvas
aria-label="业务数据趋势图"
></canvas>
</div>
<a href="https://www.yxtiktok.com/blog/guides/">
根据实际设备像素比调整缓冲区尺寸:
function resizeCanvas(canvas, width, height) {
const ratio = window.devicePixelRatio || 1;
canvas.width = Math.round(width * ratio);
canvas.height = Math.round(height * ratio);
canvas.style.width = `${
width}px`;
canvas.style.height = `${
height}px`;
const context = canvas.getContext("2d");
context.setTransform(
ratio, 0,
0, ratio,
0, 0
);
return context;
}
使用 setTransform() 而不是反复调用 scale(),可以避免多次重绘时缩放比例累积。
每次调整 Canvas 的内部宽高都会重置绘图状态,因此需要重新设置变换矩阵并重新绘制内容。
六、组件卸载时释放观察器
ResizeObserver 会保留观察目标与回调之间的关联。
在单页应用中,如果组件已经从页面移除,却没有停止观察,可能增加不必要的资源占用。
例如,在弹窗组件中使用尺寸观察:
<dialog data-settings-dialog>
<section class="settings-content" data-settings-content>
<h2>显示设置</h2>
<p>调整布局参数后更新预览区域。</p>
</section>
</dialog>
<a href="https://www.yxtiktok.com/blog/guides/">
可以将观察器封装成具有明确生命周期的函数:
function observeElementSize(element, callback) {
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
callback(entry.contentRect);
}
});
observer.observe(element);
return () => {
observer.disconnect();
};
}
使用时:
const stopObserving = observeElementSize(
document.querySelector("[data-settings-content]"),
size => {
console.log(size.width, size.height);
}
);
// 组件卸载时执行
stopObserving();
这种封装方式适合在不同组件中重复使用。
七、ResizeObserver 的常见问题
1. 回调中直接修改被观察元素尺寸
如果回调触发后立即修改元素宽度,而新的宽度又触发回调,就可能造成尺寸反馈循环。
浏览器通常会对这种情况进行限制,并可能报告 ResizeObserver 循环相关错误。
应尽量避免在回调中无条件修改观察目标的尺寸。
2. 观察器数量过多
不需要为每个元素都创建独立的 ResizeObserver 实例。
一个观察器可以同时观察多个元素,并通过 entry.target 区分不同目标。
3. 重复进行复杂 DOM 查询
观察目标及相关元素在组件生命周期内保持稳定时,可以缓存引用。
这样可以减少回调执行过程中的重复查询。
4. 忽略初始尺寸
调用 observe() 后,符合条件的初始尺寸通知通常会在浏览器后续渲染阶段触发。
如果业务需要同步取得初始布局信息,可以单独读取当前尺寸,但应避免不必要的强制布局计算。
八、总结
ResizeObserver 解决了传统窗口尺寸监听无法精确感知组件内部变化的问题。
通过它可以实现容器驱动的图表重绘、自适应卡片布局和组件尺寸状态管理。
在实际项目中,应结合 CSS 容器查询、requestAnimationFrame 和明确的资源释放机制,让组件保持稳定的性能表现。
对于复杂后台系统,真正有效的布局优化不仅是减少重绘次数,还包括避免重复计算、减少 DOM 操作,并确保组件生命周期结束时正确释放资源。