Async & Suspense Patterns
In Remotion, managing asynchronous operations is critical for ensuring render consistency. Because the rendering engine captures frames deterministically, any "floating" promises or side effects can lead to flickering, missing assets, or mismatched durations.
Data Fetching with calculateMetadata
The primary pattern for non-blocking asynchronous data fetching is the calculateMetadata function. This function executes before the composition starts rendering, allowing you to fetch external data, determine video dimensions, or set the composition duration based on remote assets.
Basic Implementation
Use calculateMetadata to transform props or set composition properties dynamically.
import { Composition, CalculateMetadataFunction } from "remotion";
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
abortSignal,
}) => {
// Fetch remote data before rendering begins
const response = await fetch(`https://api.example.com/data/${props.id}`, {
signal: abortSignal,
});
const data = await response.json();
return {
durationInFrames: data.duration * 30,
props: {
...props,
fetchedData: data,
},
};
};
export const RemotionRoot = () => (
<Composition
id="MyComp"
component={MyComponent}
durationInFrames={1} // Placeholder
fps={30}
width={1080}
height={1080}
calculateMetadata={calculateMetadata}
/>
);
Parallel Fetching
To optimize performance, especially when dealing with multiple assets, use Promise.all within calculateMetadata. This ensures all dependencies are resolved in parallel before the render pipeline starts.
const calculateMetadata: CalculateMetadataFunction<Props> = async ({ props }) => {
const [videoMeta, themeData] = await Promise.all([
getMediaMetadata(props.videoUrl),
fetch(props.themeUrl).then(res => res.json())
]);
return {
durationInFrames: Math.ceil(videoMeta.durationInSeconds * 30),
props: {
...props,
theme: themeData,
}
};
};
Suspense and Media Loading
Remotion components such as <Img />, <Video />, and <Audio /> (from @remotion/media) are "Suspense-aware." They automatically notify the Remotion engine when they are loading.
- Non-blocking Rendering: The engine will not attempt to capture a frame until these components have resolved their internal promises.
- Deduplication: Repeatedly using the same source URL across multiple components is handled efficiently, though manual deduping via
calculateMetadatais preferred for heavy data objects.
Usage with @remotion/media
import { Video } from "@remotion/media";
import { staticFile } from "remotion";
export const MyScene = () => {
// This component will pause the render until 'clip.mp4' is ready
return <Video src={staticFile("clip.mp4")} />;
};
SWR and Request Deduping Patterns
When working within the Remotion Studio, props can change rapidly as you scrub the timeline or adjust inputs. To prevent "waterfalls" or stale data, Remotion provides an abortSignal within the calculateMetadata context.
Handling Stale Requests
Always pass the abortSignal to your fetch requests. This ensures that if the user changes a prop, the previous (now stale) request is cancelled, preventing race conditions and unnecessary network overhead.
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
abortSignal,
}) => {
const res = await fetch(props.apiUrl, { signal: abortSignal });
const data = await res.json();
return { props: { ...props, data } };
};
Non-Blocking Server Actions
For operations that involve heavy processing (like transcribing audio or extracting frames), perform these actions outside the main render loop.
Pre-processing with Mediabunny
If you need to extract frames from a video to display a filmstrip or thumbnail, use the extractFrames pattern. This should typically be triggered by a user action or during a pre-render step rather than inside the component's useCurrentFrame() loop.
// Pattern: Extracting frames asynchronously outside the render
useEffect(() => {
const controller = new AbortController();
extractFrames({
src: videoUrl,
timestampsInSeconds: [1, 2, 3],
onVideoSample: (sample) => {
// Handle the sample (e.g., draw to a canvas)
},
signal: controller.signal
});
return () => controller.abort();
}, [videoUrl]);
Rules for Async Code
To maintain render determinism, follow these constraints:
- No side effects in Render: Never trigger a
fetchorPromisedirectly inside the component body or auseFrame()callback. - Use
calculateMetadatafor logic: If the duration, dimensions, or props depend on an async result, they must be resolved incalculateMetadata. - Asset References: Always wrap local asset paths in
staticFile()to ensure they are correctly resolved across different deployment environments (e.g., Lambda vs. Local).