Lazy-load components
Lazy-load a component when its code is large and most compositions don't use it. Studio still lists it in the Components tab, but its code downloads only the first time a composition renders it.
The example on this page is the team blog. The AI 101 post at /blog/ai-101 shows code samples through a CodeBlock component in src/components/CodeBlock.tsx, which pulls in a syntax-highlighting library. BlogHero and Avatar appear on every post, so they stay eager.
Prerequisites
CodeBlockregistered the normal way and rendering on the canvas. See Register a component. Get it working eagerly first, so any problem you hit after the switch is about loading and not about the component.CodeBlockexported by name from its own file.
What this is and what you get
registerLazyComponent takes the same config as registerComponent, without component. In its place you pass a loader that imports the file. Your bundler moves CodeBlock and its syntax highlighter into a separate file, and that file downloads the first time a composition renders CodeBlock. Visitors who read a post with no code sample never download it.
It's worth doing for a component with a heavy dependency, such as a chart, a map, a video player, a code editor or a syntax highlighter, or for a component few compositions use. A small component that's on every composition gains nothing.
Register a component lazily
-
In your registration module, replace the
CodeBlockentry inregisterComponentswith aregisterLazyComponentcall.import { registerLazyComponent } from "@contentstack/studio-react";registerLazyComponent({type: "code-block",displayName: "Code block",wrap: false,props: {code: { type: "string", displayName: "Code" },language: {type: "choice",displayName: "Language",options: ["python", "javascript", "bash"],defaultValue: "python",},},},() => import("./components/CodeBlock").then((m) => m.CodeBlock),);The loader has to resolve to the component, not the module.
import()gives you the module, so pick the export off it with.then((m) => m.CodeBlock). For a default export, write.then((m) => m.default). -
Keep the component itself unchanged. With
wrap: false, it spreadsstudioAttributeson its root element. This short version leaves out the highlighter.import type { StudioAttributes } from "@contentstack/studio-react";export type CodeBlockProps = StudioAttributes & { code: string; language: string };export function CodeBlock({ code, language, studioAttributes }: CodeBlockProps) {return (<pre {...studioAttributes}><code className={`language-${language}`}>{code}</code></pre>);} -
Remove any other static import of
CodeBlock, such asimport { CodeBlock } from "./components/CodeBlock"in a route file. A static import puts the component back in that file's bundle, and no separate file is created. -
Open a template on the canvas, open your browser's developer tools on the Network tab, and drag Code block from the Components tab onto the canvas. A new JavaScript file downloads as the component appears, and not before.
For the full signature, see SDK API.
What loads up front and what waits
Registration runs as soon as your registration module loads, lazy or not. Studio gets the type, display name and props straight away, so Code block is in the Components tab before any of its code has downloaded.
The code downloads the first time something renders the component:
- On the canvas, when an author drops Code block, or opens a composition that already has one.
- On your site, when a visitor opens a composition that uses it, such as AI 101.
Once the code has downloaded, later renders on that page use it without downloading it again.
Customise the fallback
While the code downloads, a lazy component renders nothing. There's no option on registerLazyComponent to change that, and a Suspense boundary in your app shell doesn't help, because the SDK wraps every registered component in its own boundary with an empty fallback.
To show a placeholder, do the lazy loading yourself and register the result with registerComponent:
import { lazy, Suspense } from "react";
import { registerComponent } from "@contentstack/studio-react";
import { CodeBlockSkeleton } from "./components/CodeBlockSkeleton";
import type { CodeBlockProps } from "./components/CodeBlock";
const CodeBlockLazy = lazy(() =>
import("./components/CodeBlock").then((m) => ({ default: m.CodeBlock })),
);
function CodeBlockWithSkeleton(props: CodeBlockProps) {
return (
<Suspense fallback={<CodeBlockSkeleton />}>
<CodeBlockLazy {...props} />
</Suspense>
);
}
registerComponent({
type: "code-block",
displayName: "Code block",
component: CodeBlockWithSkeleton,
wrap: false,
props: {
code: { type: "string", displayName: "Code" },
language: {
type: "choice",
displayName: "Language",
options: ["python", "javascript", "bash"],
defaultValue: "python",
},
},
});
The wrapper has to declare its props parameter. Studio treats a component function that declares no parameters as a loader, so a wrapper written as () => <Suspense>...</Suspense> fails with the element type error under If it doesn't work. lazy needs a module with a default export, which is why the example maps m.CodeBlock to default. The type-only import of CodeBlockProps doesn't pull CodeBlock into the bundle.
CodeBlock gets studioAttributes through the spread props, so it keeps working with wrap: false.
You can also pass a React.lazy or next/dynamic component to registerComponent as component, with no wrapper. Its own boundary keeps the loading state to that component, so the rest of the page stays on screen. A React.lazy component renders nothing while it loads, so keep the wrapper above if you want a placeholder.
Mixing eager and lazy
You can register some components eagerly and others lazily in the same app. Put them in one registration module: a registerComponents call for the eager ones and a registerLazyComponent call for each lazy one.
For the blog, BlogHero and Avatar are small and on every post, so they stay in registerComponents. CodeBlock is heavy and only on some posts, so it's lazy. A good default is eager for anything small or on most compositions, and lazy for anything that pulls in a chart, map, video player, editor or highlighter library.
Keep every component registered, whichever way. Lazy loading changes when a component's code downloads, not whether Studio knows the component. If you leave a component unregistered, no composition can render it.
Lazy components on server-rendered routes
A lazy component waits for its code in a React Suspense boundary. Whether the server waits too depends on the React function your server renders with.
renderToStringdoesn't wait. If the server hasn't loadedCodeBlock's code yet, such as on the first request after the server starts, the server HTML has nothing whereCodeBlockgoes. The code sample appears once the browser loads the component.renderToPipeableStreamandrenderToReadableStreamsupportSuspense, and can wait for the code before they send the component's HTML.
If your server uses renderToString and you need a component in the server HTML, for search engines or for the first paint, register it eagerly. For how Studio renders on the server, see Render on the server.
If it doesn't work
The component is blank for a moment after you drop it
That's the empty fallback while its code downloads. To show a placeholder instead, register your own wrapper, as in Customise the fallback.
The component doesn't render, and the console says the element type is invalid
In a development build, the console shows this message from React:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Studio got something other than a component where it expected one. Check two things:
- The loader resolves to the module. Change
() => import("./components/CodeBlock")to() => import("./components/CodeBlock").then((m) => m.CodeBlock). - Your own wrapper takes no parameters. Declare
propsand spread it onto the lazy component, as in Customise the fallback.
No separate file downloads when the component first renders
Another file imports CodeBlock statically, so your bundler put it in that file's bundle. Search your app for from "./components/CodeBlock" and remove every import of the component except the one in the loader. A type-only import, such as import type { CodeBlockProps }, is fine.
The component is missing from the server HTML and appears after the page loads
Your server renders with renderToString, which doesn't wait for lazy code. Register that component eagerly, or render with renderToPipeableStream or renderToReadableStream. See Lazy components on server-rendered routes.