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:

  1. Handle Generation: The function generates a random identifier and pushes it onto scope.remotion_delayRenderHandles (lines 71-73).
  2. Render Blocking: It immediately sets scope.remotion_renderReady = false, signaling the renderer that the current frame is not ready for capture (lines 109-111).
  3. Timeout Management: When running in headless environments, an optional timeout begins. If the handle isn't cleared before expiration, cancelRenderInternal aborts the render with a descriptive error (lines 84-106).
  4. Completion Handling: Calling continueRender(handle) clears the timeout, removes the handle from the list, and sets remotion_renderReady back to true if 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:

Summary

  • delayRender() creates a unique handle that pauses frame rendering until explicitly cleared, storing state in window.remotion_delayRenderHandles and setting remotion_renderReady to false.
  • continueRender(handle) removes the specific handle from the pending list and resumes rendering when no handles remain, while cancelRender(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/three compositions.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →