Remotion Performance Optimization: Faster Renders and Smoother Previews
Remotion Performance Optimization: Faster Renders and Smoother Previews
Remotion gives you the expressive power of React for video. That power comes with a cost: a composition that renders beautifully in your browser can take an unexpectedly long time to encode as an actual video file. Remotion renders frames by opening your composition in a headless browser, capturing screenshots frame by frame, and stitching them with FFmpeg. Every React re-render, every unoptimized asset, every call to Math.random() directly taxes that pipeline.
This guide covers eight concrete levers you can pull to cut render times, sharpen Studio preview responsiveness, and scale to production with Lambda — verified against the official Remotion documentation.
1. Understanding the Remotion Render Pipeline
Before optimizing anything, it pays to understand what is actually slow.
When you run npx remotion render, Remotion spawns one or more headless Chromium instances. Each instance seeks to a specific frame, waits for all delayRender() handles to be resolved, takes a screenshot, and moves on. FFmpeg then encodes the accumulated frames into the final video file.
This means render time is dominated by two factors:
- Per-frame JavaScript cost — how much work React and your code do on every frame.
- I/O wait — how long Chromium stalls waiting for images, audio, fonts, and async data to load.
Concurrency multiplies both: run four Chromium instances in parallel and you cut elapsed time roughly by four, but only if each instance has enough CPU headroom. Overdo it and you starve the CPU, making every instance slower.
The single best diagnostic command in the Remotion toolbox is:
npx remotion render --log=verbose MyComposition out.mp4
With verbose logging, Remotion prints the slowest frames in descending order at the end of the run. Start there before touching any other setting.
2. Local Render: The --concurrency Flag and Optimal Values
Concurrency controls how many Chromium browser tabs Remotion opens in parallel during a local render. The default is 50% of your logical CPU cores.
# Explicit concurrency — four parallel browser instances
npx remotion render MyComposition out.mp4 --concurrency=4
# Express as a percentage of CPU cores
npx remotion render MyComposition out.mp4 --concurrency=75%
Higher concurrency is not always better. If your composition does heavy JavaScript work per frame — particle systems, complex spring calculations, large data transformations — each Chromium tab consumes significant CPU. Opening too many tabs causes all of them to slow down because they compete for the same cores. Opening too few leaves cores idle.
The official way to find the sweet spot is the benchmark command:
# Test concurrency values 1, 2, 4, and 8 to compare elapsed time
npx remotion benchmark --concurrency=1,2,4,8
This renders a short sample of your composition at each concurrency level and reports wall-clock time. The winning value varies by machine and composition. On a developer MacBook Pro with an M-series chip, compositions with moderate JS complexity typically peak around 4–8. CPU-light compositions (mostly CSS transitions) can benefit from concurrency as high as the logical core count.
You can persist your preferred concurrency in remotion.config.ts:
import { Config } from "@remotion/cli/config";
Config.setConcurrency(6);
3. useMemo for Expensive Calculations
React renders your composition on every frame. If a component computes a particle layout, processes a dataset, or builds a complex path string, that computation runs thousands of times across a full render. useMemo caches the result and only recomputes when its declared dependencies change.
Particle systems
import { useMemo } from "react";
import { useCurrentFrame } from "remotion";
const ParticleField: React.FC<{ count: number }> = ({ count }) => {
const frame = useCurrentFrame();
// Without useMemo: recalculates initial positions on every frame
// With useMemo: calculates once; frame changes do not trigger recalc
const initialPositions = useMemo(() => {
return Array.from({ length: count }, (_, i) => ({
x: (i * 137.5) % 100,
y: (i * 73.1) % 100,
}));
}, [count]); // depends only on count, not on frame
return (
<>
{initialPositions.map((pos, i) => (
<circle
key={i}
cx={`${pos.x + Math.sin(frame * 0.05 + i) * 5}%`}
cy={`${pos.y + Math.cos(frame * 0.03 + i) * 5}%`}
r={3}
fill="white"
/>
))}
</>
);
};
Data processing
const ChartBars: React.FC<{ dataset: number[] }> = ({ dataset }) => {
const frame = useCurrentFrame();
// Normalization does not depend on frame — memoize it
const normalized = useMemo(() => {
const max = Math.max(...dataset);
return dataset.map((v) => v / max);
}, [dataset]);
return (
<>
{normalized.map((ratio, i) => (
<rect
key={i}
x={i * 60}
y={400 - ratio * 350 * Math.min(frame / 30, 1)}
width={50}
height={ratio * 350 * Math.min(frame / 30, 1)}
fill="#6366f1"
/>
))}
</>
);
};
useCallback applies the same principle to functions passed as props. Both hooks have negligible overhead compared to repeated expensive calculations.
4. Asset Preloading: Images, Fonts, and Audio
Remotion renders frames sequentially within each browser instance. If an image or audio file has not finished loading by the time the frame is screenshotted, the output is either blank or the render stalls on a delayRender() timeout.
The @remotion/preload package ships four dedicated helpers:
npm install @remotion/preload
import { preloadImage, preloadAudio, preloadFont } from "@remotion/preload";
// Call outside a component so preloading starts immediately,
// not when the component first renders
preloadImage("https://example.com/hero-image.png");
preloadAudio("https://example.com/background-music.mp3");
preloadFont("https://example.com/font.woff2");
Calling these outside of any React component means the browser begins fetching the resource as soon as the module loads, before the composition even mounts. The preload functions inject a <link rel="preload"> tag into the document.
For assets you need to be completely downloaded before a frame is captured — not just signalled to the browser — use prefetch() instead:
import { prefetch } from "remotion";
// Returns { waitUntilDone, free }
const { waitUntilDone } = prefetch("https://example.com/heavy-asset.mp4");
// Inside a component:
const handle = delayRender("waiting for asset");
waitUntilDone().then(() => continueRender(handle));
prefetch() downloads the entire asset and converts it to a Blob URL, which is then served locally. For large audio and video files this is the most reliable way to guarantee the asset is ready before rendering begins.
Practical guidance: bundle static assets into your public/ folder and reference them with staticFile() rather than remote URLs whenever possible. Local assets load from disk at memory speed and eliminate any network variability during rendering.
5. Avoiding Non-Deterministic Operations
Remotion renders frames using multiple Chromium instances, sometimes on different machines. For a frame to look identical across all rendering contexts, every operation must produce the same output given the same frame number.
Math.random() breaks this guarantee. It returns a different value every time it is called, regardless of frame. A particle animation seeded with Math.random() at module load time will look different in each Chromium instance, causing flickering or outright rendering artifacts.
Remotion ships a deterministic alternative:
import { random } from "remotion";
// Bad — value changes every render, breaks multi-instance consistency
const x = Math.random() * 100;
// Good — same seed always returns same value
const x = random("particle-x-0") * 100;
const y = random("particle-y-0") * 100;
// Seed can be a number or any string
const offset = random(frameIndex * 0.01 + particleId) * 50;
The random() function is a seeded pseudorandom number generator. As long as the seed is the same — and frame-dependent seeds vary only with the frame number — the output is identical across all threads and all machines.
Similarly, avoid Date.now() and new Date() in render paths. These return the current wall-clock time, which differs between instances. If you need a timestamp-based value, derive it from useCurrentFrame() and the composition’s frame rate via useVideoConfig().
The Remotion ESLint plugin will warn you when it detects Math.random() in a render context — an easy automated safety net.
6. Studio Preview: Reducing Re-Render Triggers
Remotion Studio runs your composition inside a React app with a timeline scrubber. Every scrub triggers a re-render of the entire composition tree at the new frame. If your tree is deep and expensive, the preview will feel sluggish.
Several techniques help here:
Component memoization with React.memo: Wrap components that depend only on inputProps (not on useCurrentFrame) in React.memo. They skip re-rendering when the frame changes but their props have not.
const StaticBackground = React.memo(({ color }: { color: string }) => (
<div style={{ background: color, width: "100%", height: "100%" }} />
));
Move frame-independent logic out of render: Any computation that produces the same result regardless of the current frame belongs either in useMemo with empty or stable dependencies, or outside the component entirely as a module-level constant.
PNG vs JPEG: If you are rendering still frames for preview (not the final encode), switch to JPEG:
npx remotion studio --image-format=jpeg
PNG is lossless and slower to encode; JPEG preview frames render noticeably faster, which makes Studio scrubbing feel more responsive.
Hardware acceleration: Remotion Studio supports GPU-accelerated rendering via the --hardware-acceleration flag or via the Advanced tab in the in-Studio render dialog. For compositions that use WebGL, 3D CSS transforms, or canvas-heavy effects, this can cut frame capture time significantly.
Identify slow frames: Run a full render with --log=verbose. The output lists the ten slowest frames with their elapsed time. Focus optimization effort on those specific frames rather than guessing.
7. Lambda Parallelization: Splitting Long Compositions
For compositions longer than a minute or production pipelines that need fast turnaround, Remotion Lambda is the right tool. It distributes frame rendering across up to 200 concurrent AWS Lambda functions.
The key tuning parameter is framesPerLambda. It controls how many frames each Lambda function renders. The concurrency (number of simultaneous functions) is:
concurrency = totalFrameCount / framesPerLambda
A 900-frame video (30 seconds at 30 fps) with framesPerLambda=15 spawns 60 simultaneous Lambda invocations. The entire video can finish in the time it takes to render 15 frames.
import { renderMediaOnLambda } from "@remotion/lambda/client";
const result = await renderMediaOnLambda({
region: "ap-northeast-1",
functionName: "remotion-render-4-0-0",
serveUrl: "https://your-site-url.s3.amazonaws.com/sites/my-video/",
composition: "MyComposition",
inputProps: { title: "Hello" },
codec: "h264",
// Let Remotion pick an optimal value — recommended for most cases
framesPerLambda: null,
// Increase memory to proportionally scale CPU on Lambda
memorySizeInMb: 3009,
});
Key guidance from the official documentation:
- Set
framesPerLambda: nullfor most use cases. Remotion will choose a reasonable value that keeps function count within safe limits. - Increase
memorySizeInMbto speed up individual Lambda functions. Memory and CPU scale proportionally on Lambda, so doubling memory roughly doubles per-function render speed. Cost increases linearly with memory, so benchmark the trade-off. - Maximum 200 concurrent functions. More than 200 yields diminishing returns. Remotion enforces this limit.
- Switch audio codec to MP3. The default audio codec is AAC. Changing it to MP3 makes the final “combining videos” stage significantly faster since MP3 encodes much faster than AAC.
await renderMediaOnLambda({
// ...
audioCodec: "mp3",
});
8. File Size Optimization: Codec and Bitrate
Render time and file size are connected. A codec that produces smaller files generally takes longer to encode; a codec that encodes quickly produces larger files.
H.264 is the right default for most video content. It is widely supported, encodes quickly, and produces files that are manageable in size.
Avoid VP8 and VP9 unless you specifically need WebM output. According to the Remotion documentation, both are significantly slower to encode than H.264 due to the stronger compression algorithms they apply.
Set a target CRF (Constant Rate Factor):
npx remotion render MyComposition out.mp4 --crf=23
CRF controls quality vs. file size. Lower values mean higher quality and larger files. For web delivery, values between 18 and 28 are typical. The default is 18, which is high quality but produces larger files. If your output is destined for social media (which re-encodes anyway), a CRF of 23–26 reduces file size with minimal perceptible quality loss.
Use JPEG intermediate frames:
npx remotion render --image-format=jpeg --jpeg-quality=80 MyComposition out.mp4
PNG intermediate frames are lossless but large. JPEG intermediate frames are smaller and faster to write to disk, which reduces the I/O overhead between frame capture and FFmpeg encoding. For most motion graphics work, the quality difference is imperceptible in the final encode.
ProRes for broadcast delivery: If your delivery target is a broadcast or post-production workflow, use the prores codec for maximum compatibility. File sizes are large but editing software handles them natively.
9. Frequently Asked Questions
Q: My render is slow despite high concurrency. What should I check first?
Run --log=verbose to identify the slowest frames. High concurrency with slow individual frames usually means your per-frame JavaScript is the bottleneck, not parallelism. Look for expensive calculations that run on every frame and apply useMemo or lift them out of the render path.
Q: Studio preview stutters even on a fast machine. What helps most?
Wrap static components in React.memo, switch preview image format to JPEG, and enable hardware acceleration. Also check whether any useEffect or useCallback is creating unnecessary re-render cascades on every frame change.
Q: Can I use Math.random() if I seed it once outside the component?
Seeding outside the component prevents re-seeding per frame, but the random sequence still differs between Chromium instances if each instance starts from a different seed. Use Remotion’s random() function instead — it guarantees identical output for the same seed across all instances.
Q: What is the fastest codec for a quick preview render?
H.264 is the fastest widely-compatible codec. For the absolute fastest output — useful for quick review only — you can use --codec=gif for short compositions or render with low CRF values. For internal preview, rendering to a lower resolution (--scale=0.5) also cuts time significantly.
Q: How do I know if Lambda is worth it for my composition?
Lambda pays off when render time exceeds roughly two to three minutes locally. For shorter compositions, Lambda’s cold-start and overhead may cancel out the speed gain. Use the local benchmark first: npx remotion benchmark, then compare estimated Lambda time using the concurrency formula above.
Q: How do I handle fonts in a render environment?
Use preloadFont() from @remotion/preload and call it at module level. For custom fonts that live in your public/ folder, reference them with staticFile() inside a @font-face declaration within a <style> tag in your composition. Avoid linking to external font CDNs — they introduce network latency and can fail in restricted environments.
Q: My Lambda render returns a timeout error. What is wrong?
The most common cause is an unresolved delayRender() handle. Every delayRender() call must be matched by a continueRender() call within 30 seconds. Check for async operations — data fetches, font loads, image preloads — that never complete. Use --log=verbose in a local render to identify the exact frame that times out.
Start Optimizing with RenderComp Templates
Performance optimization is only worthwhile if your composition is architecturally sound from the start. RenderComp templates are built with memoization patterns, @remotion/preload integration, and Lambda-ready composition structures baked in — so you spend time on your content, not on rebuilding performance foundations.
Browse the template library at rendercomp.com and start your next video project already optimized.
Sources: Performance Tips — Remotion | Optimizing for speed — Remotion Lambda | Preloading assets | @remotion/preload | Using randomness | Lambda Concurrency
Now available
Get 1,000+ Remotion Templates
Pay once — no subscription. Lifetime updates. TypeScript-first.
View pricing →