| 1 | --- |
| 2 | title: Strategic Suspense Boundaries |
| 3 | impact: HIGH |
| 4 | impactDescription: faster initial paint |
| 5 | tags: async, suspense, streaming, layout-shift |
| 6 | --- |
| 7 | |
| 8 | ## Strategic Suspense Boundaries |
| 9 | |
| 10 | Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads. |
| 11 | |
| 12 | **Incorrect (wrapper blocked by data fetching):** |
| 13 | |
| 14 | ```tsx |
| 15 | async function Page() { |
| 16 | const data = await fetchData() // Blocks entire page |
| 17 | |
| 18 | return ( |
| 19 | <div> |
| 20 | <div>Sidebar</div> |
| 21 | <div>Header</div> |
| 22 | <div> |
| 23 | <DataDisplay data={data} /> |
| 24 | </div> |
| 25 | <div>Footer</div> |
| 26 | </div> |
| 27 | ) |
| 28 | } |
| 29 | ``` |
| 30 | |
| 31 | The entire layout waits for data even though only the middle section needs it. |
| 32 | |
| 33 | **Correct (wrapper shows immediately, data streams in):** |
| 34 | |
| 35 | ```tsx |
| 36 | function Page() { |
| 37 | return ( |
| 38 | <div> |
| 39 | <div>Sidebar</div> |
| 40 | <div>Header</div> |
| 41 | <div> |
| 42 | <Suspense fallback={<Skeleton />}> |
| 43 | <DataDisplay /> |
| 44 | </Suspense> |
| 45 | </div> |
| 46 | <div>Footer</div> |
| 47 | </div> |
| 48 | ) |
| 49 | } |
| 50 | |
| 51 | async function DataDisplay() { |
| 52 | const data = await fetchData() // Only blocks this component |
| 53 | return <div>{data.content}</div> |
| 54 | } |
| 55 | ``` |
| 56 | |
| 57 | Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data. |
| 58 | |
| 59 | **Alternative (share promise across components):** |
| 60 | |
| 61 | ```tsx |
| 62 | function Page() { |
| 63 | // Start fetch immediately, but don't await |
| 64 | const dataPromise = fetchData() |
| 65 | |
| 66 | return ( |
| 67 | <div> |
| 68 | <div>Sidebar</div> |
| 69 | <div>Header</div> |
| 70 | <Suspense fallback={<Skeleton />}> |
| 71 | <DataDisplay dataPromise={dataPromise} /> |
| 72 | <DataSummary dataPromise={dataPromise} /> |
| 73 | </Suspense> |
| 74 | <div>Footer</div> |
| 75 | </div> |
| 76 | ) |
| 77 | } |
| 78 | |
| 79 | function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) { |
| 80 | const data = use(dataPromise) // Unwraps the promise |
| 81 | return <div>{data.content}</div> |
| 82 | } |
| 83 | |
| 84 | function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) { |
| 85 | const data = use(dataPromise) // Reuses the same promise |
| 86 | return <div>{data.summary}</div> |
| 87 | } |
| 88 | ``` |
| 89 | |
| 90 | Both components share the same promise, so only one fetch occurs. Layout renders immediately while both components wait together. |
| 91 | |
| 92 | **When NOT to use this pattern:** |
| 93 | |
| 94 | - Critical data needed for layout decisions (affects positioning) |
| 95 | - SEO-critical content above the fold |
| 96 | - Small, fast queries where suspense overhead isn't worth it |
| 97 | - When you want to avoid layout shift (loading → content jump) |
| 98 | |
| 99 | **Trade-off:** Faster initial paint vs potential layout shift. Choose based on your UX priorities. |
| 100 |