ComponentsGENERALFRAMEWORK-SPECIFICDEVICE-SPECIFIC

What a Component Costs to Render

Framework work and browser work are two different bills. Re-running a component is cheap; mutating the DOM, invalidating style and forcing layout are not — and most components are not your bottleneck.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

When a component re-renders, what does the framework do, what does the browser do, and which of the two is actually costing me?

The user intent

Someone types in a search box and expects the caret to keep up with their fingers. Everything else in this lesson is in service of that.

The obvious build

Re-rendering is expensive, so prevent re-renders. Wrap components in memo, memoize every callback and every derived value, and the app will be fast.

Why it breaks

Memoization is not free. Every memoized value costs a comparison and a retained reference on every render, and a memo around a component whose props change every render adds a check that always fails (Memoization).

How it breaks in a real browser
  • Memoization is not free. Every memoized value costs a comparison and a retained reference on every render, and a memo around a component whose props change every render adds a check that always fails (Memoization).
  • It frequently does nothing. A memo component receiving an inline object or an inline function prop re-renders every time regardless, so the wrapper is pure overhead and the profile is unchanged.
  • It hides the real cost. A component re-running is usually microseconds of framework work; the expensive parts are DOM mutation, style invalidation and forced synchronous layout, and none of them are addressed by memoizing (Layout Thrashing).
  • It misdiagnoses. If typing lags, the cause is often one expensive child, an unvirtualised list, or a synchronous filter over a large array — one specific thing, not a diffuse re-render problem (List Virtualization).
  • It changes what the framework can do. In React, memoization interacts with concurrent rendering and with the compiler; hand-written memoization that is wrong is worse than none, because it produces stale values instead of slow ones.
  • It is framework-specific advice applied universally. In Solid or Svelte the component function does not re-run at all, so the entire premise does not apply (Reactivity Models).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Two bills. Framework work: re-running component functions, diffing or dependency tracking, deciding what changed. Browser work: DOM mutation, style recalculation, layout, paint, composite. They are billed separately and the second one is usually the larger (The Rendering Pipeline).
  • A re-render that produces identical output costs framework work and zero browser work. The framework compares, finds nothing changed, and mutates nothing — so the DOM, style, layout and paint stages all cost nothing (Reconciliation and Keys).
  • A re-render that changes one text node costs framework work plus one DOM mutation plus the browser work that mutation invalidates — which for a text change is style and paint on that node, and layout only if the text changes the box size (The Cost of a Change).
  • Where the framework really matters is list identity. Wrong or missing keys turn a reorder into a teardown and rebuild: every node destroyed and recreated, every subtree remounted, focus lost, scroll lost, and a full paint (Node Identity Across Updates).
  • Reading layout during render is the expensive mistake. offsetHeight, getBoundingClientRect and friends force the browser to compute layout now, synchronously, before returning — and doing that inside a loop is the classic frontend performance bug (Layout Thrashing).
  • Different reactivity models bill differently. Virtual-DOM frameworks re-run components and diff. Signal-based frameworks re-run only the effects that read the changed value, so component granularity barely matters. Compiled frameworks decide much of this at build time. Portable advice is about the browser half (Reactivity Models).
  • And the honest headline: most components are not the bottleneck. The bottleneck is usually one list, one layout read, one enormous subtree, or one thing on the main thread that is not rendering at all (Measure Before Optimising).

What this makes the browser do

And which of it is avoidable.

  • Nothing at all, when reconciliation produces no mutation. This is the case people forget exists, and it is the common case.
  • Style recalculation over the invalidated subtree when attributes or classes change — scoped by the DOM and by the selectors involved, not by your component tree (Style Invalidation).
  • Layout when geometry could have changed: size, position, text content that affects size, insertion or removal of an element (Normal Flow, Overflow and Margin Collapsing).
  • Paint when appearance changes without geometry — colour, shadow, border. Composite only, when the change is limited to transform or opacity on an element that already has its own layer (Cheap and Expensive Animation).
  • Forced synchronous layout whenever script reads a layout property after a write in the same task. This is the one that turns a smooth interaction into a visible stall (Layout Thrashing).
  • Node creation and destruction on remount: allocating elements, attaching listeners, running style for a fresh subtree, and discarding whatever the old one had — including focus and scroll position.

From a state change to a pixel, and where the money goes

The path from "state changed" to "the user sees it" has two halves with a clear seam. Everything up to and including the DOM mutation is the framework's. Everything after it is the browser's, and the browser does not know or care which framework produced the mutation.

This matters because the two halves have different fixes. Framework-half problems are solved by state placement, keys, composition and occasionally memoization. Browser-half problems are solved by changing what you mutate, containing where the invalidation spreads, and not reading layout in the middle of writing it.

One state change, both bills
  1. 1
    State change

    A value the framework is watching is written. Nothing has happened yet; the update is scheduled.

    fails by Writing state during render, producing an update loop that pins the main thread.

  2. 2
    Schedule

    The framework decides when to process. Updates in the same tick usually batch into one pass.

    fails by Code assuming one render per update, which is unspecified and differs across frameworks and origins.

  3. 3
    Re-evaluate

    Component functions re-run (virtual DOM) or only the effects reading the value re-run (signals). Pure framework work.

    fails by An expensive computation inline in the body, re-running on every pass for no reason.

  4. 4
    Reconcile

    Compare the new description with the old one and compute a minimal set of DOM operations. Keys decide identity here.

    fails by Index or unstable keys turning a reorder into a teardown and rebuild, losing focus and scroll (Reconciliation and Keys).

  5. 5
    Mutate the DOM

    Apply the operations. This is the seam: the first moment the browser is involved at all.

    fails by Mutating far more than changed, so the browser invalidates a subtree that did not need it.

  6. 6
    Style

    The browser recomputes style for the invalidated elements and their affected descendants.

    fails by A class toggled high in the tree invalidating everything below it (Style Invalidation).

  7. 7
    Layout

    Geometry recomputed, but only if something that affects geometry changed.

    fails by Script reading offsetHeight mid-update, forcing this to run synchronously and repeatedly (Layout Thrashing).

  8. 8
    Paint and composite

    Paint commands generated for changed regions; the compositor assembles the frame.

    fails by Layer explosion from over-applying will-change, trading main-thread time for GPU memory (Layer Explosion).

Steps one to four are the framework. Steps five to eight are the browser, and it charges for them regardless of which framework asked. Most render-cost bugs that actually reach users are in the second half.

What each change really costs

This is the table to consult before optimising. The honest answer for several rows is maybe, because whether a change triggers layout depends on what else is on the page — the element's containing block, whether it is in flow, whether an ancestor sizes to its content, and whether anything has been contained.

The first row is the one that changes how people think: a re-render that produces identical DOM costs the browser nothing whatsoever. All of that framework work, and the pipeline never starts. That is why "prevent re-renders" is the wrong first question and "what reached the DOM" is the right one.

Component-level changes, priced by pipeline stage
ChangestylelayoutpaintcompositeWhy
Re-render producing identical outputnonononoReconciliation finds no difference and emits no mutation. The cost is framework work only, and it is usually microseconds.
Text content changes, same box sizeyesmaybeyesyesStyle resolves for the affected node and paint redraws it. Layout is skipped only if the new text does not change the box — with intrinsic sizing anywhere above it, it will (Intrinsic Sizing and the Automatic Minimum).
Class toggled that changes colour onlyyesnoyesyesA paint-only property. Style must recompute for the subtree the selector affects, which can be far wider than the element you had in mind.
Class toggled that changes widthyesyesyesyesGeometry changed, so layout runs for the containing block and everything it affects. The full pipeline, every time.
Row inserted into a listyesyesyesyesNew nodes need style and boxes, and siblings after it move. Cost scales with what follows the insertion point, not with the row itself.
List reordered with stable keysmaybeyesmaybeyesThe framework moves existing nodes rather than recreating them. Positions change so layout runs; paint may be reusable if nothing about the nodes changed.
Same reorder with index keysyesyesyesyesThe framework matches by position, so it rewrites the contents of every row instead of moving any. Focus, scroll and per-row state are destroyed as well (Node Identity Across Updates).
Animating `transform` on a composited elementnononoyesHandled by the compositor without re-running style, layout or paint — provided the element already has its own layer and nothing else forces it back (Compositing Layers).
Reading `offsetHeight` after a writeyesyesmaybemaybeForces the browser to compute layout synchronously before the read returns. In a loop this runs once per iteration and is the classic frontend performance bug (Layout Thrashing).

caveat Every maybe here means "it depends what else is on the page" — containment, containing block, intrinsic sizing, existing layers and the specific selectors involved all change the answer. Treat the table as a set of hypotheses to check in a trace, not as a lookup you can quote (The Cost of a Change).

Reading a slow keystroke

SIMULATEDThese proportions are an Engineer Atlas teaching model, not a measurement of any real application. The shape — a small framework band next to much larger data-processing and browser bands — is what transfers; the numbers are relative units and would differ on any specific page and device.

Here is the shape of a lagging search field, drawn in relative units. The framework work is the narrow band on the left, and it is the part that gets optimised because it is the part the framework profiler shows. The wide bands are a synchronous filter over a large array and a style-and-layout pass over several thousand rendered rows.

Memoizing the row component moves the first narrow band and nothing else. Virtualising the list removes most of the last two bands. That is the entire argument for measuring before optimising, in one picture (Measure Before Optimising).

One keystroke in an unvirtualised filtered listrelative units (modelled, not measured)
Input event dispatch
State update + re-render
Synchronous filter over the dataset
Reconcile + DOM mutation
Style recalculation
Layout
Paint + composite
Frame presented
  • Input event dispatchThe browser delivers input. Trivial, and the last cheap thing that happens.
  • State update + re-renderComponent functions re-run. This is the band a framework profiler highlights.
  • Synchronous filter over the datasetNot rendering at all. Plain array work on the main thread, scaling with dataset size.
  • Reconcile + DOM mutationRows added and removed. Cost scales with how many rows actually changed.
  • Style recalculationEvery changed row and its descendants. Scales with rendered node count, not with visible node count.
  • LayoutThe list changed height, so everything after it is repositioned.
  • Paint + compositeOnly the regions that changed, but the whole task has already overrun the frame.
  • Frame presentedThe character finally appears. Several keystrokes may have queued behind this one.

The two widest bands are the filter and the style-plus-layout pass. Neither is addressed by memoizing a component. Virtualising the list attacks both; moving the filter off the main thread attacks the first (Web Workers and the DOM Boundary).

How to build it

Most important first.

  • Measure first, and measure the interaction the user complained about. A profiler recording of one keystroke tells you whether the cost is framework work, DOM mutation, layout or something that is not rendering at all (Measure Before Optimising).
  • Fix the browser half before the framework half. Removing a forced layout read, virtualising a long list, or containing an invalidated subtree usually beats every memoization change combined (CSS Containment).
  • Give lists stable keys derived from identity, never from array index, whenever items can be reordered, inserted or removed (Reconciliation and Keys).
  • Move state down. A component that owns state re-renders itself and its subtree; pushing the state into the smallest component that needs it shrinks the affected region without any memoization at all (Who Owns This State?).
  • Pass content as children so it is created in the caller's scope and is not re-created when the intermediate component re-renders. This is composition doing a performance job for free (Composition and Slots).
  • Apply memoization where you have measured that it helps — a genuinely expensive computation, a stable reference a memoized child depends on, a large subtree with a narrow prop surface. Then re-measure, because it must earn its place (Memoization).
  • Batch DOM reads and writes: read everything, then write everything, so the browser can compute layout once (Layout Thrashing).
  • For very long lists, render only what is visible. This is the single highest-leverage change available in most render-cost problems (List Virtualization).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Remounting destroys focus. If the focused element is torn down and recreated — by a key change, a conditional branch, or a parent remount — focus falls to body and a keyboard user is returned to the top of the document with no announcement (Focus Management).
  • Remounting also destroys scroll position, text selection, aria-expanded state held in the DOM, and any in-progress input method composition. All of these are accessibility regressions, not cosmetic ones.
  • Long tasks caused by render work block assistive technology as thoroughly as they block everyone else: focus moves late, announcements queue, and there is no visual cue that anything is happening (Long Tasks).
  • Virtualisation removes elements from the DOM and therefore from the accessibility tree, which breaks "how many items are there" and can strand focus on a row that gets recycled. It needs aria-setsize, aria-posinset and deliberate focus handling (List Virtualization).
  • A live region that re-renders excessively announces repeatedly. Render cost is directly audible when it touches a live region (Live Regions and Announcement).
  • Respect prefers-reduced-motion in anything you add to smooth a render. A transition added to hide a re-render is motion the user may have asked not to see (Contrast, Colour and Motion).

What can go wrong

Failure modes
  • Index keys with a reorderable list: the framework matches by position, so it mutates every row instead of moving any, and any component state inside a row attaches to the wrong item.
  • A memoized component whose prop is an inline object, so the comparison always fails. The memo runs on every render, does its work, and never once returns early.
  • A stale memo: a dependency list missing an entry, so the value never updates and the bug is wrong output rather than slow output — much harder to spot.
  • A layout read inside a render or a loop, forcing synchronous layout dozens of times in one task (Layout Thrashing).
  • A component that renders ten thousand rows and then optimises the row component. The row was never the problem; the count was (List Virtualization).
  • An unstable key derived from Math.random() or from index-plus-content, producing a full teardown every render and silently destroying focus and scroll.
  • The mitigation failing: memoization everywhere, so every render allocates and compares, the code is harder to read, and the profile is unchanged because the cost was in layout the whole time.
What can arrive out of order
  • In a framework with interruptible rendering, a render can be started, abandoned and restarted with newer state, so a component function may run more than once for one visible update — which is why side effects during render are unsafe.
  • An async update that resolves after a component unmounts writes into something that is gone; frameworks differ on whether that warns, silently drops, or leaks (Cancelling a Request Nobody Is Waiting For).
  • Two state updates in the same tick may batch into one render or produce two, depending on the framework and where they originated. Code that assumes one render per update is relying on unspecified behaviour.
Security
  • Render cost is a side channel in the narrow case where render time depends on secret data — rare in practice, and worth knowing exists.
  • Performance-driven caching of rendered output can outlive an authorization change, so a component memoized on stale permissions renders UI the user should no longer see. The server is still the boundary; this is a correctness bug with a security-shaped symptom (Authorization-Aware UI).
  • A render loop — state written during render, triggering another render — is a self-inflicted denial of service that pins the tab at full CPU and is very easy to ship.
  • Third-party components you do not control render inside your tree and on your main thread; their cost is your interaction latency (Third-Party Scripts and the Supply Chain).
Misreads
  • "Re-render means re-paint." It means the framework re-evaluated a function. If the output is identical, nothing reaches the DOM and the browser does nothing at all.
  • "Memoize everything and you cannot lose." You lose memory, comparison time, readability, and — when a dependency is wrong — correctness. Memoization is a measured decision, not a default (Memoization).
  • "The virtual DOM is fast." It is a strategy for avoiding DOM mutations, faster than naive full re-rendering and slower than a fine-grained system that knows exactly what changed. "Faster" needs a *than what* and a workload (Reactivity Models).
  • "Fewer components means faster." Fewer components means less framework work and identical browser work. The browser half is usually the bigger half (Over-Componentization).
  • "The profiler says this component is the widest bar, so optimise it." Check whether that span included a forced layout caused by something it called. The attribution is often one frame off (Layout Thrashing).
  • "It is fast on my machine." Your machine is near the top of your user population's hardware distribution, and render cost is the most device-sensitive thing in the frontend (Measure Before Optimising).

Measuring it, and what changes in the field

How you would see this
  • A framework profiler recording of one interaction: which components re-evaluated, how long each took, and — the important part — which of them produced a DOM change.
  • The browser Performance panel for the same interaction, where the framework work appears as script and the browser work appears as separate style, layout and paint entries. The relative widths are the answer (A Mental Model of the Devtools).
  • The rendering overlay that flashes repainted regions: if a keystroke repaints the whole page, the problem is invalidation scope, not component count (Debugging Rendering and Jank).
  • Forced-reflow warnings in the performance trace. Each one is a synchronous layout you can usually remove outright (Layout Thrashing).
  • Interaction latency from real users, which is the only measurement that reflects real devices — and is where a render-cost problem is either confirmed or revealed to be a network problem (Interaction Responsiveness).
  • DOM node count over time. A count that grows on every interaction is a retention bug wearing a performance costume (Memory Leaks).
Slow device, slow network, large data, old tab
  • On a slow device, framework work scales with CPU and so does style and layout. A component tree that is comfortable on a laptop can miss frames on a mid-range phone at the same node count (The Real Cost of JavaScript).
  • On a large dataset, everything changes: the cost becomes proportional to item count and virtualisation goes from unnecessary to mandatory (List Virtualization).
  • In a signals-based framework, component-granularity re-render advice is close to irrelevant, while every browser-work item in this lesson applies unchanged (Reactivity Models).
  • During hydration, the framework is doing the most work it will ever do while the page already looks ready, so render cost is at its most visible exactly when the user first tries to interact (Hydration).
  • In a long-lived tab, accumulated nodes and listeners raise the baseline cost of every subsequent render (Long-Lived Clients and Version Skew).
  • With a screen reader or magnifier attached, the accessibility tree is being updated alongside the DOM, so heavy mutation has a cost that no visual profile shows (The Accessibility Tree).
What this costs
  • Memoization trades memory and comparison cost for skipped work, and adds a correctness risk: a wrong dependency produces stale output, which is worse than slow output.
  • Virtualisation trades DOM size for complexity — measurement, scroll restoration, find-in-page stops working, and accessibility metadata you now have to supply by hand (List Virtualization).
  • Moving state down improves render scope and can make the state harder to observe from outside, which is the same trade the state-ownership lesson makes from the other direction (Who Owns This State?).
  • Containment (contain, content-visibility) limits invalidation scope and constrains layout in ways that will surprise you if the contained element needed to size to its content (CSS Containment).
  • All of it costs reader attention. Optimised render code is harder to change, and if the win was inside measurement noise you have paid the maintenance cost for nothing.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALThe browser half — DOM mutation, style invalidation, layout, paint, composite, and forced synchronous layout — is identical across Blink, Gecko and WebKit, and is where the portable advice lives.
  • FRAMEWORK-SPECIFICThe framework half does not transfer. React and Vue re-evaluate at component granularity and reconcile; Solid and Svelte 5 track dependencies at the value and never re-run the component function; Angular offers both zone-based checking and signals depending on configuration. memo-shaped advice is meaningful in the first group and meaningless in the second.
  • DEVICE-SPECIFICWhether any of this is perceptible depends on the device. The same interaction can be imperceptible on a development laptop and clearly laggy on a mid-range phone, and the ratio between them is far larger for main-thread script than for network transfer (Interaction Responsiveness).

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Concurrencyevent-loops
Domains that do not exist yet
  • Programming Languages & Runtime Internals — allocation and garbage collection behind every render: memoization keeps references alive, and retained references are the other half of the memory story.
  • Testing & Reliability Engineering — a performance budget is only enforceable if something fails when it is exceeded, which makes render cost a CI concern rather than a review opinion.