Home / Articles / React 19.2 SSR Primitives: Activity, cacheSignal, and PPR Explained

This article is published in English.

React 19.2 SSR Primitives: Activity, cacheSignal, and PPR Explained

Learn how React 19.2's new Activity component, cacheSignal, and Partial Pre-rendering give developers direct control over server rendering performance.

2240 words

React performance work tends to fall into two buckets: making the initial server render faster and leaner, and making the client-side app avoid wasted rendering once it's mounted. The two halves of this article attack the problem from opposite ends of that spectrum, but they share the same underlying philosophy — that speed comes from telling React precisely what work matters, what can be deferred, and what should never run at all, rather than from throwing more caching or more hardware at the problem. The first half covers the server-rendering side and the new primitives that shipped in React 19.2; the second turns to the everyday client-side techniques that keep a mounted app responsive.

Rethinking Server Rendering Performance in React 19.2

Most guidance on "SSR performance" boils down to piling on caching layers and hoping for the best. React 19.2, released in October 2025, instead hands you dedicated primitives for controlling server work directly. Treating this release as a minor patch means missing out on real speed gains — the details below are what actually move the needle.

Keep components alive instead of tearing them down

A recurring problem in SSR apps is that tab switches, modal opens, and route transitions destroy components outright, wiping their state and forcing data to be fetched all over again. The new Activity component is built specifically to solve this.

// old way — full unmount, state gone, effects re-run
{activeTab === 'analytics' && <AnalyticsPanel />}

// React 19.2 — stays alive, just deprioritized
<Activity mode={activeTab === 'analytics' ? 'visible' : 'hidden'}>
  <AnalyticsPanel />
</Activity>

By keeping hidden content mounted instead of destroying it, React can preload what's inside a hidden Activity block before a user ever clicks into it. This approach is described as meaningfully cutting perceived navigation latency on dashboards with heavy SSR, since there's no refetching and no layout jump when the content becomes visible.

Let cacheSignal clean up abandoned work automatically

Before 19.2, if a server render was abandoned partway through — say, a client navigated away or a request timed out — any in-flight fetches and cached data had no way of knowing that the work was no longer needed. cacheSignal fixes this by giving React Server Components a real lifecycle signal to hook into for cleanup.

async function getUserOrders(userId, { signal }) {
  const res = await fetch(`/api/orders/${userId}`, { signal });
  return res.json();
}

Once the cache's lifetime runs out, the associated signal fires an abort, which stops orphaned requests from continuing to consume CPU on the server during traffic spikes.

Build a static shell and stream the rest

Partial Pre-rendering (PPR) is the headline SSR feature in this release. The idea is to build the static shell of a page — navigation, layout, footer — once, serve it straight from an edge CDN, and stream in the dynamic pieces behind Suspense boundaries.

<Suspense fallback={<ProductSkeleton />}>
  <PartialPreRender>
    <PersonalizedRecommendations userId={user.id} />
  </PartialPreRender>
</Suspense>

Batch multiple Suspense reveals together

Previously, when several Suspense boundaries happened to resolve at roughly the same moment, the UI could suffer from a "popcorn" effect, with fragments of content popping in one after another instead of together. React 19.2 batches these reveals so client and server behavior stays consistent, and it adds support for Web Streams in Node.js for teams that need finer-grained control over streaming.

These APIs raise the ceiling, not the floor

None of this replaces the basics. You still need to eliminate N+1 data-fetching patterns and break up monolithic bundles before these features can help you at all. React 19.2 doesn't lower the minimum amount of optimization work required — it raises the upper limit on how fast a well-optimized app can go. It's also worth reading the official React 19.2 release notes, along with the Turbopack defaults introduced in Next.js 16, which pair well with PPR.

As of 2026, SSR performance isn't about caching more aggressively — it's about telling React what can wait, what can stream, and what can fail gracefully. React 19.2 is what finally gives you the vocabulary to express that.

Beyond these SSR-specific mechanisms, a lot of what makes a React app feel fast comes down to everyday habits that hold regardless of rendering strategy or React version. Where the previous section focused on streaming, prerendering and Suspense at the server boundary, what follows covers the client-side patterns that keep any React app responsive in practice.

Practical Techniques for Everyday React Performance

React already renders efficiently by default. It's as an application grows that unnecessary re-renders, oversized lists, excess JavaScript, and too many network calls start to add up. The fix rarely involves exotic tricks — a handful of disciplined habits usually gets you most of the way there.

Render Only What Actually Needs to Update

Every re-render causes React to run a component's function body again. That's not inherently a problem — the real cost is repeating expensive work when nothing meaningful has actually changed. Avoid tucking unrelated state inside a component that renders a large chunk of your UI, since updating that state then forces the whole subtree to re-render along with it. Split components instead so an update only touches the part of the UI it's actually meant to affect. The objective isn't zero re-renders — it's eliminating the wasted ones.

Place State Where It's Actually Needed

Resist lifting every piece of state up to the top of your component tree. If only one component reads a given value, that's exactly where it belongs.

function SearchBox() {
  const [query, setQuery] = useState("");

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
    />
  );
}

Keeping state local like this limits how far an update can ripple, which cuts down on incidental re-renders elsewhere in the tree. As a rule of thumb, place state near the component that consumes it rather than higher up the hierarchy.

Derive Values Instead of Storing Them

Not everything belongs in useState. If you already track firstName and lastName, there's no reason to also store fullName separately. The wasteful version looks like this:

const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

A simpler approach just computes the value while rendering:

const fullName = `${firstName} ${lastName}`;

This removes both an extra state variable and an unnecessary Effect. In general, if a value can be computed on the fly during render, it probably doesn't need to be state.

Handle Large Lists Without Overloading the DOM

Rendering thousands of nodes at once gets expensive quickly. Picture a chat screen holding 10,000 messages — there's no need for all of them to exist in the DOM simultaneously. For large collections, reach for one of:

  • Virtualization
  • Pagination
  • Infinite scroll

Virtualization keeps only the rows currently in view, plus a small buffer, mounted at any given time; libraries such as react-window implement this pattern for you. That said, don't apply virtualization reflexively — a list of 50 items almost certainly doesn't need it.

Defer Loading Code Until It's Needed

Users shouldn't have to download JavaScript for features they haven't opened yet. React's lazy and Suspense APIs let you split that code out of the initial bundle:

const Settings = lazy(() => import("./Settings"));

With this in place, the Settings component is fetched only once it's actually rendered, rather than shipping with the initial page load. This is worthwhile for heavy or infrequently used features — charts, editors, maps, settings screens, large dashboards — and generally results in a faster initial load.

Keep Interactive UI Responsive Under Load

Not every update needs to happen in lockstep with user input. A search box should react to keystrokes instantly, even while filtering a large dataset runs at a lower priority in the background. React's useTransition and useDeferredValue hooks are designed for exactly this kind of trade-off. Debouncing raw input offers a similar effect:

const debouncedSearch = useDebounce(search, 500);

Instead of firing a request on every keystroke, you wait until the user pauses. These patterns are useful for search fields, filters, long lists, and dashboards with a lot of moving parts.

Cut Down on Redundant Network Requests

Rendering speed is only part of the picture — too many outstanding requests can make an app feel slow even when rendering itself is fast. Depending on the situation, consider:

  • Caching responses
  • Deduplicating requests
  • Paginating results
  • Debouncing search input
  • Canceling requests that are no longer relevant

If a user quickly types

react
react performance
react performance optimization

you probably don't want three separate requests competing at once. The underlying principle stays simple: avoid making the network perform work you don't actually need.

Use Memoization Selectively

React ships three common memoization primitives:

  • useMemo caches the result of a computation.
  • useCallback caches a function reference across renders.
  • React.memo can skip re-rendering a component when its props haven't changed.

A typical example:

const filteredUsers = useMemo(() => {
  return users.filter(user =>
    user.name.includes(search)
  );
}, [users, search]);

But memoization isn't free — it has its own overhead and adds complexity to the surrounding code. Reach for it when a computation is genuinely expensive, when a component keeps re-rendering without reason, or when a stable reference actually matters further down the tree. Don't spend effort optimizing code that isn't causing a measurable problem.

Let the Compiler Take Over Some of the Work

A newer addition to the React toolchain, the React Compiler, can automatically apply many of these optimizations on your behalf — memoizing values, functions, and components without you writing it by hand in every case. That means you no longer need to default to reaching for:

useMemo(...)
useCallback(...)
React.memo(...)

That said, React Compiler doesn't eliminate the value of asking whether a performance issue actually exists first. The right sequence is still to confirm there's a real problem, let the compiler apply the optimizations it's capable of, and fall back to manual memoization only when you have a concrete reason.

Give React Stable Identities to Work With

Keys tell React which item in a list is which across renders. Favor deriving the key from a stable, unique property of the data rather than from its position:

items.map(item => (
  <Item key={item.id} />
));

Avoid using the array index as the key, since reordering, inserting, or removing items shifts every index below the change point:

items.map((item, index) => (
  <Item key={index} />
));

A stable key lets React correctly detect which entries were added, removed, or updated instead of guessing based on position. The same principle carries over to objects and functions you pass as props: creating a brand-new object or callback on every render defeats the purpose of a memoized child component, since its props will look different each time even though nothing meaningful changed.

Locate the Actual Bottleneck Before Acting

Once you're comfortable with these techniques, don't rely on intuition to decide what to fix. Open the React DevTools Profiler to see exactly which components render and how long each one takes. For issues that go beyond React itself, the browser's Performance panel can reveal long tasks, slow script execution, expensive layout work, or rendering bottlenecks. Rather than assuming "this component feels slow," use these tools to pin down why it's slow.

Confirm the Fix Actually Helped

After applying an optimization, re-measure instead of assuming it worked. Check whether render time actually dropped, whether the bundle got smaller, and whether interactions feel more responsive. If none of these improved, the change may not have been necessary in the first place.

A Checklist to Run Before Shipping

Before releasing a React application, review whether you're rendering UI you don't need, whether state lives in the right place, whether you're storing values that could be calculated instead, whether large lists are handled efficiently, whether heavy code loads only when required, whether search and interactions stay responsive, whether you're issuing unnecessary API calls, whether memoization is solving a real problem, whether React Compiler could take over an optimization, whether your keys are stable, whether you measured the real bottleneck, and whether you verified the improvement afterward.

Ultimately, the best optimization isn't the one that adds the most code — it's the one that gets React to do less unnecessary work.