Skip to content

Commit d1cf4e6

Browse files
Cache Component Guide: Building public, _mostly_ static pages (vercel#87248)
This PR adds a draft guide for building public pages with Cache Components and PPR. Part of a 3-guide series on: - Building public pages with shared data - Building page variants with route params (`generateStaticParams`) - Build private pages with user-specific data Todo: - [x] Link to glossary terms - [x] Link to video - [x] Link to demo + code
1 parent b3f7763 commit d1cf4e6

File tree

2 files changed

+286
-13
lines changed

2 files changed

+286
-13
lines changed
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
---
2+
title: Building public pages
3+
description: Learn how to build public, "static" pages that share data across users, such as landing pages, list pages (products, blogs, etc.), marketing and news sites.
4+
nav_title: Building public pages
5+
---
6+
7+
Public pages show the same content to every user. Common examples include landing pages, marketing pages, and product pages.
8+
9+
Since data is shared, these kind of pages can be [prerendered](/docs/app/glossary#prerendering) ahead of time and reused. This leads to faster page loads and lower server costs.
10+
11+
This guide will show you how to build public pages that share data across users.
12+
13+
## Example
14+
15+
As an example, we'll build a product list page.
16+
17+
We'll start with a static header, add a product list with async external data, and learn how to render it without blocking the response. Finally, we'll add a user-specific promotion banner without switching the entire page to [request-time rendering](/docs/app/glossary#request-time-rendering).
18+
19+
You can find the resources used in this example here:
20+
21+
- [Video](https://youtu.be/F6romq71KtI)
22+
- [Demo](https://cache-components-public-pages.labs.vercel.dev/)
23+
- [Code](https://github.com/vercel-labs/cache-components-public-pages)
24+
25+
### Step 1: Add a simple header
26+
27+
Let's start with a simple header.
28+
29+
```tsx filename=app/products/page.tsx
30+
// Static component
31+
function Header() {
32+
return <h1>Shop</h1>
33+
}
34+
35+
export default async function Page() {
36+
return (
37+
<>
38+
<Header />
39+
</>
40+
)
41+
}
42+
```
43+
44+
#### Static components
45+
46+
The `<Header />` component doesn't depend on any inputs that change between requests, such as: external data, request headers, route params, the current time, or random values.
47+
48+
Since its output never changes and can be determined ahead of time, this kind of component is called a **static** component.
49+
50+
With no reason to wait for a request, Next.js can safely **prerender** the page at [build time](/docs/app/glossary#build-time).
51+
52+
We can confirm this by running [`next build`](/docs/app/api-reference/cli/next#next-build-options).
53+
54+
```bash filename=terminal
55+
Route (app) Revalidate Expire
56+
┌ ○ /products 15m 1y
57+
└ ○ /_not-found
58+
59+
○ (Static) prerendered as static content
60+
```
61+
62+
Notice that the product route is marked as static, even though we didn't add any explicit configuration.
63+
64+
### Step 2: Add the product list
65+
66+
Now, let's fetch and render our product list.
67+
68+
```tsx filename=page.tsx
69+
import db from '@/db'
70+
import { List } from '@/app/products/ui'
71+
72+
function Header() {}
73+
74+
// Dynamic component
75+
async function ProductList() {
76+
const products = await db.product.findMany()
77+
return <List items={products} />
78+
}
79+
80+
export default async function Page() {
81+
return (
82+
<>
83+
<Header />
84+
<ProductList />
85+
</>
86+
)
87+
}
88+
```
89+
90+
Unlike the header, the product list depends on external data.
91+
92+
#### Dynamic components
93+
94+
Because this data **can** change over time, the rendered output is no longer guaranteed to be stable.
95+
96+
This makes it a **dynamic** component.
97+
98+
Without guidance, the framework assumes you want to fetch **fresh** data on every user request. This design choice reflects standard web behavior where a new server request renders the page.
99+
100+
However, if this component is rendered at request time, fetching its data will delay the **entire** route from responding.
101+
102+
If we refresh the page, we can see this happen.
103+
104+
Even though the header is rendered instantly, it can't be sent to the browser until the product list has finished fetching.
105+
106+
To protect us from this performance cliff, Next.js will show us a [warning](/docs/messages/blocking-route) the first time we **await** data: `Blocking data was accessed outside of Suspense`
107+
108+
At this point, we have to decide how to **unblock** the response. Either:
109+
110+
- [**Cache**](/docs/app/glossary#cache-components) the component, so it becomes **stable** and can be prerendered with the rest of the page.
111+
- [**Stream**](/docs/app/glossary#streaming) the component, so it becomes **non-blocking** and the rest of the page doesn't have to wait for it.
112+
113+
In our case, the product catalog is shared across all users, so caching is the right choice.
114+
115+
### Cache components
116+
117+
We can mark a function as cacheable using the [`'use cache'`](/docs/app/api-reference/directives/use-cache) directive.
118+
119+
```tsx filename=page.tsx
120+
import db from '@/db'
121+
import { List } from '@/app/products/ui'
122+
123+
function Header() {}
124+
125+
// Cache component
126+
async function ProductList() {
127+
'use cache'
128+
const products = await db.product.findMany()
129+
return <List items={products} />
130+
}
131+
132+
export default async function Page() {
133+
return (
134+
<>
135+
<Header />
136+
<ProductList />
137+
</>
138+
)
139+
}
140+
```
141+
142+
This turns it into a [cache component](/docs/app/glossary#cache-components).
143+
144+
The first time it runs, whatever we return will be cached and reused.
145+
146+
If a cache component's inputs are available **before** the request arrives, it can be prerendered just like a static component.
147+
148+
If we refresh again, we can see the page loads instantly because the cache component doesn't block the response. And, if we run `next build` again, we can confirm the page is still static:
149+
150+
```bash filename=terminal
151+
Route (app) Revalidate Expire
152+
┌ ○ /products 15m 1y
153+
└ ○ /_not-found
154+
155+
○ (Static) prerendered as static content
156+
```
157+
158+
But, pages rarely stay static forever.
159+
160+
### Step 3: Add a dynamic promotion banner
161+
162+
Sooner or later, even simple pages need some dynamic content.
163+
164+
To demonstrate this, let's add a promotional banner.
165+
166+
```tsx filename=app/products/page.tsx
167+
import db from '@/db'
168+
import { List, Promotion } from '@/app/products/ui'
169+
import { getPromotion } from '@/app/products/data'
170+
171+
function Header() {}
172+
173+
async function ProductList() {}
174+
175+
// Dynamic component
176+
async function PromotionContent() {
177+
const promotion = await getPromotion()
178+
return <Promotion data={promotion} />
179+
}
180+
181+
export default async function Page() {
182+
return (
183+
<>
184+
<PromotionContent />
185+
<Header />
186+
<ProductList />
187+
</>
188+
)
189+
}
190+
```
191+
192+
Once again, this starts off as dynamic. And as before, introducing blocking behavior triggers a Next.js warning.
193+
194+
Last time, the data was shared, so it could be cached.
195+
196+
This time, the promotion depends on request specific inputs like the user's location and A/B tests, so we can't cache our way out of the blocking behavior.
197+
198+
### Partial prerendering
199+
200+
Adding dynamic content doesn't mean we have to go back to a fully blocking render. We can unblock the response with streaming.
201+
202+
Next.js supports streaming by default. We can use a [Suspense boundary](/docs/app/glossary#suspense-boundary) to tell the framework where to slice the streamed response into _chunks_, and what fallback UI to show while content loads.
203+
204+
```tsx filename=app/products/page.tsx
205+
import { Suspense } from 'react'
206+
import db from '@/db'
207+
import { List, Promotion, PromotionSkeleton } from '@/app/products/ui'
208+
import { getPromotion } from '@/app/products/data'
209+
210+
function Header() {}
211+
212+
async function ProductList() {}
213+
214+
// Dynamic component (streamed)
215+
async function PromotionContent() {
216+
const promotion = await getPromotion()
217+
return <Promotion data={promotion} />
218+
}
219+
220+
export default async function Page() {
221+
return (
222+
<>
223+
<Suspense fallback={<PromotionSkeleton />}>
224+
<PromotionContent />
225+
</Suspense>
226+
<Header />
227+
<ProductList />
228+
</>
229+
)
230+
}
231+
```
232+
233+
The fallback is prerendered alongside the rest of our static and cached content. The inner component streams in later, once its async work completes.
234+
235+
With this change, Next.js can separate prerenderable work from request-time work and the route becomes [partially prerendered](/docs/app/glossary#partial-prerendering-ppr).
236+
237+
Again, we can confirm this by running `next build`
238+
239+
```bash filename=terminal
240+
Route (app) Revalidate Expire
241+
┌ ◐ /products 15m 1y
242+
└ ◐ /_not-found
243+
244+
◐ (Partial Prerender) Prerendered as static HTML with dynamic server-streamed content
245+
```
246+
247+
At [**build time**](/docs/app/glossary#build-time), most of the page, including the header, product list and promotion fallback, is rendered, cached and pushed to a content delivery network.
248+
249+
At [**request time**](/docs/app/glossary#request-time), the prerendered part is served instantly from a CDN node close to the user.
250+
251+
In parallel, the user specific promotion is rendered on the server, streamed to the client, and swapped into the fallback slot.
252+
253+
If we refresh the page one last time, we can see most of the page loads instantly, while the dynamic parts stream in as they become available.
254+
255+
### Next steps
256+
257+
We've learned how to build mostly static pages that include pockets of dynamic content.
258+
259+
We started with a static page, added async work, and resolved the blocking behavior by caching what could be prerendered, and streaming what couldn't.
260+
261+
In future guides, we'll learn how to:
262+
263+
- Revalidate prerendered pages or cached data.
264+
- Create variants of the same page with route params.
265+
- Create private pages with personalized user data.
Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,9 @@ The process of dividing your application into smaller JavaScript chunks based on
4444

4545
# D
4646

47-
## Dynamic APIs
48-
49-
Functions that access request-specific data, causing a component to opt into [dynamic rendering](#dynamic-rendering). These include:
50-
51-
- [`cookies()`](/docs/app/api-reference/functions/cookies) - Access request cookies
52-
- [`headers()`](/docs/app/api-reference/functions/headers) - Access request headers
53-
- [`searchParams`](/docs/app/api-reference/file-conventions/page#searchparams-optional) - Access URL query parameters
54-
- [`draftMode()`](/docs/app/api-reference/functions/draft-mode) - Enable or check draft mode
55-
5647
## Dynamic rendering
5748

58-
When a component is rendered at [request time](#request-time) rather than [build time](#build-time). A component becomes dynamic when it uses [Dynamic APIs](#dynamic-apis).
49+
See [Request-time rendering](#request-time-rendering).
5950

6051
## Dynamic route segments
6152

@@ -131,6 +122,10 @@ Information about a page used by browsers and search engines, such as title, des
131122

132123
Caching the return value of a function so that calling the same function multiple times during a render pass (request) only executes it once. In Next.js, fetch requests with the same URL and options are automatically memoized. Learn more about [React Cache](https://react.dev/reference/react/cache).
133124

125+
## Middleware
126+
127+
See [Proxy](#proxy).
128+
134129
# N
135130

136131
## Not Found
@@ -161,7 +156,7 @@ Loading a route in the background before the user navigates to it. Next.js autom
161156

162157
## Prerendering
163158

164-
Generating HTML for a page ahead of time, either at build time (static rendering) or in the background (revalidation). The pre-rendered HTML is served immediately, then hydrated on the client.
159+
When a component is rendered at [build time](#build-time) or in the background during [revalidation](#revalidation). The result is HTML and [RSC Payload](#rsc-payload), which can be cached and served from a CDN. Prerendering is the default for components that don't use [Request-time APIs](#request-time-apis).
165160

166161
## Proxy
167162

@@ -177,6 +172,19 @@ Sending users from one URL to another. In Next.js, redirects can be configured i
177172

178173
The time when a user makes a request to your application. At request time, dynamic routes are rendered, cookies and headers are accessible, and request-specific data can be used.
179174

175+
## Request-time APIs
176+
177+
Functions that access request-specific data, causing a component to opt into [request-time rendering](#request-time-rendering). These include:
178+
179+
- [`cookies()`](/docs/app/api-reference/functions/cookies) - Access request cookies
180+
- [`headers()`](/docs/app/api-reference/functions/headers) - Access request headers
181+
- [`searchParams`](/docs/app/api-reference/file-conventions/page#searchparams-optional) - Access URL query parameters
182+
- [`draftMode()`](/docs/app/api-reference/functions/draft-mode) - Enable or check draft mode
183+
184+
## Request-time rendering
185+
186+
When a component is rendered at [request time](#request-time) rather than [build time](#build-time). A component becomes dynamic when it uses [Request-time APIs](#request-time-apis).
187+
180188
## Revalidation
181189

182190
The process of updating cached data. Can be time-based (using [`cacheLife()`](/docs/app/api-reference/functions/cacheLife) to set cache duration) or on-demand (using [`cacheTag()`](/docs/app/api-reference/functions/cacheTag) to tag data, then [`updateTag()`](/docs/app/api-reference/functions/updateTag) to invalidate). Learn more in [Caching and Revalidating](/docs/app/getting-started/caching-and-revalidating).
@@ -221,15 +229,15 @@ A deployment mode that generates a fully static site with HTML, CSS, and JavaScr
221229

222230
## Static rendering
223231

224-
When a component is rendered at [build time](#build-time) or in the background during [revalidation](#revalidation). The result is cached and can be served from a CDN. Static rendering is the default for components that don't use [Dynamic APIs](#dynamic-apis).
232+
See [Prerendering](#prerendering).
225233

226234
## Static Assets
227235

228236
Files such as images, fonts, videos, and other media that are served directly without processing. Static assets are typically stored in the `public` directory and referenced by their relative paths. Learn more in [Static Assets](/docs/app/api-reference/file-conventions/public-folder).
229237

230238
## Static Shell
231239

232-
The pre-rendered HTML structure of a page that's served immediately to the browser. With [Partial Prerendering](#partial-prerendering-ppr), the static shell includes all statically renderable content plus [Suspense boundary](#suspense-boundary) fallbacks for dynamic content that streams in later.
240+
The prerendered HTML structure of a page that's served immediately to the browser. With [Partial Prerendering](#partial-prerendering-ppr), the static shell includes all statically renderable content plus [Suspense boundary](#suspense-boundary) fallbacks for dynamic content that streams in later.
233241

234242
## Streaming
235243

0 commit comments

Comments
 (0)