Guide
Resize Observer API explained
Harbor Commerce’s merchant analytics dashboard rendered six Chart.js canvases
inside a collapsible sidebar layout. The team wired
window.addEventListener('resize', redrawAll) so charts
reflowed when users rotated a phone. That worked for orientation changes — but
it also fired when users toggled the sidebar, opened a filter drawer, or resized a
split panel, because those actions changed container width without changing
the viewport. Every sidebar click triggered six full chart redraws and a 120 ms
main-thread stall. Replacing the global listener with
ResizeObserver — one observer per chart wrapper that called
chart.resize() only when that wrapper’s box changed — cut
wasted redraws by 84% and dropped
INP
on the toggle interaction from 210 ms to 48 ms. The
Resize Observer API is a browser-native way to ask “when did
this element’s size change?” and receive batched, asynchronous callbacks.
It powers responsive charts, dynamic text truncation, sticky layout recalculation,
embed sizing, and component libraries that must react to parent width — not
just the window. This guide covers observer lifecycle, box size entry fields,
pairing with
container queries
and
Intersection Observer,
the Harbor Commerce dashboard refactor, a technique decision table, pitfalls, and a
production checklist.
Why window resize listeners miss the real problem
Modern layouts are nested: sidebars, split panes, CSS grid areas, and
responsive components
that reflow inside a parent without the viewport changing. A
window resize event fires only when the browser window dimensions
change — not when:
- A sidebar collapses from 280 px to 64 px icons-only mode.
- A modal opens and squeezes the main content column.
- Flex or grid siblings grow because one child loads async content.
- Text wraps after a font loads or a translation string arrives.
- An accordion section expands and shrinks a chart container below it.
Developers used to poll element.offsetWidth inside
requestAnimationFrame loops or attach
MutationObserver to watch DOM changes — both expensive and
indirect. ResizeObserver delivers size-change notifications directly from the
layout engine, batched per frame, without forcing synchronous layout reads in
your scroll or input handlers.
ResizeObserver vs Intersection Observer
These APIs solve different questions. Intersection Observer reports whether an element intersects a root region (viewport or scroll container) — useful for lazy loading and infinite scroll. ResizeObserver reports when an element’s content or border box dimensions change. A chart can stay fully visible while its container width halves; only ResizeObserver notices. Use both together when a component needs to know visibility and available width.
API basics: observe, callback, disconnect
The API is small. Create one observer, pass a callback, call
observe(target) on each element whose size you care about:
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
// legacy shorthand — still widely supported
chart.resize(width, height);
}
});
ro.observe(chartContainer);
// later: ro.unobserve(chartContainer) or ro.disconnect()
Key methods:
observe(element)— start watching an element. Observing the same element twice is a no-op.unobserve(element)— stop watching one target without tearing down others.disconnect()— stop watching all targets and release the observer. Call this on component unmount.
Reading size from entries
Each callback receives an array of ResizeObserverEntry objects. Modern
browsers expose granular box sizes:
entry.contentBoxSize— the content box (inside padding). Prefer this for canvas and SVG layout.entry.borderBoxSize— includes padding and border. Use when your drawing surface fills the outer box.entry.contentRect— legacyDOMRectReadOnlywithwidthandheight. Still fine for most cases; map to the newer arrays when you need sub-pixel precision or multiple fragments.entry.devicePixelContentBoxSize— physical pixels for high-DPI canvas backing stores. Essential for crisp chart rendering on Retina displays.
Callbacks are delivered before paint in the same frame when possible, but you should still keep work cheap. Debounce heavy redraws (chart recompute, layout diff) if multiple observed elements resize in one frame.
Common use cases
Responsive charts and canvases
Chart.js, D3, ECharts, and Mapbox GL all need an explicit
resize() when their container changes. Observe the wrapper div, read
contentBoxSize, set the canvas backing store to
width × devicePixelRatio, and call the library’s resize
API. This is more reliable than window listeners and survives sidebar animations.
Dynamic text truncation and “show more”
Clamp headings to two lines with CSS -webkit-line-clamp, then use
ResizeObserver on the text node’s parent to detect when available width
changes and re-measure whether the overflow button should appear. Container
queries handle static breakpoints; ResizeObserver handles JS-driven truncation
logic.
Virtual lists and split layouts
Virtual scrollers need the viewport height of their scroll container. When a header collapses or a bottom sheet appears, the scrollport height changes without a window resize. Observe the scroll container and pass the new height to your row-height calculator.
Third-party embeds and iframes
Embedded widgets (maps, payment forms, video players) often assume a fixed width. Observe the slot element and postMessage the new dimensions to the iframe, or trigger the vendor’s responsive API.
When CSS alone is enough
If your component only needs different styles at certain widths — stack columns, hide labels, change font size — prefer container queries or media queries. Reserve ResizeObserver for imperative APIs (canvas, WebGL, measuring text overflow) that CSS cannot drive.
Harbor Commerce dashboard refactor (worked example)
The merchant analytics page had six chart cards in a responsive CSS grid. A 240 px sidebar sat left; toggling it animates width over 200 ms. The old code:
- Attached one
windowresize listener on page load. - On fire, looped all six Chart.js instances and called
chart.resize(). - Sidebar toggle did not fire
windowresize — so engineers also calledredrawAll()manually from the sidebar click handler, duplicating logic.
The refactor:
- Created a module-level
ResizeObserverwith aWeakMap<Element, Chart>from container to chart instance. - Each card mounted with
ro.observe(cardEl); unmount calledro.unobserve(cardEl). - The callback read
entry.contentBoxSize[0].inlineSizeand calledchart.resize()only for entries in the batch. - Removed the window listener and the manual sidebar redraw.
Results: sidebar toggle INP dropped from 210 ms to 48 ms; orientation changes still reflowed correctly because the grid cells resize with the viewport; no chart drew at stale dimensions after the 200 ms CSS transition because ResizeObserver fires on each intermediate layout during the animation.
React and framework patterns
Pitfalls in React and Vue are similar:
- Do not create an observer per render. Use
useRef+useEffectto construct once and disconnect on cleanup. - Prefer a shared observer. One module-level ResizeObserver with a Map from element to callback scales to dozens of chart cards.
- Guard against zero-size containers. Hidden tabs and
display: noneparents report 0×0. Skip resize logic or defer until the element is visible (pair with Intersection Observer or tab activation). - Libraries:
@react-hook/resize-observerand component libraries (Recharts, MUI) wrap the API; understand the underlying observer so you can debug double-subscribe leaks.
Technique decision table
| Need | Recommended approach | Why |
|---|---|---|
| Stack columns when a card is narrower than 400 px | CSS container queries | Declarative, no JS, works without resize callbacks |
| Redraw Chart.js when a flex sibling collapses | ResizeObserver on chart wrapper | Window resize misses parent-driven width changes |
| Lazy-load images below the fold | Intersection Observer | Visibility, not size, is the trigger |
| React to viewport breakpoint (mobile vs desktop) | matchMedia or CSS media queries |
Viewport-level, not element-level |
| Detect DOM node insertion | MutationObserver | ResizeObserver does not fire on mount alone unless size is non-zero |
| Crisp canvas on Retina displays | ResizeObserver + devicePixelContentBoxSize |
Sets backing store to physical pixels |
Common pitfalls
- Infinite resize loops — callback changes element size (e.g. sets explicit height) which triggers another callback. Break the cycle with threshold checks or only updating when delta exceeds 1 px.
- Observing
document.bodyorhtml— fires on every page mutation; observe specific containers instead. - Heavy work in the callback — chart full-data recomputes
belong in
requestAnimationFrameor a debounced scheduler, not inline. - Forgetting
disconnecton SPA route change — leaks observers and redraws off-screen charts. - Zero-size hidden panels — resizing a chart inside a closed tab to 0×0 breaks aspect ratio; resize on tab activation.
- Replacing CSS container queries unnecessarily — JS resize handlers for pure styling add complexity and main-thread cost.
- Assuming synchronous dimensions — read sizes from the entry object in the callback, not from a stale closure.
Production checklist
- Identify components that call imperative
resize()(canvas, maps, video). - Observe the nearest stable wrapper, not the canvas element itself if padding differs.
- Share one ResizeObserver across multiple targets where possible.
- Read
contentBoxSizeordevicePixelContentBoxSizefor new code; fall back tocontentRect. - Guard callbacks: skip when width or height is zero or unchanged within 1 px.
- Call
unobserveordisconnecton component unmount and route teardown. - Defer chart init until the container has non-zero size (tab visible, accordion open).
- Prefer container queries for layout-only responsiveness; use ResizeObserver for imperative APIs.
- Profile sidebar, drawer, and split-pane interactions for INP before and after.
- Document which elements are observed so future layout refactors do not duplicate window listeners.
Key takeaways
- ResizeObserver fires when an element’s box size changes — including parent-driven reflows that never touch the window.
- Use observe, unobserve, and disconnect deliberately; share one observer across many targets to avoid leaks.
- Pair with container queries: CSS for styling breakpoints, ResizeObserver for canvas, charts, and text measurement.
- Keep callbacks cheap; guard against zero-size hidden containers and infinite resize feedback loops.
- Intersection Observer answers visibility; ResizeObserver answers dimensions — use the right tool for each.
Related reading
- Intersection Observer API explained — lazy loading and viewport visibility
- CSS container queries explained — component-responsive layout without JS
- Responsive web design explained — breakpoints, fluid layout, and mobile-first patterns
- Debouncing and throttling explained — scheduling expensive resize follow-up work