Claude Fable 5.1 & GPT-6 Astra packages are live

Rendering

Free

Rendering performance — the frame budget, layout thrash, compositor-only animation, virtualised lists, and keeping interaction responsive.

193 lines8.1 KB Minimax Performance
targetModels
MiniMax M3MiniMax M2MiniMax M FamilyFuture MiniMax Models
name
rendering
category
Performance
description
Rendering performance — the frame budget, layout thrash, compositor-only animation, virtualised lists, and keeping interaction responsive.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for MiniMax: scripts/model-profiles.json -->

#Scope contract

FILE_ISOLATION: Modify only files inside the scope the task names; report any out-of-scope change instead of making it.


#Purpose

Rules for keeping the browser's rendering work cheap. The measurable target is INP under 200 ms and animations that hold their frame budget.

The frame budget is the whole constraint: at 60 Hz a frame is 16.7 ms, and the browser needs part of that for itself. Roughly 10 ms of your work per frame is the ceiling. Exceed it and frames are dropped, which users perceive as jank.


#The pipeline, and where to stop

JavaScript → Style → Layout → Paint → Composite

Every stage you avoid is time saved. Which stages run depends on what property you changed:

ChangingTriggers
width, height, top, left, margin, padding, font-sizeLayout → Paint → Composite
color, background-color, box-shadow, border-radiusPaint → Composite
transform, opacity, filterComposite only

Animate transform and opacity and nothing else. Composite runs on the GPU, off the main thread, and holds 60fps under load that would drop frames if layout were involved.

css
/* Layout on every frame — janky */
.slide { transition: left 300ms; }

/* Compositor only — smooth */
.slide { transition: transform 300ms; will-change: transform; }

Use will-change sparingly and remove it after the animation. Each hint promotes an element to its own layer and consumes GPU memory; applying it broadly makes things worse.


#Avoid layout thrash

Reading a layout property after writing one forces the browser to recompute layout synchronously, in the middle of your loop.

ts
// Forced synchronous layout on every iteration — O(n) layouts
for (const el of items) {
  el.style.height = el.offsetHeight + 10 + "px";   // read after write, repeatedly
}

// Batch: all reads, then all writes — one layout
const heights = items.map((el) => el.offsetHeight);
items.forEach((el, i) => { el.style.height = heights[i] + 10 + "px"; });

Layout-forcing reads include offsetTop, offsetHeight, clientWidth, scrollTop, getBoundingClientRect() and getComputedStyle(). In the DevTools Performance panel they appear as purple "Recalculate Style / Layout" bars inside a script block — that pattern is the signature.

Prefer ResizeObserver and IntersectionObserver over polling geometry: they deliver measurements without forcing layout.


#Render less

  • Virtualise long lists. Rendering 10,000 rows is slow regardless of how cheap each row is. @tanstack/virtual or equivalent renders the visible window plus a small overscan.
  • Paginate rather than rendering everything and hiding most of it with CSS — display: none still costs DOM nodes and memory.
  • content-visibility: auto lets the browser skip rendering off-screen sections entirely; pair it with contain-intrinsic-size so the scrollbar does not jump.
  • CSS containment (contain: layout paint) scopes recalculation to a subtree, so a change inside a widget cannot force layout of the whole page.
  • Keep the DOM shallow. Deep trees make every style recalculation more expensive.

In React specifically: state placed too high re-renders subtrees that do not care, and index keys make React reuse the wrong DOM nodes. Both show up as rendering cost with no obvious cause. → Frontend/react


#Keep interaction responsive

INP measures the worst interaction latency users experience: input → processing → next paint.

  • Never block the main thread with a long task. A 300 ms synchronous handler is 300 ms of unresponsive UI. Break long work with scheduler.yield(), or move it to a web worker.
  • Mark non-urgent updates so typing stays responsive:
tsx
const [query, setQuery] = useState("");
const deferred = useDeferredValue(query);      // list lags; input does not
  • Debounce or throttle high-frequency handlers (input, scroll, resize, mousemove). Use requestAnimationFrame for anything that updates visuals.
  • Add { passive: true } to scroll and touch listeners so the browser does not wait to discover whether you will call preventDefault().
  • Respond immediately, even if the work is not finished: show a pending state on the first frame rather than after the work completes.
  • Honour prefers-reduced-motion — for accessibility, and because it removes work. → Testing/accessibility

#Measure, do not guess

  • DevTools Performance panel with 4–6× CPU throttling. Look for long tasks (> 50 ms), forced synchronous layout, and frames exceeding the budget.
  • React Profiler for component render counts and durations — but confirm against the browser profile, since the cost is often in layout, not in React.
  • Field data (web-vitals) for INP and CLS at p75, segmented by device class. A desktop profile does not represent a mid-range Android phone.
  • Reproduce on a real low-end device before and after. Throttling approximates; hardware is the truth. → Performance/optimization

#Anti-patterns

Anti-patternWhy it failsFix
Animating left, top, width, heightLayout every frametransform
Animating box-shadow or backgroundRepaint every frameopacity, or pre-rendered layers
will-change on many elementsLayer explosion; GPU memoryApply narrowly, remove after
Reading geometry after writing stylesForced synchronous layoutBatch reads, then writes
Polling getBoundingClientRect in a loopLayout per callResizeObserver
Rendering thousands of rowsSlow render and interactionVirtualise
Hiding rendered content with CSSDOM and memory cost remainsDo not render it
Deep DOM treesEvery recalculation is more expensiveFlatten
Long synchronous handlersUnresponsive UI; poor INPYield or use a worker
Non-passive scroll listenersBrowser waits for preventDefault{ passive: true }
Unthrottled high-frequency handlersWork every eventDebounce, throttle, rAF
Waiting to show feedbackFeels broken even when fastImmediate pending state
Ignoring prefers-reduced-motionAccessibility failure and wasted workRespect it
Profiling on a fast machineHides what most users experienceThrottle; test real devices
Trusting the React Profiler aloneThe cost is often layoutBrowser performance profile

#Checklist

  • Verify: Animations use only transform and opacity
  • Verify: will-change is applied narrowly and removed after use
  • Verify: DOM reads and writes are batched; no forced synchronous layout in loops
  • Verify: Geometry is observed with ResizeObserver/IntersectionObserver, not polled
  • Verify: Long lists are virtualised
  • Verify: Off-screen content is not rendered, or uses content-visibility
  • Verify: CSS containment scopes recalculation for independent widgets
  • Verify: The DOM is shallow and no hidden content is rendered unnecessarily
  • Verify: No task on the main thread exceeds 50 ms
  • Verify: Heavy computation runs in a web worker
  • Verify: Non-urgent updates are deferred so input stays responsive
  • Verify: High-frequency handlers are throttled and use requestAnimationFrame
  • Verify: Scroll and touch listeners are passive
  • Verify: Interactions show feedback on the first frame
  • Verify: prefers-reduced-motion is honoured
  • Verify: Profiling is done with CPU throttling and verified on a real low-end device
  • Verify: INP is tracked in the field at p75, segmented by device class