Home / Articles / Partial Pre-rendering and Concurrent Rendering Explained

This article is published in English.

Partial Pre-rendering and Concurrent Rendering Explained

Learn how Next.js Partial Pre-rendering and React Concurrent Rendering both fix slow apps by letting frameworks schedule and stream work instead of treating renders as one blocking unit.

1346 words

Modern React and Next.js performance work keeps circling back to the same insight: forcing an entire page or an entire render to behave as one single, uninterruptible unit is what makes apps feel slow. Two techniques address this from different angles — Partial Pre-rendering, which lets a single Next.js route mix static and dynamic content, and Concurrent Rendering, which lets React interrupt and reorder work inside the browser. Together they show a pattern worth understanding: performance gains increasingly come not from writing "faster" code, but from letting the framework schedule and stream work more intelligently.

The old all-or-nothing rendering choice

For a long time, a Next.js page had exactly two rendering modes to pick from:

  • Static Generation (SSG) — pages are fast to serve because they're built ahead of time, but the content goes stale until the next rebuild.
  • Server-Side Rendering (SSR) — content is always current, but every request has to wait for the slowest piece of data before anything is sent back.

The problem is that most real pages don't fit neatly into either bucket. A product page, for instance, is mostly static — the layout, navigation, and marketing copy don't change per request — but it also contains a handful of genuinely dynamic elements, like a cart badge or a live stock counter. Rendering the whole page through SSR just to keep one small widget fresh means paying the full server-rendering cost for content that didn't need it.

Letting one route be both static and dynamic

Partial Pre-rendering (PPR) exists to close that gap. It allows a single route to ship a static shell immediately, while the dynamic fragments inside it are streamed in as soon as their data resolves — no separate pages, and no juggling between getStaticProps and getServerSideProps.

// app/product/[id]/page.tsx
import { Suspense } from 'react';
import ProductShell from '@/components/ProductShell';
import LiveInventory from '@/components/LiveInventory';

export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <ProductShell productId={params.id}>
      {/* static instantly, no waiting on the network */}
      <Suspense fallback={<InventorySkeleton />}>
        {/* streamed in once the dynamic data resolves */}
        <LiveInventory productId={params.id} />
      </Suspense>
    </ProductShell>
  );
}

The mechanism is a Suspense boundary. Anything placed outside it gets pre-rendered at build time and served instantly; anything wrapped inside it is computed and streamed at request time. That's the entire mental model: one file, one route, two rendering strategies coexisting.

This split produces several concrete wins. Time to First Byte improves because the static shell comes straight from the edge instead of being computed for every request. There's no need for a full-page loading state either — visitors see the meaningful, static parts of the page right away, with the smaller dynamic pieces filling in as they finish. And the mental overhead is lower than maintaining separate static and server-rendered pages, since you're working with one route and one file that simply mixes strategies. The Next.js team describes the goal as delivering the benefits of both static and dynamic rendering without the usual architectural trade-offs.

A few practical guidelines make PPR effective in production. Keep the Suspense boundaries tight around only the parts that are truly dynamic — wrapping too much content defeats the purpose of pre-rendering anything at all. Keep fallback skeletons lightweight, since those fallbacks are themselves part of the statically rendered shell. If you're using TypeScript, define clear prop contracts between the static shell and the streamed-in components so the two pieces stay in sync as they evolve. And when testing, throttle your connection to something like slow 3G in your browser's dev tools — PPR's advantages become much clearer under realistic network conditions than on a fast local connection.

If your team is running Next.js 14 or later, it's worth trying PPR on a single route before adopting it across the whole application. It isn't a marketing term; it's a rendering strategy that finally matches how real pages actually behave — partly static, partly live, at the same time.

Extending the same idea to client-side rendering

Partial Pre-rendering solves the static-versus-dynamic split at the server and network level. Concurrent Rendering, introduced with React 18 and refined further in React 19, solves an analogous problem inside the browser: instead of choosing between "render everything now" and "render nothing yet," React can now render some things immediately and let others wait.

Before React 18, rendering was synchronous and blocking. A single state update triggered a re-render that ran to completion no matter what — even if it meant freezing scrolling or input while React worked through the whole tree.

Concurrent Rendering changes that behavior. React can now pause a render partway through, prioritize urgent updates such as typing or clicking over non-urgent ones such as filtering a long list, and discard in-progress work if a newer update makes it irrelevant. Importantly, this isn't a new API you have to learn from scratch — it's a different scheduling model operating underneath the hooks you already use.

The hooks that expose concurrent scheduling

useTransition lets you mark a state update as non-urgent, so React can keep the interface responsive while that update is processed in the background.

function ProductSearch() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleChange(e) {
    const value = e.target.value;
    setQuery(value); // urgent — keep input snappy

    startTransition(() => {
      // non-urgent — can be interrupted
      setResults(filterProducts(value));
    });
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <span className="text-gray-400">Updating…</span>}
      <ResultsList items={results} />
    </>
  );
}

With this pattern, typing into a search box stays smooth even while filtering through thousands of items behind the scenes.

Suspense plays a similar streaming role on the client that it plays in PPR on the server: rather than blocking the whole page while data loads, it lets pieces of the UI resolve independently. Paired with the Next.js App Router, this also brings server components that stream progressively into the page instead of holding up the entire load.

useDeferredValue addresses a related but distinct case: expensive re-renders driven by values coming from props or context, rather than from local component state. It lets React delay recalculating those expensive parts until it has spare time.

For teams working across React, Next.js, TypeScript, Redux, and Tailwind CSS, this scheduling model matters beyond the individual hooks — newer async patterns in Redux Toolkit and the way Next.js Server Actions behave under the hood both rely on the same underlying concurrent scheduling that React now provides. Concurrency isn't something you switch on directly; it's a capability React applies automatically once your components are structured in a way that allows their rendering to be interrupted and resumed.

What to remember

Across both techniques, the underlying lesson is the same: treating an entire page, or an entire render, as one indivisible unit is what causes sluggishness, not necessarily inefficient code. Concurrent Rendering isn't about writing faster code — it's about smarter scheduling of the code you already have. In practice that means deferring non-urgent state updates with useTransition, streaming in data with Suspense, and testing on lower-end devices, since that's where the benefits of concurrency are most visible. Combined with Partial Pre-rendering on the server side, these tools let a single route or a single component tree deliver static content instantly while dynamic content streams in only when it's ready — the best of both rendering worlds, without having to rebuild your architecture around either extreme.