Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/01-app/02-guides/analytics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ experience of a web page. The following web vitals are all included:
- [Time to First Byte](https://developer.mozilla.org/docs/Glossary/Time_to_first_byte) (TTFB)
- [First Contentful Paint](https://developer.mozilla.org/docs/Glossary/First_contentful_paint) (FCP)
- [Largest Contentful Paint](https://web.dev/lcp/) (LCP)
- [First Input Delay](https://web.dev/fid/) (FID)
- [Cumulative Layout Shift](https://web.dev/cls/) (CLS)
- [Interaction to Next Paint](https://web.dev/inp/) (INP)

Expand Down
4 changes: 3 additions & 1 deletion docs/01-app/02-guides/migrating-to-cache-components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const instant = false

You don't have to migrate every route at once. `instant = false` lets you get the whole app building and running first, then convert routes one at a time:

1. **Enable the flag and remove the route segment configs** (`dynamic`, `revalidate`, `fetchCache`). Routes that still render instantly need no further work.
1. **Enable the flag and migrate each route segment config.** Follow the relevant sections below for [`dynamic = "force-dynamic"`](#dynamic--force-dynamic), [`dynamic = "force-static"`](#dynamic--force-static), [`revalidate`](#revalidate), and [`fetchCache`](#fetchcache). For routes with dynamic params, also follow the [`generateStaticParams`](#generatestaticparams-and-dynamicparams) guidance. Routes that still render instantly need no further work.
2. **Opt out the routes that aren't ready.** Where an insight or error appears, set `instant = false` on the segment that raised it. To do this in one pass across the whole app, run the [`cache-components-instant-false`](/docs/app/guides/upgrading/codemods#cache-components-instant-false) codemod, which adds the opt-out to every `page`, `layout`, and `default` that doesn't already declare `instant`:

```bash filename="Terminal"
Expand Down Expand Up @@ -569,6 +569,8 @@ Cache Components changes how [dynamic routes](/docs/app/api-reference/file-conve

**Returning an empty array now errors.** Without Cache Components, returning `[]` defers every path to the first runtime visit. With Cache Components, [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) must return at least one param so Next.js can prerender the route and validate it produces a non-empty [static shell](/docs/app/glossary#static-shell). An empty array raises [`empty-generate-static-params`](/docs/messages/empty-generate-static-params).

Keep `generateStaticParams` and return at least one real param. Removing the export opts the route out of ISR, so Next.js renders it on every request, even when it uses `use cache` for data. Read [ISR with Cache Components](/docs/app/guides/incremental-static-regeneration-cache-components) to learn how `generateStaticParams` prerenders dynamic routes and upgrades unlisted paths after their first visit.

```tsx filename="app/blog/[slug]/page.tsx" switcher
// Before - defer all paths to runtime
export async function generateStaticParams() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,14 @@ export default function Layout({ children }) {
The `metric` object passed as the hook's argument consists of a number of properties:

- `id`: Unique identifier for the metric in the context of the current page load
- `name`: The name of the performance metric. Possible values include names of [Web Vitals](#web-vitals) metrics (TTFB, FCP, LCP, FID, CLS) specific to a web application.
- `name`: The name of the performance metric. Possible values include names of [Web Vitals](#web-vitals) metrics (TTFB, FCP, LCP, INP, CLS) specific to a web application.
- `delta`: The difference between the current value and the previous value of the metric. The value is typically in milliseconds and represents the change in the metric's value over time.
- `entries`: An array of [Performance Entries](https://developer.mozilla.org/docs/Web/API/PerformanceEntry) associated with the metric. These entries provide detailed information about the performance events related to the metric.
- `navigationType`: Indicates the navigation type that triggered metric collection. Values are derived from [PerformanceNavigationTiming.type](https://developer.mozilla.org/docs/Web/API/PerformanceNavigationTiming/type) and may include `"navigate"`, `"reload"`, `"prerender"`, `"back-forward"` (normalized from `"back_forward"`), `"back-forward-cache"` (BFCache restore), and `"restore"` (page restored after discard).
- `navigationType`: Indicates the navigation type that triggered metric collection. Values are derived from [PerformanceNavigationTiming.type](https://developer.mozilla.org/docs/Web/API/PerformanceNavigationTiming/type) and may include `"navigate"`, `"reload"`, `"prerender"`, `"back-forward"` (normalized from `"back_forward"`), `"back-forward-cache"` (BFCache restore), `"restore"` (page restored after discard), and `"soft-navigation"`.
- `navigationId`: The ID of the navigation for which the metric was measured.
- `navigationURL`: The URL of the navigation for which the metric was measured. This is especially useful for soft navigations because a metric may be reported after the URL has changed again.
- `navigationStartTime`: The start time of the navigation for which the metric was measured.
- `navigationInteractionId`: For soft navigations, the interaction ID that triggered the navigation.
- `rating`: A qualitative rating of the metric value, providing an assessment of the performance. Possible values are `"good"`, `"needs-improvement"`, and `"poor"`. The rating is typically determined by comparing the metric value against predefined thresholds that indicate acceptable or suboptimal performance.
- `value`: The actual value or duration of the performance entry, typically in milliseconds. The value provides a quantitative measure of the performance aspect being tracked by the metric. The source of the value depends on the specific metric being measured and can come from various [Performance API](https://developer.mozilla.org/docs/Web/API/Performance_API)s.

Expand All @@ -84,7 +88,6 @@ experience of a web page. The following web vitals are all included:
- [Time to First Byte](https://developer.mozilla.org/docs/Glossary/Time_to_first_byte) (TTFB)
- [First Contentful Paint](https://developer.mozilla.org/docs/Glossary/First_contentful_paint) (FCP)
- [Largest Contentful Paint](https://web.dev/lcp/) (LCP)
- [First Input Delay](https://web.dev/fid/) (FID)
- [Cumulative Layout Shift](https://web.dev/cls/) (CLS)
- [Interaction to Next Paint](https://web.dev/inp/) (INP)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The `taint` option enables support for experimental React APIs for tainting obje
- [`experimental_taintObjectReference`](https://react.dev/reference/react/experimental_taintObjectReference) taint objects references.
- [`experimental_taintUniqueValue`](https://react.dev/reference/react/experimental_taintUniqueValue) to taint unique values.

> **Good to know**: Activating this flag also enables the React `experimental` channel for `app` directory.
> **Good to know**: Activating this flag also enables the React `experimental` channel for `app` directory, and taints `process.env` so it cannot be passed whole to a Client Component.

```ts filename="next.config.ts" switcher
import type { NextConfig } from 'next'
Expand Down Expand Up @@ -51,6 +51,7 @@ It is recommended to model your data and APIs so that sensitive data is not retu
## Caveats

- Tainting can only keep track of objects by reference. Copying an object creates an untainted version, which loses all guarantees given by the API. You'll need to taint the copy.
- The built-in `process.env` taint applies to the object reference only. Reading individual variables such as `process.env.MY_VAR` and passing the resulting string to a Client Component is unaffected, as is passing a copy like `{ ...process.env }`.
- Tainting cannot keep track of data derived from a tainted value. You also need to taint the derived value.
- Values are tainted for as long as their lifetime reference is within scope. See the [`experimental_taintUniqueValue` parameters reference](https://react.dev/reference/react/experimental_taintUniqueValue#parameters), for more information.

Expand Down
4 changes: 4 additions & 0 deletions evals/eval.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
"agent-047-adopt-cache-components": {
"skills": ["next-cache-components-adoption"],
"timeout": 1800
},
"agent-054-cache-components-empty-static-params": {
"skills": ["next-cache-components-adoption"],
"timeout": 1800
}
}
38 changes: 38 additions & 0 deletions evals/evals/agent-054-cache-components-empty-static-params/EVAL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Preserve on-demand ISR when adopting Cache Components
*
* The starting route uses the previous on-demand ISR pattern:
* `force-static`, `revalidate`, and an empty `generateStaticParams` result.
* Cache Components rejects an empty result, but deleting the function changes
* the route to request-time rendering. The migration must retain the export
* and give it at least one param so other params can still be cached on demand.
*/

import { expect, test } from 'vitest'
import { readFileSync } from 'fs'
import { join } from 'path'

const config = readFileSync(join(process.cwd(), 'next.config.ts'), 'utf-8')
const eventPage = readFileSync(
join(process.cwd(), 'app/events/[slug]/page.tsx'),
'utf-8'
)
const eventPageWithoutComments = eventPage
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/.*$/gm, '')

test('enables Cache Components', () => {
expect(config).toMatch(/cacheComponents\s*:\s*true/)
})

test('preserves generateStaticParams for on-demand ISR', () => {
expect(eventPageWithoutComments).toMatch(
/export\s+(?:(?:async\s+)?function\s+generateStaticParams\b|const\s+generateStaticParams\s*=)/
)
})

test('generateStaticParams no longer returns an empty array', () => {
expect(eventPageWithoutComments).not.toMatch(
/generateStaticParams[\s\S]*?return\s*\[\s*\]/
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Migrate this app to Cache Components incrementally. Make the smallest safe first change that enables Cache Components, keeps the build passing, and preserves the current rendering and caching behavior.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { notFound } from 'next/navigation'
import { getEvent } from '../../../lib/events'

export const dynamic = 'force-static'
export const revalidate = 60

export function generateStaticParams() {
return []
}

export default async function EventPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const event = await getEvent(slug)

if (!event) notFound()

return (
<main>
<h1>{event.title}</h1>
<p>{event.description}</p>
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ReactNode } from 'react'

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function HomePage() {
return (
<main>
<h1>Events</h1>
<Link href="/events/launch-day">View the featured event</Link>
</main>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const FEATURED_EVENT_SLUG = 'launch-day'

const events = {
[FEATURED_EVENT_SLUG]: {
title: 'Launch Day',
description: 'Follow the launch as it happens.',
},
} as const

export async function getEvent(slug: string) {
return events[slug as keyof typeof events] ?? null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {}

export default nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "^16",
"react": "19.1.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^4.4.1",
"typescript": "^5",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
},
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,6 @@
"stacktrace-parser@0.1.10": "patches/stacktrace-parser@0.1.10.patch",
"taskr@1.1.0": "patches/taskr@1.1.0.patch",
"minizlib@3.1.0": "patches/minizlib@3.1.0.patch",
"web-vitals@4.2.1": "patches/web-vitals@4.2.1.patch",
"@rspack/core@1.6.7": "patches/@rspack__core@1.6.7.patch",
"@modelcontextprotocol/sdk": "patches/@modelcontextprotocol__sdk.patch",
"@vercel/blob": "patches/@vercel__blob.patch",
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@
"util": "0.12.4",
"vm-browserify": "1.1.2",
"watchpack": "2.4.0",
"web-vitals": "4.2.1",
"web-vitals": "6.2.1",
"webpack": "5.98.0",
"webpack-sources1": "npm:webpack-sources@1.4.3",
"webpack-sources3": "npm:webpack-sources@3.2.3",
Expand Down
10 changes: 4 additions & 6 deletions packages/next/src/client/web-vitals.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect } from 'react'
import {
onLCP,
onFID,
onCLS,
onINP,
onFCP,
Expand All @@ -13,11 +12,10 @@ export function useReportWebVitals(
reportWebVitalsFn: (metric: Metric) => void
) {
useEffect(() => {
onCLS(reportWebVitalsFn)
onFID(reportWebVitalsFn)
onLCP(reportWebVitalsFn)
onINP(reportWebVitalsFn)
onFCP(reportWebVitalsFn)
onCLS(reportWebVitalsFn, { reportSoftNavs: true })
onLCP(reportWebVitalsFn, { reportSoftNavs: true })
onINP(reportWebVitalsFn, { reportSoftNavs: true })
onFCP(reportWebVitalsFn, { reportSoftNavs: true })
onTTFB(reportWebVitalsFn)
}, [reportWebVitalsFn])
}
Loading
Loading