Lazy Loading Vessels
A vessel’s component only matters once the engine decides to place it. This guide converts the eager greetingVessel registration into the lazy overload so its code stays out of your main bundle until then.
Why lazy-load
Section titled “Why lazy-load”Every vessel you pass to createMaelstrom() is a candidate the engine may or may not place on the Chart. With eager registration, each vessel’s component code ships in your initial bundle whether or not it ever renders. Lazy loading splits the component into its own chunk that the browser fetches only on first mount, so an app with many large or rarely shown vessels keeps its initial download small and pays for a vessel’s code only when the engine actually uses it.
The lazy overload
Section titled “The lazy overload”vessel() has two overloads. The eager form takes the component directly; the lazy form takes a loader and a loading component instead:
import { createMaelstrom, vessel } from '@maelstrom-co/react';import { greetingVessel } from './vessels/greeting/greeting.vessel';import GreetingLoading from './vessels/greeting/greeting.loading';
export const greetingEntry = vessel( greetingVessel, () => import('./vessels/greeting/greeting.component'), GreetingLoading,);
export const m = createMaelstrom({ vessels: [greetingEntry],});That’s the only change from Creating Vessels; greetingVessel and GreetingComponent are untouched. The third argument is what selects the lazy overload.
The loader matches LazyVesselComponentLoader<V>:
type LazyVesselComponentLoader<V> = () => Promise<{ default: ComponentType<TypedVesselProps<V>>;}>;A bare () => import('./greeting.component') satisfies this directly, which is why GreetingComponent is the module’s default export in Creating Vessels. The loader must resolve to a { default } shape; a named export won’t match the type, and the dynamic import won’t render.
The loading component receives { vesselId } and renders while the chunk is in flight. Keep it small:
import type { VesselId } from '@maelstrom-co/protocol';
export default function GreetingLoading({ vesselId }: { vesselId: VesselId }) { return <div style={{ padding: '1rem' }}>Loading {vesselId}…</div>;}What happens on mount
Section titled “What happens on mount”When the engine places the vessel, the Chart resolves it from the registry and drives the lazy flow:
| Stage | Behavior |
|---|---|
| Registration | The entry is stored as a lazy entry — its loader is kept aside rather than a ready component. |
| First mount | The Chart wraps the loader in React’s lazy() and renders it; the dynamic import fires now, not at registration. |
| While loading | Your loading component renders in place of the vessel until the chunk resolves. |
| Loaded | GreetingComponent mounts with the usual TypedVesselProps and replaces the loading component. |
The import runs once and the resolved chunk is reused for later mounts of the same vessel.
If the loader rejects (a network blip or a missing chunk), the Chart shows a load-error fallback with a retry affordance. Retrying produces a fresh lazy() instance, which busts the previously cached rejected promise so the loader runs again instead of replaying the failure.
When not to use
Section titled “When not to use”Do not lazy-load a tiny vessel that appears on every screen, such as a persistent status badge. In that case, the user pays an extra loading state and network round-trip for code they were going to need immediately anyway.
// Keep small, always-visible vessels eager.export const statusEntry = vessel(statusVessel, StatusBadge);Use the lazy overload for vessels whose component code is large enough, or rare enough, that delaying the chunk changes the initial page load.
When to use
Section titled “When to use”Reach for the lazy overload when a vessel is large, pulls in heavy dependencies (charts, editors, media), or is shown only in specific flows. Those are the cases where keeping the code out of the initial bundle pays off.
Leave a vessel eager when it’s small or almost always on screen. For an always-visible vessel, lazy loading just adds a loading flash and a second network round-trip without saving any meaningful bundle weight.
For the exact overload shapes, see vessel().
Next steps
Section titled “Next steps”- Error handling — handle
onErrorreporting for failed loads and other errors - Creating Vessels — the eager baseline this guide builds on