@@ -179,7 +179,8 @@ function filterUserFrames(frames: StackFrame[]): StackFrame[] {
179179export 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
556562EXAMPLE:
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+
624638If 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}
0 commit comments