React Performance Optimization
Understanding the Remotion Render Loop
In Remotion, performance optimization serves two purposes: ensuring a smooth preview in the Studio and reducing the time required to render the final MP4/WebM file. Unlike standard web applications, Remotion renders your React tree for every single frame. If your composition is 30 FPS and 10 seconds long, your components will be evaluated at least 300 times.
Frame-Driven Logic vs. Clock-Driven Logic
The most critical performance rule in Remotion is to ensure all logic is deterministic and driven by the current frame.
- Forbidden: CSS animations, CSS transitions, and Tailwind animation classes. These rely on the browser's system clock, which does not synchronize with the Remotion rendering engine.
- Required: Use
useCurrentFrame()andinterpolate()to calculate styles.
import { useCurrentFrame, interpolate, useVideoConfig } from "remotion";
export const OptimizedComponent = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Deterministic calculation: always returns the same value for the same frame
const opacity = interpolate(frame, [0, fps], [0, 1], {
extrapolateRight: "clamp",
});
return <div style={{ opacity }}>I am frame-accurate</div>;
};
Memoization Strategies
Because Remotion triggers a re-render of the entire tree on every frame change, expensive computations can significantly slow down rendering.
Using React.memo for Heavy Components
Wrap components that do not change on every frame in React.memo. For example, if a background component only depends on a color prop and not the frame, memoizing it prevents unnecessary re-renders during the timeline progression.
import React from "react";
const HeavyBackground = React.memo(({ color }: { color: string }) => {
// Expensive SVG or complex DOM structure
return <div style={{ backgroundColor: color }} />;
});
useMemo for Data Processing
If you are processing large datasets (e.g., for charts or captions), perform the transformation inside useMemo or during the calculateMetadata phase.
import { useMemo } from "react";
import { createTikTokStyleCaptions } from "@remotion/captions";
export const CaptionsProvider = ({ captions }) => {
// Only re-run when the source captions change, not every frame
const pages = useMemo(() => {
return createTikTokStyleCaptions({
captions,
combineTokensWithinMilliseconds: 1200,
});
}, [captions]);
return <RenderPages pages={pages} />;
};
Optimizing Assets and Media
Loading high-resolution assets can become a bottleneck during the rendering process.
Using staticFile() and Remotion Components
Always use staticFile() to reference assets in the public/ folder. Use the built-in <Img>, <Video>, and <Audio> components from @remotion/media. These components are optimized to ensure that the rendering engine waits for the asset to be fully buffered before capturing a frame.
import { Img, staticFile } from "remotion";
// Efficient: Asset is resolved and the engine handles loading states
export const Logo = () => <Img src={staticFile("logo.png")} />;
Video Pre-processing
When working with large video files:
- Check decodability: Use
canDecode()to ensure the browser/environment can handle the codec. - Use
calculateMetadata: Dynamically set composition dimensions to match the source video to avoid unnecessary scaling calculations during render.
3D and Canvas Performance
Rendering 3D content via @remotion/three requires specific constraints to remain performant and deterministic.
- Avoid
useFrame(): In React Three Fiber,useFrame()runs on the browser's requestAnimationFrame. In Remotion, this is forbidden. UseuseCurrentFrame()from the Remotion package instead. - Layout "none": When using
<Sequence>inside a<ThreeCanvas>, setlayout="none". This prevents Remotion from creating extra DOM wrappers that can interfere with the Three.js scene graph.
<ThreeCanvas width={1920} height={1080}>
<Sequence layout="none">
<SceneContent />
</Sequence>
</ThreeCanvas>
Measuring DOM Nodes
If you must measure DOM elements (e.g., fitting text to a container), do it sparingly. Measuring the DOM forces a layout reflow.
- Prefer Offscreen Canvas for text measurement (
measureText) instead of readinggetBoundingClientRect()on every frame. - If you must use
getBoundingClientRect(), do it once inside auseEffector a state-initialization phase and store the result, rather than recalculating it during the render pass.