Remotion パフォーマンス最適化 — レンダリング高速化とStudioプレビュー改善
Remotion パフォーマンス最適化 — レンダリング高速化とStudioプレビュー改善
Remotionは「Reactでビデオを作れる」という強力なコンセプトを提供します。しかしその自由度の裏には、パフォーマンス上の落とし穴が潜んでいます。ブラウザでは快適にプレビューできる構成が、いざエンコードすると想定外に時間がかかる——そんな経験をしたことがある方は多いはずです。
Remotionのレンダリングは、ヘッドレスChromiumがフレームごとにスクリーンショットを撮影し、FFmpegがそれを動画ファイルに結合するパイプラインで動いています。Reactの再レンダリングコスト、未最適化のアセット読み込み、非決定論的なコードはすべてこのパイプラインに直接影響します。
本記事では、レンダリング時間を短縮し、Studioプレビューをスムーズにし、Lambdaで本番スケールに対応するための8つの具体的な改善手法を、公式ドキュメントに基づいて解説します。
1. Remotionのレンダリングパイプラインを理解する
最適化の前に、何が遅いのかを理解することが重要です。
npx remotion render を実行すると、Remotionは1つ以上のヘッドレスChromiumインスタンスを起動します。各インスタンスは指定フレームにシークし、すべての delayRender() ハンドルが解決されるのを待ってからスクリーンショットを撮影し、次のフレームへ進みます。
レンダリング時間は主に2つの要素で決まります。
- フレームあたりのJavaScriptコスト — ReactとコードがフレームごとにどれだけCPUを使うか
- I/O待ち時間 — 画像・音声・フォント・非同期データの読み込みにChromiumが費やす時間
並行処理はこの両方を乗算します。4つのChromiumインスタンスを並列で動かせば経過時間は約4分の1になりますが、CPUが不足すれば逆効果です。
最初に使うべき診断コマンドはこれです。
npx remotion render --log=verbose MyComposition out.mp4
--log=verbose を付けると、レンダリング終了後に処理が最も遅かったフレームを降順で表示します。他の設定を変える前に、まずここで遅いフレームを特定してください。
2. ローカルレンダリング: --concurrency フラグと最適値
--concurrency は、ローカルレンダリング時に並列で起動するChromiumブラウザタブの数を制御します。デフォルトは論理CPUコア数の50%です。
# 4つのブラウザインスタンスを並列起動
npx remotion render MyComposition out.mp4 --concurrency=4
# CPUコア数の割合で指定する方法
npx remotion render MyComposition out.mp4 --concurrency=75%
並行数を増やせば必ず速くなるわけではありません。パーティクルシステムや複雑なスプリング計算など、フレームあたりのJavaScriptコストが高い構成では、タブを増やすほどCPU競合が起きてすべてのインスタンスが遅くなります。
最適値を見つけるには、公式のベンチマークコマンドを使います。
# 並行数 1, 2, 4, 8 を比較する
npx remotion benchmark --concurrency=1,2,4,8
各並行値で構成のサンプルをレンダリングし、実時間を比較して報告してくれます。最適値はマシンと構成によって異なります。Mシリーズチップ搭載のMacBook Proで、中程度のJSコストを持つ構成なら4〜8が多くの場合ベストです。
remotion.config.ts に設定を書いておくと毎回指定する手間が省けます。
import { Config } from "@remotion/cli/config";
Config.setConcurrency(6);
3. 高コストな計算に useMemo を使う
Reactはフレームごとに構成をレンダリングします。パーティクルの初期配置計算、データセットの正規化、複雑なパスの生成など、重い処理がレンダー関数の中にあると、それが数千フレーム分繰り返されます。useMemo は依存関係が変わらない限り結果をキャッシュし、不要な再計算を防ぎます。
パーティクルシステムへの適用
import { useMemo } from "react";
import { useCurrentFrame } from "remotion";
const ParticleField: React.FC<{ count: number }> = ({ count }) => {
const frame = useCurrentFrame();
// useMemoなし: フレームごとに初期位置を再計算する
// useMemoあり: countが変わらない限り一度だけ計算
const initialPositions = useMemo(() => {
return Array.from({ length: count }, (_, i) => ({
x: (i * 137.5) % 100,
y: (i * 73.1) % 100,
}));
}, [count]); // 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"
/>
))}
</>
);
};
データ処理への適用
const ChartBars: React.FC<{ dataset: number[] }> = ({ dataset }) => {
const frame = useCurrentFrame();
// 正規化処理はframeに依存しないのでメモ化できる
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"
/>
))}
</>
);
};
関数をpropsとして渡す場合は useCallback が同じ役割を果たします。どちらも重い計算の繰り返しと比べてオーバーヘッドはほぼゼロです。
4. アセットのプリロード: 画像・フォント・音声
Remotionは各Chromiumインスタンス内でフレームを順次処理します。スクリーンショットのタイミングで画像や音声の読み込みが完了していないと、出力が空白になるか、delayRender() のタイムアウトでレンダリングが停止します。
@remotion/preload パッケージには4つのヘルパー関数が用意されています。
npm install @remotion/preload
import { preloadImage, preloadAudio, preloadFont } from "@remotion/preload";
// コンポーネント外(モジュールレベル)で呼び出す
// コンポジションがマウントされる前から読み込みが始まる
preloadImage("https://example.com/hero-image.png");
preloadAudio("https://example.com/background-music.mp3");
preloadFont("https://example.com/font.woff2");
これらをコンポーネント外で呼び出すことで、モジュールがロードされた瞬間からブラウザがリソースの取得を開始します。内部的には <link rel="preload"> タグをドキュメントに挿入します。
フレームキャプチャの前にアセットを完全ダウンロードしておく必要がある場合は、prefetch() を使います。
import { prefetch } from "remotion";
const { waitUntilDone } = prefetch("https://example.com/heavy-asset.mp4");
// コンポーネント内:
const handle = delayRender("アセット読み込み待ち");
waitUntilDone().then(() => continueRender(handle));
prefetch() はアセット全体をダウンロードしてBlobURLに変換します。大きな音声・動画ファイルの場合、これが最も確実な方法です。
実践上の推奨: 静的アセットは public/ フォルダに配置して staticFile() で参照するのが最善です。ローカルファイルはディスクからメモリ速度で読み込まれ、レンダリング中のネットワーク変動を完全に排除できます。
5. 非決定論的操作を避ける
Remotionは複数のChromiumインスタンス(場合によっては異なるマシン)でフレームをレンダリングします。すべてのインスタンスで同じフレームが同じ見た目になるには、すべての処理が同じ入力から同じ出力を返さなければなりません。
Math.random() はこの保証を破ります。呼び出すたびに異なる値を返すため、インスタンス間でパーティクルの位置がバラバラになり、フリッカーやレンダリングアーティファクトが発生します。
Remotionには決定論的な代替APIが用意されています。
import { random } from "remotion";
// NG — インスタンスごとに異なる値になる
const x = Math.random() * 100;
// OK — 同じシードは常に同じ値を返す
const x = random("particle-x-0") * 100;
const y = random("particle-y-0") * 100;
// フレームとパーティクルIDを組み合わせたシードも有効
const offset = random(frameIndex * 0.01 + particleId) * 50;
random() はシードベースの疑似乱数生成器です。シードが同じである限り、スレッドやマシンが違っても出力は同一になります。
同様に、レンダーパスで Date.now() や new Date() を使うことも避けてください。現在の壁時計時刻はインスタンス間で異なります。時間に依存した値が必要な場合は、useCurrentFrame() とフレームレートから導出してください。
RemotionのESLintプラグインはレンダーコンテキスト内の Math.random() を自動検出して警告を出します。コードベース全体に適用しておくと安心です。
6. Studioプレビューの再レンダリングを減らす
Remotion StudioはタイムラインスクラバーつきのReactアプリとして動いています。スクラブするたびに新しいフレームでコンポジションツリー全体が再レンダリングされます。ツリーが深く処理が重いと、プレビューがもたつきます。
React.memo でコンポーネントをメモ化する
useCurrentFrame() に依存しない(つまりframeが変わっても見た目が変わらない)コンポーネントは React.memo でラップします。
const StaticBackground = React.memo(({ color }: { color: string }) => (
<div style={{ background: color, width: "100%", height: "100%" }} />
));
フレーム非依存のロジックをレンダー外に移す
フレーム番号に関わらず結果が変わらない計算は、空の依存配列を持つ useMemo か、コンポーネント外のモジュールレベル定数として配置します。
プレビューのイメージフォーマットをJPEGに切り替える
npx remotion studio --image-format=jpeg
PNGはロスレスですがエンコードが遅く、JPEGはその逆です。Studioのスクラブ中はJPEGの方がはるかにレスポンスよく動きます。
ハードウェアアクセラレーションを有効にする
--hardware-acceleration フラグを使うか、Studioの「詳細」タブからハードウェアアクセラレーションを有効にします。WebGLや3D CSSトランスフォームを多用する構成では、フレームキャプチャ時間を大幅に短縮できます。
遅いフレームを特定する
--log=verbose でフルレンダリングを実行すると、最も遅い10フレームとその処理時間が表示されます。闇雲に最適化するより、この情報を起点にした方が効果的です。
7. Lambda並列化: 長い構成を分割する
1分を超える動画や、素早い生成が必要な本番パイプラインには、Remotion Lambdaが適切な選択肢です。最大200の並列AWS Lambda関数にフレームレンダリングを分散できます。
主要なチューニングパラメータは framesPerLambda です。1つのLambda関数が担当するフレーム数を指定し、同時実行数は次の式で決まります。
同時実行数 = 総フレーム数 / framesPerLambda
30fps・30秒の動画(900フレーム)で framesPerLambda=15 に設定すると、60個のLambdaが同時に起動します。
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",
// nullにするとRemotionが最適値を自動選択(ほとんどの用途に推奨)
framesPerLambda: null,
// メモリを増やすとCPUも比例してスケールする
memorySizeInMb: 3009,
});
公式ドキュメントからの重要なポイントをまとめます。
framesPerLambda: nullを推奨します。 Remotionが関数数を安全な上限内に収める合理的な値を選択します。memorySizeInMbを増やすと個別のLambda関数が速くなります。 LambdaではメモリとCPUが比例してスケールするため、メモリを2倍にすると処理速度もほぼ2倍になります。コストも同じ割合で増えるため、ベンチマークでトレードオフを確認してください。- 同時実行の上限は200関数です。 それ以上は費用対効果が低下し、Remotionがこの制限を強制します。
- 音声コーデックをMP3に切り替える。 デフォルトはAACですが、MP3の方がエンコードがはるかに速く、最終的なチャンク結合ステージを大幅に短縮できます。
await renderMediaOnLambda({
// ...
audioCodec: "mp3",
});
8. ファイルサイズ最適化: コーデックとビットレート
レンダリング時間とファイルサイズは連動しています。ファイルが小さくなるコーデックはエンコードが遅く、エンコードが速いコーデックはファイルが大きくなる傾向があります。
H.264 がほとんどのケースで最善の選択です。 互換性が高く、エンコードが速く、ファイルサイズも扱いやすいバランスです。
VP8 / VP9 は特別な理由がない限り避けてください。 Remotionの公式ドキュメントでも、WebMコーデックは圧縮アルゴリズムの性質上H.264よりエンコードが大幅に遅いと明記されています。
CRF(固定品質係数)で品質とサイズを調整します。
npx remotion render MyComposition out.mp4 --crf=23
CRFは数値が小さいほど高品質・大ファイル、大きいほど低品質・小ファイルです。デフォルトは18(高品質)です。SNS向け出力(プラットフォーム側で再エンコードされる)なら23〜26に上げるとファイルサイズを大幅に削減できます。
中間フォーマットをJPEGにします。
npx remotion render --image-format=jpeg --jpeg-quality=80 MyComposition out.mp4
PNG中間フレームはロスレスですが書き込みが重く、I/Oオーバーヘッドを増やします。モーショングラフィックスの多くでは、最終エンコード後の品質差はほぼ感知できません。
放送・ポストプロダクション向けにはProResを使います。 対応ソフトウェアとのネイティブ互換性が必要な場合は prores コーデックを指定してください。ファイルサイズは大きくなりますが、編集ソフトが直接扱えます。
よくある質問
Q: 並行数を上げても速くならない。何を確認すればよいですか?
まず --log=verbose で遅いフレームを特定してください。並行数が高いのに全体が遅い場合、フレームあたりのJavaScriptが真のボトルネックです。useMemo の適用漏れや、不必要にレンダーパスで行っている重い計算がないか確認してください。
Q: Studioのプレビューが重い。最も効果的な対処は?
React.memo による静的コンポーネントのメモ化と、プレビューのイメージフォーマットをJPEGへの切り替えが最も即効性があります。その上でハードウェアアクセラレーションを有効にしてみてください。
Q: Math.random() をコンポーネント外で一度だけ呼べば使えますか?
モジュール外で一度呼べばフレームごとの再シードは防げますが、異なるChromiumインスタンスは別々のランダム列を持つため、フレームが一致しない問題は残ります。Remotionの random() 関数を使うのが唯一の正しい解決策です。
Q: 素早い確認用に最も速いコーデックは?
H.264 が最速の互換コーデックです。さらに速くしたい場合は --scale=0.5 で解像度を半分にしてレンダリングするのが効果的です。短い確認用なら --codec=gif も選択肢になります。
Q: Lambdaが自分の構成に値するか判断する基準は?
ローカルレンダリングが2〜3分を超えるならLambdaを検討する価値があります。それ以下の短い動画では、Lambdaのコールドスタートとオーバーヘッドがスピードゲインを相殺することがあります。まず npx remotion benchmark でローカルの最速値を把握してから比較してください。
Q: レンダリング環境でのフォント読み込みのベストプラクティスは?
@remotion/preload の preloadFont() をモジュールレベルで呼び出してください。public/ フォルダに配置したカスタムフォントは staticFile() で参照し、@font-face 宣言を構成内の <style> タグに書きます。外部フォントCDNへのリンクはネットワーク遅延と可用性リスクを生むため避けてください。
Q: Lambdaレンダリングでタイムアウトエラーが出る。原因は?
最も多い原因は未解決の delayRender() ハンドルです。delayRender() を呼んだら必ず30秒以内に continueRender() を呼ぶ必要があります。データフェッチ・フォント読み込み・画像プリロードで完了しないケースがないか確認してください。ローカルで --log=verbose を使うとタイムアウトが発生しているフレームを特定できます。
RenderCompテンプレートで最適化済みの状態から始める
パフォーマンス最適化は、構成の設計が正しい前提があってはじめて効果を発揮します。RenderCompのテンプレートは、メモ化パターン・@remotion/preload 統合・Lambda対応の構成構造があらかじめ組み込まれています。パフォーマンスの基盤を再実装する時間をなくし、コンテンツ制作に集中できます。
rendercomp.com でテンプレートラインナップをご確認ください。
参照: Performance Tips — Remotion | Optimizing for speed — Remotion Lambda | Preloading assets | @remotion/preload | Using randomness | Lambda Concurrency