题干与适用场景
你维护一个多列仪表盘。卡片的尺寸由网格、拖拽、侧栏和字体共同决定,不能假设它只随视口变化。卡片宽度低于 320px 时显示紧凑标题和单列指标,高于该阈值时显示完整布局;窗口不变时,侧栏折叠也必须立即生效。
这道题考察的核心是元素级尺寸观察、浏览器布局时序、React 生命周期和性能边界。公开前端面试资料通常覆盖 DOM、CSS、浏览器事件和性能;官方文档则明确了 ResizeObserver 的通知语义与循环保护,适合把“会用 API”追问到可验证的工程设计。
面试官考察点
- 能否说明
window.resize只描述视口变化,无法覆盖网格重排、字体替换或父容器改变。 - 能否选择
ResizeObserver的content-box或border-box,并解释读取尺寸的含义。 - 能否把测量写入状态或 CSS 变量,而不是在回调里同步改尺寸造成反馈环。
- 能否处理观察目标变化、组件卸载、隐藏元素、旧浏览器降级和大量卡片的成本。
- 能否用真实用户指标、长任务和布局轨迹证明“流畅”,而非只看回调次数。
回答前需要澄清的问题
- 阈值依据内容宽度、边框盒宽度,还是 CSS 容器查询已经能表达的纯样式差异?
- 卡片尺寸变化是否需要触发数据请求,还是只改变呈现?
- 需要连续跟随每一帧,还是允许一帧内合并一次更新?
- 浏览器最低版本是否支持 ResizeObserver?服务端渲染阶段是否可能访问
window? - 卡片数量是几十个还是上千个?是否有虚拟列表或只观察可见卡片?
30 秒回答框架
“我先确认这是元素尺寸而非视口尺寸问题。纯样式切换优先用 CSS 容器查询;需要把尺寸交给 JavaScript 时,在客户端为卡片挂载 ResizeObserver,读取约定的 box 尺寸,经过阈值和去重后写入 CSS 变量或状态。回调只读测量,不同步修改会影响被观察尺寸的样式;必要的 DOM 写入放到下一帧,并在清理时 unobserve。大量卡片要限制观察范围并测量 INP、长任务和布局成本。旧环境用窗口监听或固定布局作为明确降级。”
分步骤深入解答
第一步:先判断 CSS 是否足够
如果只是“宽度小于 320px 显示紧凑样式”,容器查询通常比 JavaScript 更简单,也不会引入测量状态。只有当尺寸要驱动图表采样、虚拟化窗口、第三方渲染器或可观测业务逻辑时,才需要 ResizeObserver。候选人应先问清需求,避免为 CSS 问题增加脚本。
第二步:定义尺寸契约
ResizeObserverEntry 可提供 content box、border box 等尺寸。要先约定阈值对应哪一个盒子,并统一单位与取整策略。不要把一次浮点测量直接当作稳定的业务事件;例如宽度从 319.9 到 320.1 时,可以按整数或断点状态去重。
第三步:建立观察生命周期
在客户端组件挂载后创建 observer,观察卡片节点;节点替换或组件卸载时调用 unobserve 或 disconnect。回调不得引用已经卸载的状态。React 中应让 ref 负责目标节点,Effect 负责 observer 的创建与清理,避免每次普通渲染都重建实例。
const ref = useRef<HTMLDivElement>(null)
const [compact, setCompact] = useState(false)
useEffect(() => {
const node = ref.current
if (!node || !('ResizeObserver' in window)) return
const observer = new ResizeObserver(([entry]) => {
const width = entry.contentRect.width
const next = width < 320
setCompact((current) => (current === next ? current : next))
})
observer.observe(node)
return () => observer.disconnect()
}, [])第四步:避免观察回调形成反馈环
浏览器会在布局后批量投递尺寸通知。如果回调修改了被观察元素的宽高,修改又触发下一轮通知,最终可能出现 ResizeObserver loop completed with undelivered notifications。回调应只更新与尺寸契约相关的离散状态,或把非必要的视觉写入安排到 requestAnimationFrame,并用期望尺寸或状态去重。
第五步:分离测量和渲染
测量值可以写入 CSS 自定义属性,让 CSS 负责布局;也可以只保留 compact 这样的离散状态。避免把每个像素变化都塞进 React state,拖拽时会产生大量渲染。若确实需要连续值,使用 rAF 合并同一帧通知,并记录丢帧与长任务。
第六步:处理多卡片和不可见节点
几十个卡片通常可以各自观察;上千个卡片应只观察可见节点,或由布局层统一分发尺寸。display: none、折叠面板和虚拟列表会改变可观测尺寸,恢复可见后要验证首个通知是否能重新建立状态。不要把观察器当作轮询器,也不要在回调里遍历整个仪表盘。
第七步:明确降级与服务端边界
服务端渲染不能访问 window。组件应在客户端 Effect 中检测能力;不支持 ResizeObserver 时,使用固定布局、CSS 媒体查询或经过节流的窗口监听,并说明降级会失去哪些元素级适配。降级路径也要有可测试的阈值和视觉结果。
第八步:用证据验证体验
测试侧栏折叠、拖拽、字体加载、网格重排、旋转屏幕和浏览器缩放。Chrome Performance 记录回调、布局、绘制和长任务;用 INP 或输入延迟确认拖拽时仍能响应。WebKit 文档提醒回调修改尺寸可能触发通知循环,因此应专门断言控制台无循环告警,并确认卸载后观察器不再更新状态。
设计取舍与边界
容器查询与 ResizeObserver
容器查询适合纯 CSS 断点,声明性强、没有状态同步。ResizeObserver 适合 JavaScript 计算或第三方渲染,但需要生命周期、去重和性能控制。两者可以并存,边界由“尺寸是否要离开样式层”决定。
content-box 与 border-box
content-box 适合内容排版阈值,border-box 适合卡片包含边框和内边距的可用外框。选错盒子会造成断点提前或滞后;答案应把选择写进契约和测试,而不是只说“取 width”。
连续值与离散值
连续宽度能驱动图表,但更新频率高、渲染成本大;离散断点更稳定、易测试。先用离散状态满足需求,只有性能预算和视觉收益都明确时才引入连续更新。
失败演练与演进计划
失败:只监听 window.resize
侧栏折叠和网格重排不会触发窗口事件,卡片会停留在错误布局。修复是把观测目标放在卡片或其容器,并补充非窗口变化的测试。
失败:回调直接修改被观察尺寸
尺寸变化触发回调,回调再次改变尺寸,可能形成循环和额外布局。修复是写入独立 CSS 变量、使用离散状态,或在下一帧更新且保证幂等。
失败:每个像素变化都 setState
拖拽时会产生高频 React 渲染,输入响应变差。修复是阈值去重、rAF 合并、只观察可见卡片,并用 Performance 面板确认布局成本。
常见误区与追问
误区:ResizeObserver 是更强的 resize 事件
它观察元素盒子并按浏览器布局时序投递通知,不是窗口事件的简单替代。
追问:如何在 React Strict Mode 下验证?
确认开发环境的建立与清理成对出现,观察器数量不会累积;不要把 Strict Mode 的额外检查误判为生产重复订阅。
追问:能否在回调里调用 getBoundingClientRect?
可以读取,但要意识到额外测量可能触发布局开销。优先使用 entry 提供的盒子尺寸,并用性能轨迹证明必要性。
追问:如何测试尺寸循环?
让回调改变会影响自身尺寸的样式,观察控制台告警、通知次数和最终尺寸;修复后断言状态稳定且没有持续通知。
追问:什么时候完全不需要 JavaScript?
只要需求是容器断点下的样式变化,就优先 CSS 容器查询;脚本应留给数据、测量或第三方渲染真正需要的场景。
追问:如何证明优化有效?
比较同一拖拽脚本下的 INP、长任务数量、布局耗时、React 提交次数和内存;不要只比较 observer 回调计数。