Using delayRender for Async Operations in Remotion Compositions
Use delayRender() to pause frame rendering until asynchronous tasks like data fetching, font loading, or texture preparation complete, then unblock with continueRender() or abort with cancelRender().
The delayRender API is the standard mechanism in the remotion-dev/remotion repository for handling asynchronous operations within video compositions. When you need to fetch external data, load custom fonts, or prepare WebGL textures before a frame can be rendered, this API ensures the renderer waits for completion rather than outputting incomplete frames.
How delayRender Works Internally
When you invoke delayRender(), the function creates a unique handle and registers it in the global render state. According to the source code in packages/core/src/delay-render.ts, the implementation follows this sequence:
- Handle Generation: The function generates a random identifier and pushes it onto
scope.remotion_delayRenderHandles(lines 71-73). - Render Blocking: It immediately sets
scope.remotion_renderReady = false, signaling the renderer that the current frame is not ready for capture (lines 109-111). - Timeout Management: When running in headless environments, an optional timeout begins. If the handle isn't cleared before expiration,
cancelRenderInternalaborts the render with a descriptive error (lines 84-106). - Completion Handling: Calling
continueRender(handle)clears the timeout, removes the handle from the list, and setsremotion_renderReadyback totrueif no other handles remain (lines 62-94).
The useDelayRender() hook in packages/core/src/use-delay-render.tsx wraps these internal functions, exposing delayRender, continueRender, and cancelRender through React context (lines 17-21).
Implementing Async Workflows with delayRender
The standard pattern involves creating a handle at the start of an async operation, performing the work, then signaling completion or failure. This pattern applies whether you are fetching JSON, loading binary assets, or initializing WebGL contexts.
Fetching Data Before Frame Rendering
When external data determines visual output, use the useDelayRender hook to block rendering until the fetch completes:
import {useEffect, useState} from 'react';
import {useDelayRender} from 'remotion';
export const DataDrivenComposition = () => {
const {delayRender, continueRender, cancelRender} = useDelayRender();
const [data, setData] = useState<any>(null);
// Initialize handle once per component lifecycle
const [handle] = useState(() => delayRender('Fetching API data'));
useEffect(() => {
fetch('https://api.example.com/data')
.then((res) => res.json())
.then((json) => {
setData(json);
continueRender(handle); // Unblock rendering
})
.catch((err) => {
cancelRender(err); // Abort with error
});
}, [handle, continueRender, cancelRender]);
// Frame rendering pauses here until continueRender executes
return <div>{data ? data.title : 'Loading...'}</div>;
};
Critical implementation detail: Create the handle using useState(() => delayRender()) rather than calling delayRender() directly in the render body. This prevents creating multiple handles during React re-renders.
Loading Custom Fonts
For typography that isn't system-default, fonts must load before text measurement and rendering occur. The remotion-dev/remotion repository includes this pattern in packages/template-react-router/app/remotion/load-fonts.ts:
import {delayRender, continueRender, staticFile} from 'remotion';
export const loadCustomFont = async () => {
const handle = delayRender('Loading custom font');
try {
const fontUrl = staticFile('fonts/CustomFont.woff2');
const fontFace = new FontFace('CustomFont', `url(${fontUrl})`);
await fontFace.load();
document.fonts.add(fontFace);
continueRender(handle);
} catch (error) {
cancelRender(error);
}
};
Invoke this loader in your composition's root or within a useEffect to ensure fonts are available before the first frame renders.
Preparing Video Textures for Three.js
When integrating @remotion/three, video textures require asynchronous initialization. The implementation in packages/three/src/use-video-texture.ts demonstrates blocking render until the video element is ready:
import {useDelayRender} from 'remotion';
import {useRef, useState} from 'react';
export const useVideoTexture = (src: string) => {
const {delayRender, continueRender, cancelRender} = useDelayRender();
const videoRef = useRef<HTMLVideoElement>(null);
const [texture, setTexture] = useState(null);
const [handle] = useState(() => delayRender('Video texture loading'));
const initializeVideo = async () => {
try {
const {VideoTexture} = await import('three/src/textures/VideoTexture.js');
const video = videoRef.current;
if (video) {
const tex = new VideoTexture(video);
setTexture(tex);
continueRender(handle);
}
} catch (error) {
cancelRender(error);
}
};
return {videoRef, texture, initializeVideo};
};
This pattern ensures the Three.js canvas only renders after the video texture is fully initialized, preventing black frames or incomplete WebGL scenes.
Configuring Timeouts and Retry Logic
The delayRender function accepts an optional configuration object for timeout handling. When rendering in headless environments, this prevents indefinite hangs:
const handle = delayRender('Fetching with retry logic', {
retries: 3,
timeoutInMilliseconds: 15000
});
According to the source in packages/core/src/delay-render.ts (lines 80-90), when the timeout expires, Remotion automatically retries the render up to the specified number of attempts. If all retries exhaust, the render fails with an error message identifying the specific handle that caused the timeout.
Key Source Files in the Remotion Repository
Understanding the implementation requires familiarity with these specific files in the remotion-dev/remotion repository:
packages/core/src/delay-render.ts– Core implementation containingdelayRenderInternal,continueRenderInternal, and timeout management logic.packages/core/src/use-delay-render.tsx– React hook wrapping the core functions for component-level access via context.packages/core/src/cancel-render.ts– Error handling logic for aborting renders when async operations fail.packages/three/src/use-video-texture.ts– Real-world example of texture loading withdelayRender.packages/template-react-router/app/remotion/load-fonts.ts– Font loading implementation demonstrating the async pattern.
Summary
delayRender()creates a unique handle that pauses frame rendering until explicitly cleared, storing state inwindow.remotion_delayRenderHandlesand settingremotion_renderReadytofalse.continueRender(handle)removes the specific handle from the pending list and resumes rendering when no handles remain, whilecancelRender(error)aborts the entire render with the provided error.- The
useDelayRender()hook exposes these functions through React context, allowing components to manage async state without accessing global window objects directly. - Timeout and retry configuration via
{retries: n, timeoutInMilliseconds: ms}prevents indefinite hangs during headless rendering, automatically retrying failed attempts before throwing. - Common use cases include API data fetching, custom font loading via
FontFace, and WebGL texture preparation in@remotion/threecompositions.
Frequently Asked Questions
What happens if I forget to call continueRender?
If continueRender() is never invoked for a given handle, the render will hang indefinitely in development mode. In headless rendering environments, the timeout mechanism will eventually trigger after the default or specified timeoutInMilliseconds, throwing an error that identifies the specific handle that remained open. Always ensure continueRender or cancelRender executes in both success and error paths of your async operations.
Can I use multiple delayRender handles in one component?
Yes. You can call delayRender() multiple times to create independent handles for different asynchronous operations. The renderer will wait until all handles have been cleared via continueRender() before marking the frame as ready. This is useful when fetching multiple independent data sources or loading several assets concurrently, as each operation can resolve at its own pace while the frame remains blocked.
How does delayRender handle timeouts and retries?
When you provide the timeoutInMilliseconds option, Remotion schedules a timer that checks if the handle still exists after the specified duration. If the handle persists, the render fails and automatically retries up to the number specified in the retries option (defaulting to 0). Each retry creates a fresh render attempt, and the timeout applies to each individual attempt. This logic is implemented in packages/core/src/delay-render.ts lines 80-106.
Is useDelayRender required or can I use the global functions directly?
While you can import and use delayRender, continueRender, and cancelRender directly from 'remotion', the useDelayRender() hook is the recommended approach for React components. The hook provides the same functions through React context, ensuring better testability and avoiding direct global window object access. Both approaches ultimately interact with the same internal state in window.remotion_delayRenderHandles, but the hook offers a more idiomatic React pattern that works seamlessly with the component lifecycle.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →