R RenderComp
remotion developer-workflow debugging productivity tips

Remotion Developer Workflow: Tips, Debugging, and Productivity

Remotion Developer Workflow: Tips, Debugging, and Productivity

Remotion’s developer experience is genuinely good — but like any specialized tool, it rewards developers who understand its model deeply. The gap between “I can make videos” and “I can make videos efficiently” often comes down to a handful of workflow habits: knowing which tool to reach for at each stage, understanding how Remotion’s rendering model differs from a normal React app, and having a mental map of what common errors actually mean.

This guide covers the full working day of a Remotion developer: starting work in Remotion Studio, catching issues with the ESLint plugin before they reach render time, debugging async data loading, benchmarking and speeding up slow renders, and diagnosing errors when things go wrong.

Remotion Studio as the Primary Development Environment

Remotion Studio (npx remotion studio) is the browser-based development environment that should be your main interface during composition development. It gives you:

  • A scrubable timeline for any registered composition
  • Live preview that updates as you save source files (via Vite HMR)
  • A props editor for compositions with defined prop schemas
  • A render dialog for kicking off local renders
  • Keyboard shortcuts for frame-by-frame navigation

The most productive workflow is to keep Studio open in one browser tab and your editor in split view. Changes to component files hot-reload in the preview almost instantly. You rarely need to trigger a full render to verify that an animation feels right — scrubbing through the timeline in Studio is faster and gives you a clearer picture of every frame.

Navigating frames efficiently in Studio:

  • Space — play/pause
  • Left/Right arrow — step one frame
  • Shift + Left/Right — step 10 frames
  • 0 — jump to frame 0
  • Type a frame number directly in the frame input

Getting comfortable with frame-stepping is essential. Most animation bugs show up at a specific frame number, and scrubbing to it precisely is much faster than hunting visually.

Single-Frame Renders for Quick Visual Checks

When you want to verify exactly how a specific frame looks at full render quality — not the preview’s scaled approximation — you can render just that frame from the CLI:

npx remotion still src/index.ts MyComposition --frame=42 out/frame-42.png

Or with the renderStill API:

npx remotion render src/index.ts MyComposition out/preview.mp4 --frames=42-42

The --frames flag accepts a range. --frames=42-42 renders a single-frame video, which is slightly slower than renderStill but uses the same code path as a full render, making it more representative. Use renderStill for PNG snapshots and --frames for spot-checking motion blur or audio sync.

This pattern is particularly useful for:

  • Verifying that text rendering (font, size, line breaks) looks correct at the actual output resolution
  • Checking that <Img> assets loaded correctly and are not showing placeholder states
  • Debugging frame-accurate data-driven visuals (e.g., a bar chart at frame 90 should show exactly this value)

Understanding console.log in Remotion Components

This is the behavior that confuses almost every new Remotion developer: console.log inside a React component does not appear when you are previewing in Remotion Studio. It appears when you trigger a render.

Here is why: Remotion Studio renders the preview by running your React component in the browser tab’s JavaScript engine. Browser console output goes to DevTools. If you are not watching the DevTools console, you miss it.

During a CLI render, Remotion launches a headless Chromium instance. Console output from that instance is captured and forwarded to your terminal when you use the --log=verbose flag:

npx remotion render src/index.ts MyComposition out/output.mp4 --log=verbose

Without --log=verbose, only errors are shown. With it, you get a stream of output including any console.log statements that fire during the render.

Practical tip: For debugging data-driven animations, place console.log calls at the top of your component and render with --log=verbose --frames=0-0. This gives you the data state at frame 0 without rendering the full video.

delayRender and continueRender for Async Data Loading

When your composition needs to fetch data before rendering — from an API, a database, or the filesystem — you must tell Remotion to wait before it starts capturing frames. This is what delayRender and continueRender are for.

The pattern is:

import { delayRender, continueRender, useCurrentFrame } from "remotion";
import { useEffect, useState } from "react";

export const DataDrivenComp: React.FC<{ dataUrl: string }> = ({ dataUrl }) => {
  const [data, setData] = useState<ChartData | null>(null);
  const [handle] = useState(() => delayRender("Loading chart data"));

  useEffect(() => {
    fetch(dataUrl)
      .then((res) => res.json())
      .then((json) => {
        setData(json);
        continueRender(handle);
      })
      .catch((err) => {
        console.error("Failed to load data:", err);
        continueRender(handle); // Must always call continueRender or render hangs
      });
  }, [dataUrl]);

  if (!data) return null;

  return <ChartAnimation data={data} />;
};

A few critical rules:

  1. Always call continueRender in the error path, not just on success. If you fail to call it, the render hangs indefinitely until it times out (default: 30 seconds).

  2. Do not call delayRender conditionally. The useState initializer pattern (useState(() => delayRender(...))) ensures it is called exactly once on mount.

  3. Multiple delayRender calls are supported. If your component makes several async requests, call delayRender for each one and pair each with its own continueRender.

The delayRender function accepts a descriptive string argument — this string appears in the Remotion Studio UI and in --log=verbose output, making it much easier to identify which async operation is pending when a render stalls.

@remotion/eslint-plugin for Catching Issues Early

The @remotion/eslint-plugin package includes rules specifically for Remotion components. Install it:

npm install --save-dev @remotion/eslint-plugin

Add it to your ESLint config:

{
  "plugins": ["@remotion"],
  "extends": ["plugin:@remotion/recommended"]
}

The plugin catches several categories of issues:

Non-deterministic functions: Using Math.random(), Date.now(), new Date(), or performance.now() inside components. These produce different values on each render call, breaking Remotion’s frame determinism guarantee. The plugin flags them with suggestions to use seeded alternatives or derive values from useCurrentFrame().

Missing continueRender calls: Detecting delayRender usage without a corresponding continueRender.

Incorrect hook usage: Catching cases where useCurrentFrame() or useVideoConfig() are called outside of a Remotion composition context.

Static file references: Flagging absolute file paths that will not work in different environments.

Running the ESLint plugin in CI as a pre-render check catches the class of bugs that are otherwise only discovered at render time — which is much more expensive to debug.

Using inputProps and calculateMetadata for Rapid Iteration

inputProps is Remotion’s mechanism for passing dynamic data into a composition at render time. But during development, it is also your main lever for iterating quickly: change the props, see the result, adjust.

calculateMetadata takes this further by allowing the composition’s duration and canvas dimensions to be derived from the input props at render time:

import { CalculateMetadataFunction } from "remotion";

type Props = {
  slides: SlideData[];
  fps: number;
};

export const calculateMetadata: CalculateMetadataFunction<Props> = async ({
  props,
}) => {
  const durationInFrames = props.slides.length * 90; // 3 seconds per slide at 30fps
  return {
    durationInFrames,
    fps: props.fps,
  };
};
<Composition
  id="SlideShow"
  component={SlideShow}
  calculateMetadata={calculateMetadata}
  fps={30}
  width={1920}
  height={1080}
  defaultProps={{
    slides: sampleSlides,
    fps: 30,
  }}
/>

This pattern is powerful for content-driven videos where the length depends on the data. It also makes your composition genuinely reusable — the same component handles a 5-slide deck and a 50-slide deck without any hardcoded durations.

For development iteration, keep a defaultProps object in your Root.tsx with realistic sample data. You can freely edit this object and see the composition update in Studio without touching the component code.

Benchmarking and Optimizing Render Speed

Slow renders come from a few common sources. Here is how to identify and address them.

Step 1: Establish a baseline. Time a full render of your composition:

time npx remotion render src/index.ts MyComposition out/output.mp4

Record the time. This is your benchmark.

Step 2: Check for delayRender overhead. If you have async data loading, each delayRender waits for a network request. In production, cache or pre-fetch data as a static file rather than making live API calls during render. Use calculateMetadata to fetch data once before rendering starts, then pass it down via inputProps:

export const calculateMetadata: CalculateMetadataFunction<Props> = async ({ props }) => {
  const data = await fetchDataOnce(props.dataUrl);
  return {
    props: { ...props, data }, // inject pre-fetched data
    durationInFrames: data.frames,
  };
};

Step 3: Reduce JavaScript complexity per frame. Every frame evaluation runs your component tree once. Complex computations inside components that run every frame (like sorting large arrays or string manipulation) should be memoized with useMemo:

const sortedData = useMemo(() => {
  return data.slice().sort((a, b) => b.value - a.value);
}, [data]); // only recomputes if data changes — which it never does in Remotion

Step 4: Parallelism. Remotion’s renderMedia API supports a concurrency option:

await renderMedia({
  composition,
  serveUrl,
  codec: "h264",
  outputLocation: "out/output.mp4",
  concurrency: 4, // render 4 frames simultaneously
});

The default concurrency is the number of CPU cores divided by 2. Increasing it can help on machines with many cores, but at some point you hit memory or I/O bottlenecks.

Step 5: For very long videos, use Remotion Lambda. Cloud rendering distributes the work across many Lambda instances. A 10-minute video that takes 8 minutes locally can render in under 60 seconds on Lambda by parallelizing across 200 instances.

Using —log=verbose for Debugging Slow Renders

When a render is slower than expected or hangs, --log=verbose is your first tool:

npx remotion render src/index.ts MyComposition out/output.mp4 --log=verbose

The verbose log shows:

  • The frame count progress (Rendering frame X of Y)
  • delayRender calls and when they resolve
  • Any console output from your components
  • Timing information for each render step

If the render stalls on a specific frame number, note that frame. Then render just that frame with --frames=N-N --log=verbose to see exactly what is happening.

A render that takes much longer on frames 0–10 than on later frames usually indicates a delayRender issue — the async data fetch is running during those frames.

Common Errors and What They Mean

“Error: A delayRender() was called but its matching continueRender() was never called after 30000ms.” Something in your component called delayRender() but the corresponding continueRender() was never reached. Check your useEffect for missing error-path continueRender calls. Also check that the component is not unmounting before the async operation completes.

“Error: The component returned null on frame X but something else on frame Y.” Your component is conditionally returning null based on data state, but the data is not loaded yet. The solution is to always call delayRender when loading async data — do not return null as a loading state.

“Error: useCurrentFrame() was called outside of a Remotion context.” You called useCurrentFrame() outside the Remotion render tree — typically in a utility function or a non-composition component. Move the hook call to the root of the composition component and pass the frame value down as a prop.

“FFmpeg failed with exit code 1.” Usually a missing or incompatible codec. Check that your codec value matches the container: h264 works with .mp4, vp8/vp9 with .webm, prores with .mov. Also check that your audio codec is compatible with the video codec.

“Cannot find module ‘remotion’” Remotion package is not installed or not found in the current working directory. Run npm install and verify your package.json lists remotion as a dependency.

Preview shows a blank white screen. Usually caused by an error in your component that React caught and rendered as nothing. Open browser DevTools → Console to see the error. Common causes: trying to access a property on undefined (data not loaded), or an invalid CSS value.

RenderComp Templates as a Workflow Foundation

One of the fastest ways to establish a productive Remotion workflow is to start from a well-structured template rather than a blank project. RenderComp templates are built with developer workflow in mind: they include sample inputProps schemas, calculateMetadata patterns for variable-length compositions, and organized file structures that separate data, animation logic, and composition registration. Visit rendercomp.com to find templates for your use case and skip the boilerplate phase.


FAQ

Q1: Should I run Remotion Studio in Chrome or another browser? Remotion Studio works in any modern browser, but Chromium-based browsers (Chrome, Edge, Brave) give the most accurate preview since Remotion uses headless Chromium for rendering. Preview in Chrome to minimize surprises between preview and final output.

Q2: How do I pass environment variables (API keys, etc.) into a Remotion render? Use process.env.YOUR_VAR in your component or calculateMetadata. For CLI renders, set the variable before the command: MY_API_KEY=abc123 npx remotion render .... For the Node.js renderer API, the variables are inherited from the calling process’s environment.

Q3: What is the difference between renderMedia and renderStill? renderMedia renders a video or audio file (all frames). renderStill renders a single frame to an image file (PNG, JPEG, WebP). Use renderStill for thumbnail generation, social preview images, or debugging individual frames.

Q4: Why does my animation look smooth in Studio but choppy in the exported video? Studio preview can drop frames when running complex compositions in real-time. The exported video is always frame-by-frame accurate — each frame is rendered individually at full quality. If the exported video looks choppy, the issue is in your animation math. Check for sudden value jumps using --frames to inspect specific frames.

Q5: Can I use React Query or SWR for data fetching in Remotion components? Technically yes, but the standard delayRender/continueRender pattern is recommended instead. React Query and SWR are designed for browser UI applications with caching and revalidation behavior that does not map cleanly onto Remotion’s render model. Using the native Remotion async pattern avoids hard-to-debug edge cases.

Q6: How do I debug a composition that renders correctly locally but fails on Remotion Lambda? The most common cause is a missing environment variable or an asset URL that works locally but is not accessible from the Lambda execution environment. Run renderMedia with --log=verbose against the Lambda deployment to see the error. Also check that all assets (images, fonts, audio) are served from a publicly accessible URL, not a local file path.

Q7: Is there a way to speed up the initial bundle build for large Remotion projects? Yes — Remotion uses Vite for bundling, so standard Vite optimization techniques apply. Split large compositions into separate entry points if possible. Use dynamic imports for heavy dependencies that are only needed by certain compositions. Keep node_modules warm (avoid cleaning the Vite cache between related renders).

Now available

Get 1,000+ Remotion Templates

Pay once — no subscription. Lifetime updates. TypeScript-first.

View pricing →