When using defer() to split RSC payloads, content is fetched on-demand as it renders. But what if you want to start fetching before the user actually needs it? By combining defer() with React 19's <Activity> component, you can prefetch deferred payloads in the background so they're ready instantly when revealed.
<Activity> is a React 19 component that controls whether its children are visible or hidden:
import { Activity } from "react";
<Activity mode="visible">
<Panel /> {/* Rendered and visible */}
</Activity>
<Activity mode="hidden">
<Panel /> {/* Rendered but not visible in the DOM */}
</Activity>
When mode is "hidden", React still renders the children, but the output is hidden from the user and effects are not run until the mode becomes "visible". The hidden content is rendered at a lower priority so it doesn't affect the performance of visible content. This is useful for keeping off-screen UI alive (preserving state, pre-warming components) without showing it.
Recall that defer() wraps a server component so its RSC payload is fetched separately from the main payload. The fetch starts when the deferred component renders on the client.
The key insight is: rendering under <Activity mode="hidden"> still triggers the fetch. The deferred component renders (starting the network request for its RSC payload), but the result isn't displayed. When you later switch the Activity to "visible", the content appears immediately because the payload has already been downloaded.
<Activity mode="hidden"> renders defer()'ed content<Activity mode="visible"> — content shows instantly, no loading stateConsider a tabbed interface where each tab contains heavy server-rendered content. Without prefetching, switching tabs shows a loading spinner while the payload is fetched. With <Activity>, you can prefetch all tabs on initial load.
"use client";
import { Activity, Suspense, useState } from "react";
export function Tabs({
tabs,
}: {
tabs: { label: string; content: React.ReactNode }[];
}) {
const [activeIndex, setActiveIndex] = useState(0);
return (
<div>
<div role="tablist">
{tabs.map((tab, i) => (
<button
key={i}
role="tab"
aria-selected={i === activeIndex}
onClick={() => setActiveIndex(i)}
>
{tab.label}
</button>
))}
</div>
{tabs.map((tab, i) => (
<Activity key={i} mode={i === activeIndex ? "visible" : "hidden"}>
<div role="tabpanel">
<Suspense fallback={<p>Loading...</p>}>{tab.content}</Suspense>
</div>
</Activity>
))}
</div>
);
}
On the server side, each tab's content is wrapped with defer():
import { defer } from "@funstack/static/server";
import { Tabs } from "./Tabs";
import Overview from "./tabs/Overview";
import Specifications from "./tabs/Specifications";
import Reviews from "./tabs/Reviews";
function ProductPage() {
return (
<Tabs
tabs={[
{
label: "Overview",
content: defer(<Overview />, { name: "Overview" }),
},
{
label: "Specifications",
content: defer(<Specifications />, { name: "Specifications" }),
},
{
label: "Reviews",
content: defer(<Reviews />, { name: "Reviews" }),
},
]}
/>
);
}
When this page loads:
<Activity mode="hidden">, triggering their payload fetches in the backgroundAnother common pattern is pre-fetching content inside a collapsible section:
"use client";
import { Activity, Suspense, useState } from "react";
export function Collapsible({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>{title}</button>
<Activity mode={isOpen ? "visible" : "hidden"}>
<Suspense fallback={<p>Loading...</p>}>{children}</Suspense>
</Activity>
</div>
);
}
import { defer } from "@funstack/static/server";
import { Collapsible } from "./Collapsible";
import DetailedSpecs from "./DetailedSpecs";
function Product() {
return (
<Collapsible title="Show detailed specifications">
{defer(<DetailedSpecs />, { name: "DetailedSpecs" })}
</Collapsible>
);
}
Even though the section starts collapsed, <Activity mode="hidden"> causes the deferred content to start fetching immediately. When the user expands the section, the content is already there.
This pattern works best when:
Avoid this pattern when:
defer() to split RSC payloads