Home›Journal›This post

ResizeObserver Loops: Stop Feedback

Model observed layout as a state-transition graph, reproduce a 318↔326 oscillation, and replace suppression with a bounded stability receipt.

JP
JP Casabianca
AI Engineer and Product Designer · full-stack delivery · Bogotá

ResizeObserver loops are feedback systems: a callback reads a box, writes layout that changes that box, and asks the browser to observe the consequence again. This guide maps the cycle, reproduces a two-state oscillation, and replaces error suppression with a bounded convergence receipt.

ResizeObserver loops are layout feedback

The familiar message “ResizeObserver loop completed with undelivered notifications” names a delivery condition, not the root mutation. ResizeObserver loops begin when a callback reads an observed box, writes something that changes that box, and causes the same decision to run again. Hiding the message leaves the causal edge intact.

Model the component as a feedback system. Record the measured box, component state before the callback, requested state, property or class written, and size after layout. A stable component reaches a no-op: the next decision requests the state already applied. A cyclic component revisits an earlier canonical state-and-size pair.

This framing separates deterministic logic from browser scheduling. A local state-machine simulator can prove that a rule oscillates for declared sizes. A real DOM probe can show how one browser/version schedules observations and error events. Neither should borrow the other’s certainty.

The container-query card layout is the first escape route when the decision is presentational. CSS can own the relationship between container size and child styling without a JavaScript measurement loop. Use observation when code genuinely needs size data, then demand a bounded convergence receipt rather than a filtered console.

ResizeObserver delivery-loop anatomyA circular graph links measure, callback, class or style write, and layout, while a separate skipped-notification branch ends at an error event rather than application crash.THE ERROR MESSAGE IS A BRANCH · THE WRITE IS THE EDGEMEASURECALLBACKCLASS / STYLE WRITELAYOUTSKIPPED → ERROR EVENTdeferral ≠ application crash
ResizeObserver delivery-loop anatomy. The diagram and visible semantic equivalent carry the same conclusion.
  1. Measure: observe the declared content or border box.
  2. Callback: read size and decide component state.
  3. Write: class, style, child, font, padding, or border changes layout.
  4. Layout: if the observed box changes, the graph returns to measure.
  5. Skipped branch: the delivery algorithm can defer notifications and issue a loop error; this is not itself an application crash.
  6. Suppression: filtering the error does not remove the read/write cycle.

Reading rule: labels, shapes, patterns, markers, and the semantic content carry every conclusion; color is supplementary.

Read the delivery algorithm before debugging

ResizeObserver gathers active observations and delivers entries according to depth. If ResizeObserver loops produce more size changes, the user agent can repeat processing; observations that cannot be delivered in the current cycle are skipped and an error notification is issued. Delivery can continue in a later rendering opportunity. That mechanism limits same-cycle work; it does not prove the application has converged.

The Resize Observer editor’s draft defines observation boxes, active and skipped observations, depth, delivery, and loop-error notification. Read it as a behavior contract, not as a fixed callback-count benchmark. Browser scheduling details and the exact number of observed callbacks can vary.

Choose the observed box deliberately. Content-box and border-box sizes react to different properties. Writing padding or border while reading border-box size creates a direct edge; changing child content can create an intrinsic path back to either box. Record logical inline and block dimensions so vertical writing modes remain intelligible.

A ResizeObserver loop error does not by itself mean a crash, nor is it always harmless. It means some observations were deferred. The debugging question is whether the component reaches a stable state within a declared budget and whether deferred visual updates are acceptable. Suppression answers neither question.

Draw the read/write dependency graph

To map ResizeObserver loops, start with four nodes: observed target and box; callback measurement; state decision; layout mutation. Draw an arrow from the mutation back to the observed size whenever the changed class, style, child, font, border, padding, grid track, or intrinsic contribution can affect that box. The cycle is present even if the arrow crosses several ancestors.

Write the actual property on the edge. “Updates component” is too vague to debug. “Toggles compact class, changing horizontal padding by four pixels per side” reveals why border-box size moves. Note ownership: a child-only custom property is safer only if the child cannot feed intrinsic size back into the observed parent.

Use Long Animation Frames UI diagnosis if the loop also creates main-thread stalls, but keep responsiveness evidence separate from convergence. A quick callback can still oscillate forever; a slow stable callback has a different problem.

The graph also reveals redundant observation. If the decision merely changes layout presentation at a width, a container query removes the callback, state, and write nodes. If JavaScript must update an external chart scale or coordinate another system, preserve the observer but isolate mutation ownership and make the requested state idempotent. ResizeObserver loops become tractable when every return path to measurement has a name.

Reproduce the 318↔326 oscillation

The frozen component has regular and compact states. Its observed border-box inline size is 318 pixels when regular and 326 pixels when compact because the compact class changes geometry. The naïve rule says: below 320, request compact; otherwise request regular.

Start at (regular,318). Delivery one requests compact and writes the class. The new size is 326. Delivery two observes (compact,326), requests regular, and writes again. The size returns to 318. At delivery three, (regular,318) repeats, so the simulator reports a period-2 cycle. It must not call the first repeat convergence.

Identity includes both state and measured size. Tracking size alone can misclassify cases where two component states share a dimension but imply different future mutations. Tracking state alone can hide size drift. Store the first index for each canonical pair and compute the period from the repeated witness.

This is a deterministic resize feedback cycle, not a frozen browser callback count. The real probe labels frame number, observation box, inline and block sizes, state, scheduled mutation, applied mutation, loop-error event, and teardown as browser-observed fields. That boundary lets ResizeObserver loops be explained without pretending the simulator is a layout engine.

318↔326 repeated-state traceA four-delivery timeline alternates regular at 318 pixels and compact at 326 pixels around threshold 320, marking the first repeated pair on delivery three.RULE: SIZE < 320 → COMPACT · OTHERWISE REGULAR318regular→ compactdelivery 1new326compact→ regulardelivery 2new318regular→ compactdelivery 3REPEAT326compact→ regulardelivery 4cycleFIRST REPEAT AT 3 · PERIOD = 3 − 1 = 2
318↔326 repeated-state trace. The diagram and visible semantic equivalent carry the same conclusion.
Frozen state-transition trace
DeliveryMeasured sizePrior stateRequested stateActionRepeat
1318regularcompactwriteno
2326compactregularwriteno
3318regularcompactwriteyes: repeats delivery 1

Classification: period-2 cycle. Browser callback/error counts are not frozen.

Reading rule: labels, shapes, patterns, markers, and the semantic content carry every conclusion; color is supplementary.

Use CSS when the decision is presentational

A container query lets descendants respond to an ancestor’s size inside the cascade. For a card that only changes spacing, type, visibility, or arrangement, CSS owns the problem more directly than a callback that measures and writes a class. Source order, specificity, and a documented fallback still matter, but there is no application-level observer cycle to converge.

Ask three questions. Does JavaScript consume the measurement outside presentation? Does the chosen style change the queried container’s own sizing? Can the child adapt without changing semantic state? If the answers point to presentation, compare container query vs ResizeObserver and remove the observer.

Container queries are not magic containment for every intrinsic dependency. The query container configuration changes which sizes are available, and fallbacks must remain usable in the supported browser set. The CSS Containment specification defines relevant containment semantics and their layout consequences. Applying contain:size can change sizing behavior, so it is an architectural choice rather than an error silencer.

Keep typed CSS design logic in the same toolbox: when values and branching remain in CSS, the cascade can express a stable relationship without exporting a measurement loop to script. Use JavaScript only for a decision CSS cannot own.

Stage necessary writes and make them idempotent

When JavaScript is necessary, compare the desired state with the last applied state before writing. If they match, do nothing and record the no-op. A last-state guard prevents redundant mutations; it does not fix a rule whose desired state alternates with the resulting size.

Staging the mutation in requestAnimationFrame moves it out of the observer callback and into a later rendering phase. The HTML rendering-loop specification supports reasoning about that boundary. ResizeObserver requestAnimationFrame staging can still oscillate once per frame, so the receipt passes only after a stable no-op, never because the warning disappeared.

Hysteresis gives entry and exit different thresholds. Enter compact below 316 and leave above 324. From (regular,318), no write is needed. From (regular,314), enter compact; if compact measures 322, remain compact because 322 is not above 324. The deadband absorbs small geometry movement but is not universally correct.

An epsilon guard treats sub-threshold changes as equivalent only when that tolerance fits the product. Child-only writes can isolate visual data, yet an intrinsically sized child can still resize its parent. Containment can break the path by changing layout semantics. ResizeObserver loops need the smallest defensible mutation edge, plus an explicit convergence test for the graph that remains.

Repair and convergence matrixThree lanes compare a container query, an animation-frame staged idempotent write, and hysteresis or containment, each with use, caveat, and convergence labels.REMOVE THE EDGE OR PROVE A NO-OP1CONTAINER QUERYUSE: presentationCAVEAT: fallback + container semanticsCONVERGENCE: ✓ no JS write2rAF + IDEMPOTENCEUSE: JS needs sizeCAVEAT: rAF can still oscillateCONVERGENCE: △ require stable no-op3HYSTERESIS / CONTAINUSE: bounded state / ownershipCAVEAT: deadband or layout changesCONVERGENCE: △ test intrinsic paths
Repair and convergence matrix. The diagram and visible semantic equivalent carry the same conclusion.
Repair choices and proof boundaries
InterventionUse whenDoes not proveConvergence check
Container queryThe decision is presentationalEvery intrinsic-size path is harmlessNo observer-owned layout write remains
rAF-staged idempotent writeJavaScript genuinely consumes sizerAF guarantees stabilityRequested state reaches a no-op inside the budget
HysteresisA justified deadband separates entry and exitThe thresholds fit every layoutNo repeated state-and-size pair
Containment / child-only writeOwnership can be isolatedIntrinsic feedback is impossibleTest real content; document changed layout semantics

Reading rule: labels, shapes, patterns, markers, and the semantic content carry every conclusion; color is supplementary.

Test convergence, teardown, and real resize paths

For ResizeObserver loops, exercise startup, a repeated Start action, Stop, Reset, content changes, font and image completion, viewport resize, zoom, and writing-mode changes. Repeated Start must disconnect the previous observer, cancel pending animation frames, and remove any loop-error listener before creating replacements. Stop must leave no live observer, frame, or listener.

The deterministic simulator ends in one of three states: stable no-op, repeated-state cycle with a period, or delivery-cap exhausted. Reaching the cap is unknown/non-convergent, never stable. Validate finite sizes from 100–1,200 pixels, thresholds from 100–1,000, epsilon from 0–16, mutation delta from -128–128, and a delivery budget from 2–64 before iterating.

Exact thresholds deserve tests because replacing a strict less-than comparison with a less-than-or-equal comparison changes behavior. Test equal regular/compact sizes, stable first delivery, both hysteresis starts, epsilon at and just beyond its boundary, the frozen period-2 trace, a period-1 synthetic self-cycle, and cap exhaustion. Hostile labels must reach the page only through text nodes.

PerformanceObserver INP debugging can connect real user interactions to responsiveness. It cannot prove this state graph converges. Keep performance observations, browser callback timing, and deterministic transition truth in separate report fields.

Publish the stability receipt

Export schema and algorithm versions, normalized inputs, canonical transition list, first repeated-state witness, period, convergence reason, write and no-op counts, and a receipt hash. Hash only deterministic simulator data. Store real-probe observations separately, with browser-dependent time and frame fields excluded from that hash.

Each transition should name delivery index, measured inline size, prior state, requested state, mutation, and repeat status. Limitations belong in the receipt: the simulator is not a browser layout engine; callback and error counts vary; staging, child writes, containment, and hysteresis have trade-offs; and global error suppression proves nothing.

For monitoring, count affected components and stable versus cyclic receipts rather than collecting user DOM or content. Redact application identifiers when a minimal component name is enough. The goal is to remove a causal mutation edge, not build a warehouse of layout snapshots. Preserve the algorithm version and input bounds with each aggregate so a later simulator change cannot be confused with an application improvement.

ResizeObserver loops stop being mysterious when represented as state transitions. Reproduce the smallest component, find the first write that returns to its observed box, select CSS or a guarded ownership boundary, and require a no-op within the declared budget. Keep the receipt beside the fix so a later design change cannot quietly restore the cycle.

The final review should be adversarial: change content length, load a font late, zoom, switch writing mode, reconnect the component, and rerun the exact deterministic fixture. A missing warning is weak evidence. A named no-op, no repeated state, complete teardown, and a bounded transition history are the evidence the repair actually needs. Record which observation box and writing axis each test exercised, plus the component revision. That small context turns a green receipt into a repeatable maintenance check instead of a one-time console cleanup.

Runnable local artifact — The simulator explains state transitions rather than reproducing a layout engine; browser probe counts vary, and rAF, containment, child writes, and hysteresis are not universal cures.

Plain text1 line
Record state-and-size transitions, reject repeated states as cycles, require a no-op inside the budget, and keep browser observations outside the deterministic hash.