|
| 1 | +# Optimizing RSC Payloads |
| 2 | + |
| 3 | +FUNSTACK Static uses React Server Components (RSC) to pre-render your application at build time. By default, all content is bundled into a single RSC payload. This page explains how to split that payload into smaller chunks for better loading performance. |
| 4 | + |
| 5 | +## The Default Behavior |
| 6 | + |
| 7 | +Without any optimization, your entire application is rendered into one RSC payload file: |
| 8 | + |
| 9 | +``` |
| 10 | +dist/public/funstack__/ |
| 11 | +└── fun:rsc-payload/ |
| 12 | + └── b62ec6668fd49300.txt ← Contains everything |
| 13 | +``` |
| 14 | + |
| 15 | +This means users must download the entire payload before seeing any content - even if they only need one page of a multi-page application. |
| 16 | + |
| 17 | +**Note:** RSC payloads contain the rendering results of server components only; client components and their JavaScript bundles are handled separately. However, it is still important to optimize RSC payload sizes because the recommended best practice is to keep as much of your UI as server components as possible. |
| 18 | + |
| 19 | +## Chunking with defer() |
| 20 | + |
| 21 | +The `defer()` function lets you split your application into multiple RSC payloads. Each `defer()` call creates a separate payload file that loads on-demand when that component renders. |
| 22 | + |
| 23 | +```tsx |
| 24 | +import { defer } from "@funstack/static/server"; |
| 25 | + |
| 26 | +// Instead of this: |
| 27 | +<HeavyContent /> |
| 28 | + |
| 29 | +// Do this: |
| 30 | +<Suspense fallback={<p>Loading...</p>}> |
| 31 | + {defer(<HeavyContent />)} |
| 32 | +</Suspense> |
| 33 | +``` |
| 34 | + |
| 35 | +The content inside `defer()` is still rendered at build time, but it's stored in a separate file and fetched only when needed. |
| 36 | + |
| 37 | +**Note:** use of `defer()` requires a `<Suspense>` boundary to handle the loading state while the payload is being fetched. |
| 38 | + |
| 39 | +## Route-Level Optimization |
| 40 | + |
| 41 | +The most impactful use of `defer()` is wrapping your route components. This ensures users only download the payload for the page they're viewing: |
| 42 | + |
| 43 | +```tsx |
| 44 | +import { defer } from "@funstack/static/server"; |
| 45 | +import { route } from "@funstack/router/server"; |
| 46 | +import HomePage from "./pages/Home"; |
| 47 | +import AboutPage from "./pages/About"; |
| 48 | +import DocsPage from "./pages/Docs"; |
| 49 | + |
| 50 | +const routes = [ |
| 51 | + route({ |
| 52 | + path: "/", |
| 53 | + component: defer(<HomePage />), |
| 54 | + }), |
| 55 | + route({ |
| 56 | + path: "/about", |
| 57 | + component: defer(<AboutPage />), |
| 58 | + }), |
| 59 | + route({ |
| 60 | + path: "/docs", |
| 61 | + component: defer(<DocsPage />), |
| 62 | + }), |
| 63 | +]; |
| 64 | +``` |
| 65 | + |
| 66 | +With this setup: |
| 67 | + |
| 68 | +- Visiting `/` only downloads the Home page payload |
| 69 | +- Navigating to `/about` fetches the About page payload on-demand |
| 70 | +- Users see content faster because they're not waiting for pages they haven't visited |
| 71 | + |
| 72 | +## Output Structure |
| 73 | + |
| 74 | +After building with route-level `defer()`, your output looks like this: |
| 75 | + |
| 76 | +``` |
| 77 | +dist/public/funstack__/ |
| 78 | +└── fun:rsc-payload/ |
| 79 | + ├── a3f2b1c9d8e7f6a5.txt ← Home page |
| 80 | + ├── b5698be72eea3c37.txt ← About page |
| 81 | + ├── b62ec6668fd49300.txt ← Main app shell |
| 82 | + └── c7d8e9f0a1b2c3d4.txt ← Docs page |
| 83 | +``` |
| 84 | + |
| 85 | +Each payload file has a **content-based hash** in its filename. This enables aggressive browser caching - the file only changes when its content changes. |
| 86 | + |
| 87 | +**Note:** during development, UUIDs are used instead of content hashes for faster rebuilds. Content hashes are applied during production builds. |
| 88 | + |
| 89 | +## Best Practices |
| 90 | + |
| 91 | +**Always wrap route components** - This is the single most important optimization. It prevents users from downloading content for pages they may never visit. |
| 92 | + |
| 93 | +**Use Suspense boundaries** - Every `defer()` call must be inside a `<Suspense>` boundary. The fallback is shown while the payload is being fetched. |
| 94 | + |
| 95 | +```tsx |
| 96 | +<Suspense fallback={<PageSkeleton />}>{defer(<PageContent />)}</Suspense> |
| 97 | +``` |
| 98 | + |
| 99 | +**Consider below-the-fold content** - Content hidden in collapsed sections, tabs, or modals is a good candidate for `defer()` since users may never need it. |
| 100 | + |
| 101 | +## See Also |
| 102 | + |
| 103 | +- [defer()](/funstack-static/api/defer) - API reference with full signature and technical details |
| 104 | +- [How It Works](/funstack-static/learn/how-it-works) - Overall FUNSTACK Static architecture |
| 105 | +- [React Server Components](/funstack-static/learn/rsc) - Understanding RSC fundamentals |
0 commit comments