How to Handle Asynchronous Asset Loading with delayRender and continueRender in Remotion
Use delayRender() to block frame rendering until async assets like fonts, videos, or data finish loading, then call continueRender() with the returned handle to unblock the render pipeline.
When building videos with Remotion, components often need to load external assets asynchronously before the frame can be painted. Whether fetching data, decoding video frames, or loading custom fonts, the headless Chromium instance used by Remotion requires explicit synchronization. The delayRender and continueRender API in remotion-dev/remotion provides the mechanism to pause rendering until these asynchronous operations complete.
Understanding the delayRender API
The API consists of three core functions exported from the Remotion core:
delayRender(label?, options?): Returns a numeric handle and marks the current frame as not ready. Internally stores the handle inwindow.remotion_delayRenderHandlesand starts a default 30-second timeout.continueRender(handle): Clears the timeout for the given handle, removes it from the internal list, and setsremotion_renderReady = trueif no handles remain.cancelRender(error): Immediately aborts the render with the supplied error message.
Core Implementation Details
The implementation resides in packages/core/src/delay-render.ts. When delayRender() is invoked during rendering, it creates a timeout that is 2 seconds shorter than the user-provided or Puppeteer timeout, allowing Remotion to clean up before the browser kills the process. The system supports retry logic through constants like DELAY_RENDER_RETRIES_LEFT and DELAY_RENDER_RETRY_TOKEN.
The useDelayRender() hook, exported from packages/core/src/index.ts, provides convenient access to these functions within React components, returning {delayRender, continueRender, cancelRender} bound to the current global scope.
Practical Implementation Examples
Loading Custom Fonts
When loading custom fonts in templates like the TikTok template, you must block rendering until the font is available:
// packages/template-tiktok/src/load-font.ts
import { continueRender, delayRender, staticFile } from "remotion";
let loaded = false;
export const loadFont = async (): Promise<void> => {
if (loaded) return;
const waitForFont = delayRender("Loading custom font");
loaded = true;
const font = new FontFace(
"TheBoldFont",
`url('${staticFile("theboldfont.ttf")}') format('truetype')`
);
await font.load();
document.fonts.add(font);
continueRender(waitForFont);
};
Video Textures in React Three Fiber
For Three.js video textures, use the useDelayRender hook to synchronize video loading:
// packages/three/src/use-video-texture.ts
import { useDelayRender } from "remotion";
export const useVideoTexture = (videoRef) => {
const { delayRender, continueRender, cancelRender } = useDelayRender();
const loadHandle = delayRender(
"Waiting for texture in useVideoTexture() to be loaded"
);
const onReady = () => {
const vt = new VideoTexture(videoRef.current);
setVideoTexture(vt);
continueRender(loadHandle);
};
const onError = (error) => {
cancelRender(error);
};
// Setup video element and event listeners...
};
Off-Thread Video with Retries and Custom Timeouts
For complex assets like off-thread video frames, configure retries and per-asset timeouts:
// packages/three/src/use-offthread-video-texture.ts
const fetchTexture = useCallback(() => {
const handle = delayRender("fetch offthread video frame", {
retries: delayRenderRetries,
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds,
});
loader
.loadAsync(offthreadVideoFrameSrc)
.then((texture) => {
setImageTexture(texture);
continueRender(handle);
})
.catch((err) => cancelRender(err));
}, [offthreadVideoFrameSrc, delayRenderRetries, delayRenderTimeoutInMilliseconds]);
Error Handling and Timeout Management
The delayRender API includes robust timeout handling to prevent indefinite hangs. By default, each handle has a 30-second timeout, though this adjusts dynamically during rendering to be 2 seconds shorter than the Puppeteer timeout. If continueRender is not called within this window, the render fails with a timeout error.
For error scenarios, cancelRender(error) immediately terminates the render and propagates the error to the CLI. This is essential when async operations like network requests or video decoding fail, ensuring that broken assets don't produce silent blank frames.
Summary
- Use
delayRender()to block frame rendering when loading asynchronous assets like fonts, videos, or external data. - Call
continueRender(handle)after the asset loads successfully to unblock the render pipeline. - Handle failures with
cancelRender(error)to abort rendering when async operations fail. - Configure timeouts and retries via the options parameter for complex assets requiring multiple attempts.
- Reference the core implementation in
packages/core/src/delay-render.tsand use theuseDelayRender()hook frompackages/core/src/index.tsfor React integration.
Frequently Asked Questions
What happens if I forget to call continueRender?
If you call delayRender() but never invoke continueRender() with the returned handle, the render will timeout after the default 30 seconds (or your custom timeout) and fail with an error message indicating which label was never resolved.
Can I use multiple delayRender calls in the same component?
Yes. Each call to delayRender() returns a unique numeric handle stored in the global window.remotion_delayRenderHandles map. The render remains blocked until continueRender() is called for every outstanding handle, allowing you to load multiple assets in parallel.
How do I set a custom timeout for a specific asset?
Pass a timeoutInMilliseconds option to delayRender(). For example: delayRender("Loading video", { timeoutInMilliseconds: 60000 }) sets a 60-second timeout for that specific asset, overriding the default 30-second limit.
What is the difference between delayRender and useDelayRender?
delayRender and continueRender are the core functions that manage the render state. useDelayRender() is a React hook exported from remotion that simply returns these functions bound to the current global scope, making them convenient to use within functional components without manual window references.
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 →