惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
G
Google Developers Blog
J
Java Code Geeks
The GitHub Blog
The GitHub Blog
F
Full Disclosure
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Vercel News
Vercel News
酷 壳 – CoolShell
酷 壳 – CoolShell
Recent Announcements
Recent Announcements
Help Net Security
Help Net Security
The Hacker News
The Hacker News
IT之家
IT之家
Y
Y Combinator Blog
Martin Fowler
Martin Fowler
L
Lohrmann on Cybersecurity
C
CERT Recently Published Vulnerability Notes
V
Visual Studio Blog
博客园 - 聂微东
Hacker News: Ask HN
Hacker News: Ask HN
H
Hacker News: Front Page
Know Your Adversary
Know Your Adversary
Security Latest
Security Latest
Security Archives - TechRepublic
Security Archives - TechRepublic
Simon Willison's Weblog
Simon Willison's Weblog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Troy Hunt's Blog
Last Week in AI
Last Week in AI
Schneier on Security
Schneier on Security
N
News and Events Feed by Topic
博客园 - 【当耐特】
有赞技术团队
有赞技术团队
AWS News Blog
AWS News Blog
Blog — PlanetScale
Blog — PlanetScale
博客园_首页
Google DeepMind News
Google DeepMind News
Cloudbric
Cloudbric
N
News | PayPal Newsroom
A
About on SuperTechFans
S
Schneier on Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Hugging Face - Blog
Hugging Face - Blog
M
MIT News - Artificial intelligence
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
T
The Exploit Database - CXSecurity.com
罗磊的独立博客
K
Kaspersky official blog
The Cloudflare Blog
I
Intezer

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
A closer look at partial prerendering
Peter Jacxse · 2026-05-19 · via DEV Community

Partial prerendering replaces the old binary model of rendering. Before, there were 2 cases. Either the entire route was statically rendered at build time or - if any component in the route had a dynamic element - the entire route was dynamically rendered, that is, rendered server-side at request time.

Dynamic rendering is triggered automatically when a component uses:

  • headers, cookies or connection function
  • searchParams or params page prop
  • draftMode
  • dynamic fetch (fetch with no cache)

The downside to dynamic rendering is obvious. Components that could be statically rendered aren't because the route contains a dynamic element. This is exactly what PPR solves. When a route is made up of static and dynamic components, PPR will statically render the static components into a static shell for the route. The dynamic components must be wrapped inside suspense. The suspense fallback is included into the static shell and the dynamic component itself will be streamed in at runtime.

Example

Note: the examples are available on a new repo on github.

First off, we need to turn on PPR. This is simple, add following config option:

// next.config.ts

const nextConfig: NextConfig = {
  cacheComponents: true,
};

Enter fullscreen mode Exit fullscreen mode

We will create some components to form our static shell:

// components/global/Header.tsx

export default function Header() {
  return <header>**** Header ****</header>;
}

// components/global/Footer.tsx

export default function Footer() {
  return <footer>**** Footer ****</footer>;
}

Enter fullscreen mode Exit fullscreen mode

Then we create a dynamic component. We use connection to keep it simple.

// components/ppr/SimpleConnection.tsx

export default async function SimpleConnection() {
  await connection();
  return <div>Hello world!</div>;
}

Enter fullscreen mode Exit fullscreen mode

Finally, we put it all together in a route:

// ❌ this example fails, see below
// app/chapter-12/connection/page.tsx

export default function Page() {
  return (
    <>
      <Header />
      <SimpleConnection />
      <Footer />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

This is not how you should do it. We needed to wrap <SimpleConnection /> inside suspense. But I wanted to demonstrate it first without. When we run next build, we get an error and the build fails:

Error: Route "/chapter-12/connection": Uncached data was accessed outside of <Suspense>. This delays the entire page from rendering, resulting in a slow user experience.

...

Enter fullscreen mode Exit fullscreen mode

This is great; when we forget suspense, Next shouts at us! We update the example with suspense:

export default function Page() {
  return (
    <>
      <Header />
      <Suspense fallback='...loading'>
        <SimpleConnection />
      </Suspense>
      <Footer />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

This is pure and simple PPR. Given what we learned we expect 2 things:

  1. At build time: A static shell with header, footer and the loading fallback.
  2. At runtime: the fallback gets swapped with the resolved component.

1. We expect a static shell

We run Next build. The build log gives us the first information:

Route (app)
├ ◐ /chapter-12/connection

○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

Enter fullscreen mode Exit fullscreen mode

We have partial prerendering.

Let's take a look at the static shell now. We know from earlier chapters that Next will generate HTML and rsc at build time. This is also true for PPR. On top of that the build folder is the same. So, we can take a look at it. Here is a cleaned-up version of the prerendered HTML:

<!-- .next\server\app\chapter-12\connection.html -->

<!doctype html>
<html lang="en">
  <body>
    <header>**** Header ****</header>
    <!--$?--><template id="B:0"></template>...loading<!--/$-->
    <footer>**** Footer ****</footer>
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

As for the rsc, that's a bit different. In a previous chapter, we looked at the rsc payload of static rendering. We learned that each route had an HTML and rsc file on the same level. So, for example app/example/page.tsx would be generated as example.html and example.rsc in the build folder .next/server/app.

PPR routes are a bit different. The rsc sits in a subfolder and is divided across multiple files, segments. But that doesn't really matter. Here is a cleaned-up rsc snippet:

// .next\server\app\chapter-12\connection.segments\_full.segment.rsc
[
  ['$', 'header', null, { children: '**** Header ****' }],
  ['$', '$5', null, { fallback: '...loading', children: '$L6' }],
  ['$', 'footer', null, { children: '**** Footer ****' }],
];

Enter fullscreen mode Exit fullscreen mode

Let's take a small step back. We're looking at a route that would've rendered dynamically in the previous model because it contains a dynamic element: connection. But we're now prerendering it partially. Everything in the route that can be statically rendered is prerendered. This is the static shell.

The dynamic parts are skipped. We prerender a placeholder (fallback) into the static shell and when a request is made at runtime, the dynamic component is resolved server side in the background and then streamed to the client where it simply is swapped with the placeholder.

We just looked at these static shells. HTML for initial requests and rsc for client side routing. We saw the static parts: header and footer and the fallback: "...loading".

2. We expect streaming

The dynamic elements are not prerendered. To render them, we need to run the app. We want to render <SimpleConnection />:

// components/ppr/SimpleConnection.tsx

export default async function SimpleConnection() {
  await connection();
  return <div>Hello world!</div>;
}

Enter fullscreen mode Exit fullscreen mode

We already ran next build. Now we run next start to run the app and visit our route: /chapter-12/connection. We can see the loading state very brief and then it gets swapped with "Hello world!". So it works.

Everything happens server side at request time. Nothing is saved or cached or put into the build folder so we can't take a look at it.

What is streamed?

We took a deep look at how static and dynamic rendering works in earlier chapters and we learned that routes are rendered into HTML and rsc files. The HTML and rsc get served on initial load. On client side routing however, only the rsc is used to update routes. We know this. Streaming works similarly.

On initial load, HTML get streamed in. On client side routing, rsc is streamed. We can use our /chapter-12/connection example and look at the network tab in our dev tools to see this. When we're on the route /chapter-12/connection and refresh the tab (simulate on first load) we see something interesting in our network tab. We're looking at the root document file (the html file)

network tab streaming

When we inspect this file we recognize the prerendered static shell from earlier but also something else:

<!-- Response  -->
<!-- edited: cleaned up -->

<!DOCTYPE html>
<html lang="en">
  <body>
    <!--&-->
    <header>**** Header ****</header>
    <!--$?-->
    <template id="B:0"></template>
    ...loading
    <!--/$-->
    <footer>**** Footer ****</footer>
    <!--/&-->
    <!-- scripts here -->
    <div hidden id="S:0">
      <div>Hello world!</div>
    </div>
    <!-- scripts here -->
  </body>
</html>

Enter fullscreen mode Exit fullscreen mode

This part:

<div hidden id="S:0">
  <div>Hello world!</div>
</div>

Enter fullscreen mode Exit fullscreen mode

That is our resolved and generated <SimpleConnection /> component, inside our static shell. This was streamed in. Next kept the connection open and ... added this. I have no idea how these connections work so I can't explain the how. But it's the what that is important here. Extra HTML was streamed into the HTML file that Next first sent to the browser.

It is a two part process. First, the static shell that was prerendered at build time was send to the browser. Then, when Next rendered the dynamic component, it sent over some more HTML into the same file. The fallback swap is easy then. Take content of div with ID X and put it into div with ID Y.

So, on initial load, Next renders dynamic elements into HTML and streams it to the browser.

On client side navigation, only rsc is used. I added some links in my examples so we can navigate to our route. We clear our network tab and navigate to route /chapter-12/connection. A single rsc files appears (well, 2 icons as well but meh): http://localhost:3000/chapter-12/connection?_rsc=ob2DjmuOZafxCs0z.

Streaming rsc

As expected, it's rsc and it's complex. But, we can just about make out the original static shell fragments and also content of the resolved component ("Hello world!"). How Next uses this rsc to paint in the browser doesn't matter. We only wanted to confirm rsc was streamed.

Client components

Maybe you're wondering how client components fit into the PPR model. They don't. PPR does not affect client components.

Quick recap: client components are components that use:

  • hooks or custom hooks
  • interactive element (event listeners)
  • browser only APIs (e.g. localStorage)

Dynamic components are components that use dynamic functions (headers, cookies, searchParams or params page prop, connection or draftmode) of dynamic fetches (no caching).

Dynamic components and client components don't overlap. However, all dynamic functions are asynchronous while client components cannot be async. This means that client components cannot be dynamic. All of this isn't new.

Client components never require a dynamic suspense boundary - because they can't be dynamic. Except when using the useSearchParams hook. But we covered this already in a previous chapter.

Client components can of course be inside suspense boundaries as a child of a dynamic component. Take a look at this example:

// app/chapter-12/dynamic-with-client-child/page.tsx

export default function Page() {
  return (
    <>
      <Header />
      <Suspense fallback='...loading'>
        <DynamicParent />
      </Suspense>
      <Footer />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

// components/ppr/DynamicParent.tsx

export default async function DynamicParent() {
  await connection();
  return (
    <div>
      <h1>**** Dynamic component</h1>
      <ClientChild />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

// components/ppr/ClientChild.tsx

'use client';

import { useState } from 'react';

export default function ClientChild() {
  const [count, setCount] = useState(0);
  return (
    <>
      <div>**** count: {count}</div>
      <button onClick={() => setCount((prev) => prev + 1)}>add one</button>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

So, we have suspense that wraps a dynamic component and said component imports a client component.

Just before, we learned that dynamic components are streamed as HTML on initial load or as rsc on client side routing. But what about our client component?

On initial load, the client component will also be rendered server-side into a non interactive shell, just the HTML, no event listeners:

<div>**** count: {count}</div>
<button>add one</button>

Enter fullscreen mode Exit fullscreen mode

This is streamed to the client, as HTML to the HTML document file, where it is swapped with the fallback. Once the entire client bundle is loaded, Next will virtually run <ClientChild /> again in the browser and use it to hydrate the prerendered HTML.

On client side routing, the process is different. Only rsc is streamed. The server resolves <DynamicParent /> and renders it into rsc. Inside this rsc, there will be a placeholder that refers to <ClientChild />. <ClientChild /> does not get rendered into rsc itself. The rsc is sent to the client. On the client, <ClientChild /> is rendered and fills the placeholder. Finally, this rendered chunk (<DynamicParent /> and <ClientChild />) is swapped with the fallback. Hydration of <ClientChild /> is not needed since there was no prerendered HTML.

This is just normal client component rendering behaviour. We already covered this in a previous chapter. As I said before, PPR does not affect client components. The only difference is the suspense boundary.

Managing boundaries

In the old model, once you used a dynamic element, the route became dynamic and that was that. Nothing got prerendered. In the PPR model, boundaries determine dynamic rendering and these boundaries need to be managed.

Every dynamic element needs to be resolved: headers, cookies, searchParams page prop, connection and dynamic fetches are all async functions. They return a promise that needs to be resolved. To resolve a promise in PPR, you need to wrap it in suspense (or use loading.ts). Elements wrapped in suspense do not get prerendered. But we want to prerender as much as possible. Therefore, we need to be careful with using suspense.

The first way to optimize this is to place suspense boundaries as far down a component tree as possible. Take a header for example with a logo, a menu and a user account icon. Make the user account icon a separate component and wrap it in suspense. The other elements in the header can then be statically prerendered. We always want to maximize static rendering.

This isn't always obvious. Here is an example. We use our previous example but rework it with the searchParams page prop instead of connection:

// app/chapter-12/searchparams/page.tsx
// ❌ this example fails, see below

export default async function Page({
  searchParams,
}: PageProps<'/chapter-12/searchparams'>) {
  const { name } = await searchParams;
  return (
    <>
      <Header />
      <Suspense fallback='...loading'>
        <SimpleSearchParams name={name} />
      </Suspense>
      <Footer />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

// components/ppr/SimpleSearchParams.tsx

type Props = {
  name: string | string[] | undefined;
};

export default function SimpleSearchParams({ name }: Props) {
  const validatedName = !name ? 'guest' : Array.isArray(name) ? name[0] : name;
  return <div>Hello {validatedName}</div>;
}

Enter fullscreen mode Exit fullscreen mode

This fails at build time. Why? Because we resolved searchParams inside our page component. When Next encounters a promise that needs to be resolved, it marks the component as dynamic and looks for a suspense boundary and we don't have one. This is the error:

Error: Route "/chapter-12/searchparams": Uncached data was accessed outside of <Suspense>.

Enter fullscreen mode Exit fullscreen mode

A very bad ❌ way to solve this problem would be to add a loading.tsx file in our page route. The error will be gone, but the entire page will be rendered dynamically, including our <Header /> and <Footer /> components. This is exactly what we don't want.

The correct ✅ way to solve this is by passing the promise - unresolved - to our <SimpleSearchParams /> component:

export default function Page({
  searchParams,
}: PageProps<'/chapter-12/searchparams'>) {
  return (
    <>
      <Header />
      <Suspense fallback='...loading'>
        <SimpleSearchParams searchParams={searchParams} />
      </Suspense>
      <Footer />
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

The component is no longer async and we just pass the searchParams promise. We then resolve this promise in our <SimpleSearchParams /> component:

type Props = {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
};

export default async function SimpleSearchParams({ searchParams }: Props) {
  const { name } = await searchParams;
  const validatedName = !name ? 'guest' : Array.isArray(name) ? name[0] : name;
  return <div>Hello {validatedName}</div>;
}

Enter fullscreen mode Exit fullscreen mode

We run build and everything runs fine, we have a PPR route with maximum static rendering. Passing a promise down is a second tool you can use to maximize static rendering.

Migrating terminology

With PPR, Next changed some terminology. Here are two of those changes:

The terms dynamic elements or dynamic fetches are no longer used. Instead these are now categorized as:

  • Streaming uncached data (instead of dynamic fetches).
  • Runtime APIs: headers, cookies, params and searchParams page props (instead of dynamic elements).
  • Non-deterministic operations (Math.random(), Date.now(), crypto.randomUUID()) that require connection (instead of dynamic elements).

So, the enveloping term dynamic elements is out of use. I'm not sure this is a good change. It needs to be clear what causes a component to become dynamically rendered and that seems less clear with this change.

Full route cache is no longer mentioned. What used to be the full route cache is now only referred to as the static shells. Again, not a fan of this change. Having a term like full route cache made me understand caching in Next better.

Static, dynamic and partial prerendering

For now we skipped caching and data fetching. We will cover this in later chapters. But, I want to make it clear in this chapter that static rendering still exists in the PPR model. Static rendering, a 100% prerendered route with no dynamic elements or suspense boundaries is still possible. PPR does not eliminate this.

Dynamic rendering has become mostly obsolete by PPR. In most cases we do not want to dynamically render an entire route. We want to prerender as much as possible. That is what PPR allows us to do. However, should you need it, it is still possible to dynamically render a route. Place a suspense with fallback null in the root layout.tsx:

// app/layout.tsx

import { Suspense } from 'react';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <Suspense fallback={null}>
        <body>{children}</body>
      </Suspense>
    </html>
  );
}

Enter fullscreen mode Exit fullscreen mode

When Next encounters null as fallback in the root layout, it will not generate a static shell. This means that this route will have to be generated server side at request time = dynamic rendering. Note that this setup will make every route dynamic. You can alter this by using multiple root layouts. Finally, note that these routes will again be blocking. Nothing will be sent over to the client before everything is resolved and generated.

loading.tsx

We've mostly been using suspense. Note that you can also use the loading.tsx to add a suspense boundary in PPR. See the docs for more info.

Conclusion

I hope this article gave you a more grounded feel for partial prerendering.

In the next chapters we will cover caching with cache components.

If you want to support my writing, you can donate with paypal.