Module: React and Ecosystem
React and Ecosystem·114·9 MIN READ

114: Performance, React Compiler, Transitions, Deferred Values, and Activity

TOPICS COVERED: 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, and useCallback;
  • understand how stable React Compiler changes manual memoization strategy;
  • use useTransition and startTransition;
  • 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:

jsx
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

jsx
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

jsx
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

jsx
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:

text
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

jsx
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:

jsx
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

jsx
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:

jsx
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 memo for 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

  1. Profile 10, 1,000, and 10,000 task rows.
  2. Move search state lower and compare commits.
  3. Add manual memoization only after identifying a measured hotspot.
  4. Compare compiler-enabled and manual-memoization code.
  5. Add useTransition to an expensive filter.
  6. Add useDeferredValue and identify stale UI.
  7. Lazy-load one rare route.
  8. Document whether virtualization is required.

Exit questions

  1. Why can a React app be slow even when render is fast?
  2. What does memo actually skip?
  3. What does React Compiler change about optimization strategy?
  4. What does a transition do?
  5. How is deferred value different from debounce?
  6. Why must interrupted renders be pure?

Official references


Deep dive: performance investigation should follow the critical path

When user says:

text
"typing is slow"

trace:

text
input event
→ state update
→ React render
→ commit
→ browser style/layout/paint
→ any network/effect work

Measure which stage is slow.

React Profiler workflow

  1. Use production-like build if possible.
  2. Record one slow interaction.
  3. Find expensive commit.
  4. Inspect which components rendered.
  5. Ask why each rendered.
  6. Measure component render cost.
  7. change one architecture/optimization;
  8. 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:

text
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:

jsx
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:

text
"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:

js
"use memo";

and:

js
"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:

text
typing
click feedback
controlled input

Non-urgent:

text
expensive result panel update
route content transition

Example:

jsx
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:

jsx
startTransition(() => {
  setInput(event.target.value);
});

Controlled inputs need synchronous urgent updates.

Split:

jsx
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

jsx
const deferredQuery = useDeferredValue(query);

You can detect staleness:

jsx
const stale = query !== deferredQuery;

UI:

jsx
<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:

jsx
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:

text
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:

css
.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:

text
Admin editor 800 KB

is meaningful.

Splitting:

text
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

  1. Profile controlled search and move state lower.
  2. Compare manual memoization with compiler-enabled build.
  3. Add transition to expensive result update.
  4. Add deferred value and stale visual signal.
  5. Measure 10k rows, then paginate/virtualize.
  6. Analyze bundle and lazy-load a truly large route.
  7. Compare React commit time with browser layout time.
  8. 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:

text
typing in search box feels delayed at 5,000 rows

Investigation:

  1. React Profiler shows entire page rerenders.
  2. Browser Performance shows expensive table layout.
  3. Search draft state lives in page root.
  4. 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:

jsx
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:

jsx
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:

text
scheduler priority
blocking work
transition work
component render/effect activity

This bridges the gap between:

text
React Profiler

and:

text
browser main-thread timeline

A useful investigation can now correlate:

text
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:

text
blocking update 40 ms

rather than transition work.

Inspect code and discover expensive result update is in same urgent state setter.

Split:

jsx
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:

text
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.