114: Performance, React Compiler, Transitions, Deferred Values, and Activity
Learning objectives
You will learn to:
- separate render cost from DOM/layout/network cost;
- profile before optimizing;
- understand
memo,useMemo, anduseCallback; - understand how stable React Compiler changes manual memoization strategy;
- use
useTransitionandstartTransition; - use
useDeferredValue; - reason about interruptible rendering;
- use Activity where preserving hidden state is valuable;
- reduce bundle and rendering cost;
- avoid optimization folklore.
Performance is a category problem
A “slow React app” may actually be slow because of:
- network latency;
- oversized JavaScript bundle;
- image decoding;
- layout thrashing;
- expensive render calculation;
- too many DOM nodes;
- a state update high in the tree;
- repeated parsing;
- memory pressure;
- unnecessary Effects.
A memoization Hook only helps some render-cost problems.
Measure first
Use:
- React DevTools Profiler;
- browser Performance panel;
- Network panel;
- production build;
- realistic device/network throttling.
Record:
- interaction;
- commit duration;
- expensive component;
- browser long tasks;
- layout/paint cost;
- transferred bytes.
Optimization without a baseline becomes guesswork.
State locality
Often the best React performance optimization is architecture.
Instead of:
function App() {
const [draft, setDraft] =
useState('');
return (
<>
<HugeDashboard />
<SearchInput
draft={draft}
setDraft={
setDraft
}
/>
</>
);
}
move draft state down if only the search feature needs it.
Now typing does not require the highest app component to recalculate everything.
memo
const TaskRow =
memo(
function TaskRow({
task,
onToggle,
}) {
return (
<li>
<button
onClick={() =>
onToggle(
task.id,
)
}
>
{task.title}
</button>
</li>
);
},
);
memo may skip rendering when props compare equal.
It is an optimization, not a correctness feature.
If task or onToggle gets a new identity each time, memoization may not skip anything.
useMemo
const visibleTasks =
useMemo(
() =>
expensiveFilter(
tasks,
query,
),
[tasks, query],
);
Use when the calculation is expensive enough that caching measurably helps or stable identity is required for another optimized boundary.
Do not use it around trivial arithmetic.
useCallback
const handleToggle =
useCallback(
(id) => {
dispatch({
type: 'toggled',
id,
});
},
[dispatch],
);
This caches function identity.
It does not make the function body faster.
Use it when stable identity matters to an optimized child or dependency boundary.
React Compiler
React Compiler is stable and automatically optimizes component/value reuse at build time.
This changes the recommended mindset:
purity first state design second profile compiler where enabled manual memoization when justified
Do not teach:
every callback should use useCallback
or:
every calculation should use useMemo
as default React style.
Compiler effectiveness depends on code following React's rules and purity expectations.
Code splitting
const Reports =
lazy(
() =>
import(
'./Reports.jsx'
),
);
Wrap with Suspense.
Route-level splitting is often higher-value than splitting every small component.
Inspect the production bundle; do not assume code splitting reduced total work if shared dependencies still dominate.
List virtualization
Rendering 50,000 rows creates DOM and layout cost.
Virtualization renders only the visible window.
Use a mature virtualization library for large data sets rather than inventing scrolling math casually.
Test:
- keyboard navigation;
- focus;
- dynamic row heights;
- screen reader behavior;
- scroll restoration.
useTransition
Mark non-urgent UI updates:
function Search({
tasks,
}) {
const [
input,
setInput,
] =
useState('');
const [
query,
setQuery,
] =
useState('');
const [
isPending,
startTransition,
] =
useTransition();
function change(
event,
) {
const value =
event.target.value;
setInput(value);
startTransition(
() => {
setQuery(value);
},
);
}
const visible =
tasks.filter(
(task) =>
task.title
.toLowerCase()
.includes(
query
.toLowerCase(),
),
);
return (
<section
aria-busy={
isPending
}
>
<input
value={input}
onChange={
change
}
/>
{isPending && (
<p role="status">
Updating results…
</p>
)}
<TaskList
tasks={visible}
/>
</section>
);
}
Typing stays urgent.
Updating a large result view can be non-urgent.
Transitions are not debounce
A transition:
- changes React update priority;
- can be interrupted;
- can expose pending state.
It does not:
- wait 300ms;
- cancel a network request;
- cache results;
- authorize server work.
Use debouncing when you specifically need time-based input delay.
Use AbortSignal/query cancellation for request cancellation.
useDeferredValue
const deferredQuery =
useDeferredValue(
query,
);
The source value updates immediately, while a consumer can temporarily lag.
This is useful when:
- input must remain responsive;
- rendering the dependent view is expensive.
Do not use deferred data for:
- submitted value;
- permissions;
- payment totals;
- destructive decisions.
Interruptible rendering
React can start rendering non-urgent work and abandon it before commit.
Therefore render must remain pure.
Wrong:
function Results() {
analytics.track(
'render results',
);
...
}
An interrupted render could track work that never reached the screen.
Analytics belongs in an event or appropriate Effect.
Activity
React 19.2 Activity can preserve hidden subtree state.
It is useful for expensive tabs/workspaces where returning should restore state.
Trade-off:
- hidden trees can retain memory;
- preserved state may be undesirable for sensitive/reset-required workflows.
Choose intentionally.
Browser performance
React profiling is only one layer.
Also inspect:
- layout shifts;
- forced synchronous layout;
- large images;
- font loading;
- unused JavaScript;
- third-party scripts;
- main-thread long tasks;
- request waterfalls.
Common mistakes
- memoization before measurement;
- using
memofor impure components; - unstable props defeating memoization;
- useMemo around trivial values;
- transition used as network cancellation;
- giant DOM rendered then “optimized” with callbacks;
- using development performance as production evidence;
- assuming React Compiler excuses impure code.
Exercises
- Profile 10, 1,000, and 10,000 task rows.
- Move search state lower and compare commits.
- Add manual memoization only after identifying a measured hotspot.
- Compare compiler-enabled and manual-memoization code.
- Add
useTransitionto an expensive filter. - Add
useDeferredValueand identify stale UI. - Lazy-load one rare route.
- Document whether virtualization is required.
Exit questions
- Why can a React app be slow even when render is fast?
- What does
memoactually skip? - What does React Compiler change about optimization strategy?
- What does a transition do?
- How is deferred value different from debounce?
- Why must interrupted renders be pure?
Official references
- https://react.dev/reference/react/memo
- https://react.dev/reference/react/useMemo
- https://react.dev/reference/react/useCallback
- https://react.dev/reference/react/useTransition
- https://react.dev/reference/react/useDeferredValue
- https://react.dev/learn/react-compiler
- https://react.dev/reference/react/Activity
Deep dive: performance investigation should follow the critical path
When user says:
"typing is slow"
trace:
input event → state update → React render → commit → browser style/layout/paint → any network/effect work
Measure which stage is slow.
React Profiler workflow
- Use production-like build if possible.
- Record one slow interaction.
- Find expensive commit.
- Inspect which components rendered.
- Ask why each rendered.
- Measure component render cost.
- change one architecture/optimization;
- record again.
Do not optimize from component count alone.
Parent rerender versus child render cost
A parent rerender usually calls child functions unless optimization/compiler skips work.
But if child rendering is cheap, this may not matter.
Common high-value fix:
move frequently changing state closer to consumer
rather than memoizing every descendant.
Memo boundary trade-offs
memo adds:
- prop comparison;
- cognitive complexity;
- potential stale custom comparator bugs.
A custom comparator:
memo(Component, areEqual)
must compare every prop affecting output.
Ignoring a function prop can produce stale closures.
Only use custom comparison when profiling justifies it and you can prove correctness.
React Compiler deeper model
Compiler analyzes component/Hook code and can insert memoization-like optimizations.
It depends on Rules of React:
- purity;
- Hooks rules;
- no unsupported mutations/patterns.
Do not treat compiler as:
"React makes anything fast automatically"
It reduces manual memoization burden for compatible code.
Library/application build setup must actually enable the compiler.
Verify configuration rather than assuming React 19 means compiler is running.
Compiler directives
React Compiler supports directives such as:
"use memo";
and:
"use no memo";
in relevant scenarios/configurations.
These are advanced tools.
Do not sprinkle them throughout code without understanding compiler diagnostics.
ESLint compiler rules
Current React Hooks lint ecosystem includes rules that help preserve compiler-compatible/pure code.
Treat lint errors as architecture feedback, not obstacles to disable.
Transition scheduling
Urgent:
typing click feedback controlled input
Non-urgent:
expensive result panel update route content transition
Example:
const [tab, setTab] = useState('summary');
const [isPending, startTransition] = useTransition();
function selectTab(next) {
startTransition(() => {
setTab(next);
});
}
The transition is interruptible.
If user chooses another tab before previous finishes, React can abandon old work.
Transition caveat: controlled input state
Do not put controlled input's own value update in transition:
startTransition(() => {
setInput(event.target.value);
});
Controlled inputs need synchronous urgent updates.
Split:
setInput(value);
startTransition(() => {
setQuery(value);
});
Async transitions caveat
When using asynchronous work around transitions, understand current React behavior and pending boundaries carefully.
React Actions can coordinate async transition workflows.
Do not assume any arbitrary await preserves all transition context semantics without following current API patterns.
Deferred values
const deferredQuery = useDeferredValue(query);
You can detect staleness:
const stale = query !== deferredQuery;
UI:
<div style={{ opacity: stale ? 0.6 : 1 }}>
<Results query={deferredQuery} />
</div>
This communicates old results remain visible.
Do not present stale sensitive values as if current.
Deferred value and network
If Results uses query key based on deferred query, network starts when deferred value changes.
But deferred value is not a classic fixed-time debounce.
React chooses scheduling based on rendering pressure.
If server search requires "wait 300 ms after typing to reduce requests," use debouncing in addition to query cancellation/cache where appropriate.
Suspense + transition
Navigation:
startTransition(() => {
setPage(nextPage);
});
If next page suspends, React can keep current revealed content instead of immediately showing fallback, depending on boundary.
This improves continuity.
Use isPending to show subtle navigation feedback.
CPU work
React scheduling cannot rescue a 500 ms synchronous loop once it is executing.
If computation is huge:
- optimize algorithm;
- precompute;
- move server-side;
- Web Worker;
- incremental processing;
- virtualization.
Do not wrap CPU-heavy function in useMemo and assume first computation disappears.
Web Workers
For large client computation:
parse massive file image processing complex simulation
worker can keep main thread responsive.
Communication cost/serialization matters.
React state receives results; React should not own worker internals as render state.
Browser layout performance
React commit can be fast while browser layout is slow.
Common:
- thousands of DOM nodes;
- expensive CSS selectors/effects;
- layout reads/writes;
- huge images;
- sticky/fixed complexity.
Use Performance panel, not only React Profiler.
content-visibility and CSS
CSS module taught browser rendering optimizations.
React performance should reuse them:
.long-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
where appropriate.
Not every performance problem needs JS.
Virtualization complexity
Virtual list can break:
- browser find-in-page expectations;
- screen-reader access;
- focus when item unmounts;
- variable height;
- print;
- SEO/server output.
Use pagination or simpler rendering if it meets product need.
Bundle analysis
Measure:
- initial JS;
- route chunks;
- duplicate dependencies;
- large libraries;
- locale packs;
- icons;
- editor/chart packages.
Code splitting:
Admin editor 800 KB
is meaningful.
Splitting:
2 KB button
usually is not.
Hydration performance
SSR can improve initial content but hydration still costs JS execution.
RSC can reduce client JS by keeping non-interactive components server-side.
Architecture affects performance more than memoization.
Memory
Hidden Activity, caches, large state, image blobs, event listeners, and detached DOM can retain memory.
Use browser Memory tools when long-running apps degrade.
Performance is not only speed; memory pressure affects responsiveness.
Production metrics
Track user-centric metrics:
- INP;
- LCP;
- CLS;
- custom business interactions.
Connect frontend traces with backend request timing when possible.
A fast React render cannot fix 2-second API latency.
Failure clinic
useMemo everywhere
More code, little benefit.
transition around network fetch but no cache/cancellation
Scheduling doesn't manage transport.
profiler in dev only
Dev behavior distorts timing.
memoized child receives new object/function every time
Optimization ineffective.
compiler assumed enabled
Build not configured.
Exercises
- Profile controlled search and move state lower.
- Compare manual memoization with compiler-enabled build.
- Add transition to expensive result update.
- Add deferred value and stale visual signal.
- Measure 10k rows, then paginate/virtualize.
- Analyze bundle and lazy-load a truly large route.
- Compare React commit time with browser layout time.
- Record one Web Vital/business interaction metric.
Mastery check
Explain:
- profiler workflow;
- React Compiler's role;
- urgent versus transition updates;
- deferred versus debounce;
- CPU/main-thread limitations;
- browser/React performance layers;
- why architecture often beats manual memoization.
Production case study: diagnosing a slow searchable table
Symptoms:
typing in search box feels delayed at 5,000 rows
Investigation:
- React Profiler shows entire page rerenders.
- Browser Performance shows expensive table layout.
- Search draft state lives in page root.
- Every keystroke filters 5,000 records and renders all rows.
Fix in layers:
Ownership
Move draft to Search component if other page regions do not need every keystroke.
Scheduling
Use committed/deferred search term:
const deferredQuery = useDeferredValue(query);
Algorithm
Pre-normalize searchable text if expensive.
DOM
Paginate or virtualize rows.
Server
For very large datasets, move search/pagination to API.
Memo/compiler
Only after architecture, profile whether component memoization adds value.
This progression is more reliable than starting with:
useCallback useMemo memo
on every function/component.
Additional depth: React 19.2 Performance Tracks
React 19.2 adds React-specific tracks to Chrome DevTools Performance profiles.
These expose information such as:
scheduler priority blocking work transition work component render/effect activity
This bridges the gap between:
React Profiler
and:
browser main-thread timeline
A useful investigation can now correlate:
user input → blocking React update → transition update → component work → browser paint
Use current Chrome/React DevTools versions that support the tracks.
Example diagnosis
You type in search.
Performance track shows:
blocking update 40 ms
rather than transition work.
Inspect code and discover expensive result update is in same urgent state setter.
Split:
setInput(value);
startTransition(() => {
setFilter(value);
});
Record again.
The lesson is evidence-driven scheduling, not adding transitions everywhere.
Performance budget
For a production feature, define an interaction target before optimization:
search keystroke remains responsive route transition feedback < perceived delay threshold table scroll no sustained long tasks
Exact thresholds depend on device/product, but a budget creates a measurable goal.
