Skip to content

Commit 545d1ae

Browse files
committed
render prompt depending on cache components
1 parent a35a7df commit 545d1ae

File tree

2 files changed

+208
-6
lines changed

2 files changed

+208
-6
lines changed

packages/next/src/server/lib/insights-handler.ts

Lines changed: 202 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ function filterUserFrames(frames: StackFrame[]): StackFrame[] {
179179
export async function handleInsightsReport(
180180
report: InsightsReport,
181181
distDir: string,
182-
projectDir: string
182+
projectDir: string,
183+
cacheComponents: boolean = false
183184
): Promise<void> {
184185
const route = new URL(report.pageUrl).pathname
185186
const debug =
@@ -536,7 +537,12 @@ export async function handleInsightsReport(
536537
LOW: '🟢',
537538
}
538539

539-
console.log(`
540+
// Generate different prompts based on cacheComponents config
541+
let aiInsights: string
542+
543+
if (!cacheComponents) {
544+
// When cache components are NOT enabled, add force-dynamic directive
545+
aiInsights = `
540546
${'─'.repeat(80)}
541547
542548
${severityEmoji[severity]} Severity: ${severity}
@@ -551,7 +557,7 @@ CAUSE:
551557
Client components can only fetch after mounting. Dependent fetches wait for
552558
the full React commit cycle.
553559
554-
FIX: Use Server Components
560+
FIX: Use Server Components with force-dynamic
555561
556562
EXAMPLE:
557563
@@ -581,8 +587,12 @@ EXAMPLE:
581587
)
582588
}
583589
584-
// AFTER: Server Component (✅ FAST)
590+
// AFTER: Server Component with force-dynamic (✅ FAST)
585591
// app/user/[userId]/page.tsx
592+
593+
// Add this directive at the top of the file to ensure dynamic rendering
594+
export const dynamic = 'force-dynamic';
595+
586596
export default async function UserPage({ params }) {
587597
const userPromise = fetch(\`/api/user/\${params.userId}\`)
588598
.then(r => r.json())
@@ -621,9 +631,15 @@ EXAMPLE:
621631
return posts.map(post => <Post key={post.id} post={post} />)
622632
}
623633
634+
IMPORTANT: When migrating to Server Components without cache components enabled:
635+
- Add \`export const dynamic = 'force-dynamic';\` at the top of your page file
636+
- This makes all fetches dynamic by default, so no need for \`{ cache: 'no-store' }\`
637+
624638
If you need client interactivity, fetch on server and pass promises:
625639
626640
// Server component
641+
export const dynamic = 'force-dynamic';
642+
627643
export default function UserPage({ params }) {
628644
const userPromise = fetch(\`/api/user/\${params.userId}\`).then(r => r.json())
629645
const postsPromise = userPromise.then(user =>
@@ -650,5 +666,186 @@ If you need client interactivity, fetch on server and pass promises:
650666
}
651667
652668
${'═'.repeat(80)}
653-
`)
669+
`
670+
} else {
671+
// When cache components ARE enabled, ensure top-level prerendering capability
672+
aiInsights = `
673+
${'─'.repeat(80)}
674+
675+
${severityEmoji[severity]} Severity: ${severity}
676+
Waterfall depth: ${maxDepth} level(s)
677+
Total fetches: ${totalFetches} (${parallelCount} parallel, ${totalFetches - parallelCount} serial)
678+
679+
PROBLEM:
680+
Client components fetch sequentially: fetch completes → commit → re-render → next fetch.
681+
Each level adds network latency to Time To Interactive.
682+
683+
CAUSE:
684+
Client components can only fetch after mounting. Dependent fetches wait for
685+
the full React commit cycle.
686+
687+
FIX: Use Server Components with Cache Components
688+
689+
With cache components enabled, you can optimize by moving data fetching to the server
690+
and ensuring the top-level of your page can be prerendered.
691+
692+
EXAMPLE:
693+
694+
// BEFORE: Client Component Waterfall (❌ SLOW)
695+
'use client'
696+
function UserProfile({ userId }) {
697+
const [user, setUser] = useState(null)
698+
const [posts, setPosts] = useState(null)
699+
700+
useEffect(() => {
701+
fetch(\`/api/user/\${userId}\`).then(r => r.json()).then(setUser)
702+
}, [userId])
703+
704+
useEffect(() => {
705+
if (user) { // ⚠️ Must wait for user before fetching posts
706+
fetch(\`/api/posts?userId=\${user.id}\`).then(r => r.json()).then(setPosts)
707+
}
708+
}, [user])
709+
710+
if (!user) return <Skeleton />
711+
return (
712+
<div>
713+
<h1>{user.name}</h1> {/* Static after user loads */}
714+
<Bio bio={user.bio} /> {/* Static after user loads */}
715+
{!posts ? <Spinner /> : <PostList posts={posts} />}
716+
</div>
717+
)
718+
}
719+
720+
// AFTER: Server Component with Cache Components (✅ FAST)
721+
// app/user/[userId]/page.tsx
722+
export default async function UserPage({ params }) {
723+
// These will be cached by default with cache components enabled
724+
const userPromise = fetch(\`/api/user/\${params.userId}\`)
725+
.then(r => r.json())
726+
727+
// Chain dependent data
728+
const postsPromise = userPromise.then(user =>
729+
fetch(\`/api/posts?userId=\${user.id}\`).then(r => r.json())
730+
)
731+
732+
return (
733+
<div>
734+
{/* Top-level content without I/O - enables prerendering */}
735+
<Header />
736+
<StaticSidebar />
737+
738+
{/* Dynamic content in Suspense boundaries */}
739+
<Suspense fallback={<Skeleton />}>
740+
<UserProfile userPromise={userPromise} postsPromise={postsPromise} />
741+
</Suspense>
742+
</div>
743+
)
744+
}
745+
746+
async function UserProfile({ userPromise, postsPromise }) {
747+
const user = await userPromise
748+
749+
return (
750+
<div>
751+
<h1>{user.name}</h1>
752+
<Bio bio={user.bio} />
753+
<Suspense fallback={<Spinner />}>
754+
<PostList postsPromise={postsPromise} />
755+
</Suspense>
756+
</div>
757+
)
758+
}
759+
760+
async function PostList({ postsPromise }) {
761+
const posts = await postsPromise
762+
return posts.map(post => <Post key={post.id} post={post} />)
763+
}
764+
765+
IMPORTANT: With cache components enabled:
766+
1. After removing all Suspense boundaries, the remaining shell must be synchronously renderable
767+
2. This means NO async/await outside Suspense - the static shell cannot wait for I/O
768+
3. All async data fetching must happen inside components wrapped by Suspense
769+
4. The static shell can only contain: JSX, props, synchronous computations
770+
771+
BAD (no static shell - blocks on I/O):
772+
export default async function Page() {
773+
const data = await fetch('/api/data').then(r => r.json()) // ❌ await blocks rendering
774+
return (
775+
<div>
776+
<Header />
777+
<div>{data.content}</div>
778+
</div>
779+
)
780+
}
781+
782+
ALSO BAD (async but no Suspense boundary):
783+
export default function Page() {
784+
const dataPromise = fetch('/api/data').then(r => r.json())
785+
786+
return (
787+
<div>
788+
<Header />
789+
<DynamicContent dataPromise={dataPromise} /> {/* ❌ No Suspense boundary */}
790+
</div>
791+
)
792+
}
793+
794+
GOOD (static shell with Suspense for async content):
795+
export default function Page() {
796+
return (
797+
<div>
798+
<Header /> {/* ✅ Static shell - renders immediately */}
799+
<Navigation />
800+
<Suspense fallback={<Loading />}>
801+
<DynamicContent /> {/* Async content inside Suspense */}
802+
</Suspense>
803+
</div>
804+
)
805+
}
806+
807+
async function DynamicContent() {
808+
const data = await fetch('/api/data').then(r => r.json())
809+
return <div>{data.content}</div>
810+
}
811+
812+
If you need client interactivity, fetch on server and pass promises:
813+
814+
// Server component
815+
export default function UserPage({ params }) {
816+
const userPromise = fetch(\`/api/user/\${params.userId}\`).then(r => r.json())
817+
const postsPromise = userPromise.then(user =>
818+
fetch(\`/api/posts?userId=\${user.id}\`).then(r => r.json())
819+
)
820+
821+
return (
822+
<div>
823+
<StaticHeader /> {/* ✅ Static top-level content */}
824+
<ClientProfile userPromise={userPromise} postsPromise={postsPromise} />
825+
</div>
826+
)
827+
}
828+
829+
'use client'
830+
import { use } from 'react'
831+
832+
export function ClientProfile({ userPromise, postsPromise }) {
833+
const user = use(userPromise)
834+
const [expanded, setExpanded] = useState(false)
835+
return (
836+
<div>
837+
<h1 onClick={() => setExpanded(!expanded)}>{user.name}</h1>
838+
{expanded && <Bio bio={user.bio} />}
839+
<Suspense fallback={<Spinner />}>
840+
<Posts postsPromise={postsPromise} />
841+
</Suspense>
842+
</div>
843+
)
844+
}
845+
846+
${'═'.repeat(80)}
847+
`
848+
}
849+
850+
console.log(aiInsights)
654851
}

packages/next/src/server/next-server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1326,7 +1326,12 @@ export default class NextNodeServer extends BaseServer<
13261326
const report = JSON.parse(body)
13271327
const { handleInsightsReport } =
13281328
require('./lib/insights-handler') as typeof import('./lib/insights-handler')
1329-
handleInsightsReport(report, this.distDir, this.dir)
1329+
handleInsightsReport(
1330+
report,
1331+
this.distDir,
1332+
this.dir,
1333+
this.nextConfig.cacheComponents ?? false
1334+
)
13301335
rawRes.statusCode = 200
13311336
rawRes.setHeader('Content-Type', 'application/json')
13321337
rawRes.end(JSON.stringify({ ok: true }))

0 commit comments

Comments
 (0)