A component logs "render" three times, but the text on screen changes once. Is React doing unnecessary work, or are you measuring three different things as if they were one?
A function component running is not the same event as a DOM mutation, and neither guarantees an immediate browser paint. React can evaluate a possible UI without committing it. Some render work can be restarted or abandoned before the user ever sees the result.
Understanding that distinction is more useful than memorizing Fiber property names. It tells you where to look when an input lags, an Effect runs unexpectedly, or a profiler shows work that never appeared on screen.
This article describes client rendering in React 18 and 19. The examples use APIs available in React 18, and the current official references reflect React 19.3. React's internal scheduling structures are implementation details, not a public API.
Follow a state update without confusing the stages
Consider a button that calls setCount(count => count + 1). The click does not directly change a paragraph in the DOM. It queues a state update; React determines what to render and later commits the resulting change.
The useful sequence is:
- Trigger: An event or another update source queues work. React can batch updates before processing them.
- Render: React calls components as necessary to calculate the next UI. It reconciles the new elements against the existing tree, identifying which work can be reused and which changes are required.
- Commit: React applies the completed changes to the host environment. For a web root, that usually means DOM operations. A commit can happen even if a particular component's DOM output is unchanged.
- Browser paint: The browser decides when to paint the updated DOM. A React commit and a visible frame are different events.
React's render and commit guide separates the calculation from the DOM update and explicitly notes that React can render without changing any DOM nodes. The browser rendering pipeline is a separate subject, covered in the browser rendering guide.
A render is also not synonymous with an Effect. useLayoutEffect runs during the commit lifecycle before the browser repaints. useEffect generally runs after paint for non-interaction work, but interaction-triggered Effects can run before paint. Neither hook belongs inside the component's render calculation. See the React Effect timing reference.
| Observation | What it proves | What it does not prove |
|---|---|---|
A console.log in the component ran | React invoked the component function | The DOM changed or the frame was painted |
A <Profiler> callback ran | The profiled subtree participated in a commit | Every descendant produced a DOM mutation |
A useLayoutEffect callback ran | The effect reached its layout-effect lifecycle | The browser had already painted |
A useEffect callback ran | A passive Effect was processed | It always happened after a visible paint |
This is why "reduce all renders" is a poor debugging target. First identify which stage actually costs time or produces incorrect behavior.
Where Fiber fits
A JSX element is a description of what you want rendered. A DOM node is a browser object. A Fiber is neither: it is React's internal record for a unit of reconciliation work and the information React needs to maintain that part of its component tree.
In the Fiber architecture, records are connected by relationships such as child, sibling, and return. React can hold a current tree representing the committed UI alongside work-in-progress data for an update. The alternate relationship helps reuse these records instead of rebuilding every object from scratch. These names explain the architecture, but application code must not depend on them.
The benefit is not that React compares two complete DOM copies or magically makes rendering cheap. Fiber gives React an organization for traversing work, retaining component state, tracking changes, and coordinating priorities. React can prepare updates before it commits them.
An important correction to the familiar "one Fiber per frame" story: React does not necessarily yield to the browser after every Fiber, and not every state update is interruptible. Scheduling depends on the kind of update and the available rendering mode. React may process urgent work synchronously; lower-priority concurrent work can be interrupted, restarted, or discarded. A render that has started is not a promise that its output will be committed.
The linked-node model comes from Andrew Clark's Fiber architecture notes. They are useful historical explanations, not a stable specification of today's internal scheduling algorithm. React's startTransition reference documents the practical interruptible-update behavior developers can use.
State identity is part of reconciliation
Matching work is about identity, not only checking whether a text string changed. React associates state with a component's place in the rendered tree, including its type and key.
If you change <Editor key={documentId} /> from one document ID to another, you are deliberately telling React to treat it as a different component instance. Its local state resets, and React may replace DOM nodes. Keeping the same type and key at the same position generally preserves that state.
This is often a better explanation for an unexpectedly cleared form than "Fiber rendered twice." The state preservation guide walks through the exact role of tree position and keys.
A small trace you can inspect
The following is an illustrative React 18+ client component, intended to be placed in App.jsx in a small React application. It is not a benchmark, and its exact scheduling or log order is not guaranteed. React and React DevTools versions, development mode, Strict Mode, and the amount of work all affect what you observe.
import { Profiler, startTransition, useEffect, useLayoutEffect, useState } from 'react';
const items = Array.from({ length: 1000 }, (_, index) => 'Item ' + index);
function SearchPanel() {
const [input, setInput] = useState('');
const [query, setQuery] = useState('');
console.log('render', { input, query });
useLayoutEffect(() => {
console.log('layout effect', query);
}, [query]);
useEffect(() => {
console.log('passive effect', query);
}, [query]);
const matches = items.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
function handleChange(event) {
const next = event.target.value;
setInput(next);
startTransition(() => {
setQuery(next);
});
}
return (
<section>
<label htmlFor="filter">Filter items</label>
<input id="filter" value={input} onChange={handleChange} />
<p>{matches.length} matches</p>
<ul>
{matches.slice(0, 30).map(item => <li key={item}>{item}</li>)}
</ul>
</section>
);
}
export default function App() {
function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
console.log('Profiler commit', {
id, phase, actualDuration, baseDuration, startTime, commitTime
});
}
return (
<Profiler id="search-panel" onRender={onRender}>
<SearchPanel />
</Profiler>
);
}
The controlled input gets an urgent update. The result query is scheduled as a Transition, so React can prioritize input feedback over the result list. It does not make the filtering calculation itself faster. If you move expensive synchronous work into the input event handler, a Transition cannot interrupt that JavaScript function.
The sample limits visible list items to 30 and uses a lightweight filter. It is designed to help you inspect the update stages, not to force a slow render or demonstrate an actual interruption. The React Transition documentation also warns that Transition updates cannot control text inputs, which is why input and query are separate state variables.
How to read the trace
Run the component inside a client-rendered React app, open the console, and type into the field. For a clearer baseline, compare a development run with and without StrictMode. You can compare behavior with an ordinary production build, but the <Profiler> callback does not collect timings there by default. Production timing measurements require a profiling-enabled React build.
- A render log means React called
SearchPanel. That call may belong to work that is subsequently restarted or never committed. - A Profiler commit log corresponds to a commit involving the profiled subtree. The
actualDurationis render time measured for that update, not total browser paint time.commitTimeis a timestamp, not the duration of the commit. - A layout effect log occurs in the layout-effect lifecycle associated with committed output. A passive Effect is scheduled separately, with timing that depends on the interaction and React's work.
- Multiple logs are not proof of redundant DOM mutations. To diagnose actual DOM changes, inspect the Elements panel or record a Chrome Performance trace in addition to React DevTools.
The public Profiler API specifies the callback fields and its production-build limitation. In React 19.2 or later, React Performance Tracks add scheduler, component, commit, and remaining-Effects information to compatible Chrome DevTools traces. These tracks were introduced in 19.2 and are not available in React 18. In React 18, use the React DevTools Profiler and the browser's Performance panel instead.
When an extra render is a real problem
Use the symptom to choose the next measurement.
| Symptom | First check | Likely next step |
|---|---|---|
| Console prints twice on mount in development | Is StrictMode enabled? | Check for impure rendering or missing Effect cleanup rather than suppressing the check |
| A field loses its value after switching records | Did its key, type, or tree position change? | Decide whether the component should preserve or reset its state |
| Typing feels delayed | Is expensive work happening in the event handler, render, commit, or layout? | Profile first; separate urgent input updates from non-urgent result updates when appropriate |
| Many components render but the DOM barely changes | Which subtree consumed render time? | Use the Profiler; consider state placement or memoization only for measured costs |
| An Effect appears to run "twice" | Is it remounting, changing dependencies, or running an extra Strict Mode setup/cleanup cycle? | Fix dependencies and cleanup; avoid treating Effects as render callbacks |
React's Strict Mode documentation describes additional development-only component calls and Effect setup/cleanup checks. Removing Strict Mode merely to silence logs can conceal the bug it is intended to expose.
There are also two important limits to what Fiber can solve:
An urgent update is not necessarily interruptible. Putting everything inside startTransition is not the answer, particularly for controlled inputs. The scheduler chooses when yielding is appropriate, and a single long-running JavaScript calculation still blocks the main thread.
A fast render does not imply a fast frame. Layout, style calculation, painting, network activity, and third-party scripts can dominate a user-visible delay. When the React Profiler looks healthy but interaction latency remains poor, widen the investigation to the browser trace.
The model to keep
The most reliable way to reason about a React update is to keep four questions separate: What triggered work? What did React calculate? What did React commit? When did the browser display it?
Fiber is the internal machinery that helps React organize and schedule that work. It is worth understanding because it explains why a component function may execute without a corresponding DOM change, why state identity matters, and why a non-urgent render can be replaced before commit. You rarely need to inspect a Fiber object to debug any of those outcomes.
If the problem involves server HTML disagreeing with the first client render, that is a different boundary. The hydration mismatch guide addresses that failure rather than treating it as an ordinary client-side re-render.
