Remotionレンダリングコスト:Lambda・セルフホスト・Cloud Run比較
執筆: RenderComp チーム 編集方針
Remotionで動画を生成するとき、コストの本質は「決定論的なフレーム計算をどの計算資源でいつ実行するか」という一点に集約されます。30fps・60秒の動画は1,800フレームの独立したReactレンダリングサイクルとして分解でき、その並列度と実行環境がコストの形を決定します。
ウォールクロック時間を最小化すればコストが最大になるケースがあり、逆にコストを下げようとすれば処理時間が延びます。Lambdaのようなサーバーレスモデルとセルフホストではコストのドライバーが根本的に異なるため、「どちらが安いか」という問いに対する正確な答えは計測なしには出せません。自分のcompositionの複雑度、ジョブの発生頻度、許容できるウォールクロック時間。この三変数の組み合わせが正解を決めます。
この記事では、@remotion/lambdaのestimatePrice()とgetRenderProgress()、そしてrenderMedia()のonProgressコールバックを使ってレンダリングコストを実測・比較する具体的な手法を解説します。APIのパラメーターが実行時間とコストにどう影響するかを把握すると、自分のユースケースに合った設定を数値で判断できます。
Remotionのチャンク並列モデルを理解する
コスト計測の前提として、Remotionがどうレンダリングを分割するかを把握する必要があります。
renderMediaOnLambda()を呼ぶと、Remotionはcompositionのフレーム総数をframesPerLambdaで割り、複数のチャンクに分割します。各チャンクは独立したLambda関数として起動され、割り当てられたフレーム範囲を並列でレンダリングします。最後にオーケストレーターLambdaがチャンクをffmpegでスティッチして一本の動画に結合します。
import { renderMediaOnLambda } from '@remotion/lambda/client';
const result = await renderMediaOnLambda({
region: 'ap-northeast-1',
functionName: 'remotion-render-4-0-0-mem2048mb-disk2048mb-120sec',
serveUrl: 'https://your-bucket.s3.ap-northeast-1.amazonaws.com/sites/my-comp/',
composition: 'MyVideo',
inputProps: { title: 'テスト動画' },
codec: 'h264',
framesPerLambda: 30, // 1チャンクあたりのフレーム数
concurrencyPerLambda: 1, // 各Lambda内のCPUスレッド数
timeoutInMilliseconds: 120_000,
memorySizeInMb: 2048,
diskSizeInMb: 2048,
});
framesPerLambda: 30で1,800フレームの動画をレンダリングすると、60個のチャンクLambda+1個のオーケストレーターLambda、計61回のLambda起動が発生します。framesPerLambda: 90なら20チャンク+1の21回です。Lambda料金はGB-seconds(メモリ容量×実行時間)と起動回数で計算されるため、framesPerLambdaの値がそのまま起動コストに直結します。この一本の数値が、Lambdaコストの主要なレバーです。
estimatePrice()でコストを事前推定する
@remotion/lambdaはestimatePrice()というユーティリティ関数を提供しています。実際のAWS請求を代替するものではありませんが、framesPerLambdaやメモリ設定を変えたときの影響をコードレベルで事前にシミュレートできます。
import { estimatePrice } from '@remotion/lambda';
// 各チャンクLambdaが平均10秒間実行されると仮定した試算
const estimate = estimatePrice({
region: 'ap-northeast-1',
durationInMilliseconds: 10_000, // 1チャンクあたりの推定実行時間(ms)
memorySizeInMb: 2048,
diskSizeInMb: 2048,
lambdasInvoked: 61, // ceil(1800 / 30) + 1 = 61
});
console.log(estimate.estimatedDisplayCost); // "$0.0042" など
console.log(estimate.currency); // "USD"
durationInMillisecondsには1チャンクあたりの実行時間を渡します。Lambdaは並列実行を前提とするため、ウォールクロック時間は1チャンクの実行時間と一致します。また、試算対象はLambdaのコンピューティングコストのみです。S3のストレージ・リクエスト料金、CloudWatchログ料金、egress帯域幅は含まれません。
framesPerLambdaによるコスト変化を事前比較する
estimatePrice()をラップしてframesPerLambdaの候補を一括比較する関数を作ると、パラメーター選択の根拠を数値で持てます。
function compareFPLConfigs({
totalFrames,
memorySizeInMb,
diskSizeInMb,
region,
estimatedMsPerFrame,
}: {
totalFrames: number;
memorySizeInMb: number;
diskSizeInMb: number;
region: string;
estimatedMsPerFrame: number; // 1フレームあたりのChromiumレンダリング時間(ms)
}) {
const candidates = [15, 20, 30, 60, 90, 150];
return candidates.map((framesPerLambda) => {
const chunks = Math.ceil(totalFrames / framesPerLambda);
const lambdasInvoked = chunks + 1;
// 1チャンクの実行時間 = フレーム数 × 1フレームあたりの時間 + エンコードオーバーヘッド
const chunkDurationMs = framesPerLambda * estimatedMsPerFrame + 2_000;
const estimate = estimatePrice({
region,
durationInMilliseconds: chunkDurationMs,
memorySizeInMb,
diskSizeInMb,
lambdasInvoked,
});
return {
framesPerLambda,
chunks,
lambdasInvoked,
estimatedWallClockMs: chunkDurationMs, // 並列実行のためウォールクロック≈チャンク1本の時間
estimatedDisplayCost: estimate.estimatedDisplayCost,
};
});
}
estimatedMsPerFrameは実際のcompositionで一度計測して求めます。useCurrentFrame()やspring()を多用する複雑なcompositionと、静的テキストだけのcompositionでは1フレームあたりの時間が桁で違うため、composition固有の実測値を使います。
Lambdaレンダリングの実測:getRenderProgress()でコストを回収する
事前推定より精度が高いのは、実際のレンダリング後にgetRenderProgress()から取得したコスト情報を記録する方法です。
import { getRenderProgress } from '@remotion/lambda/client';
async function waitForRenderAndCollectCosts(params: {
region: string;
bucketName: string;
renderId: string;
functionName: string;
}) {
let progress;
do {
await new Promise((r) => setTimeout(r, 3_000));
progress = await getRenderProgress({
region: params.region,
bucketName: params.bucketName,
renderId: params.renderId,
functionName: params.functionName,
});
if (progress.fatalErrorEncountered) {
throw new Error(progress.errors[0]?.message ?? 'Render failed');
}
} while (!progress.done);
return {
outputFile: progress.outputFile,
costs: progress.costs, // アクリュードコスト情報
renderedFrames: progress.renderedFrames,
timeToFinish: progress.timeToFinish, // ウォールクロック時間(ms)
};
}
progress.costsには実際の起動Lambda数・実行時間から集計されたコスト情報が含まれます。timeToFinishはレンダリング開始からオーケストレーターが完了を報告するまでのウォールクロック時間です。この二つを並べて記録すると、「安い設定」と「速い設定」のトレードオフを実データで把握できます。
セルフホストレンダリングの計測:onProgressコールバック
@remotion/rendererのrenderMedia()はNode.jsプロセスとして動作します。コストはCPU時間と使用メモリに比例しますが、計測のアプローチはLambdaとは異なります。
import { renderMedia, selectComposition } from '@remotion/renderer';
const composition = await selectComposition({
serveUrl: '/path/to/bundle',
id: 'MyVideo',
inputProps: { title: 'テスト動画' },
});
const timings: { renderedDoneIn: number | null; encodedDoneIn: number | null } = {
renderedDoneIn: null,
encodedDoneIn: null,
};
const wallStart = Date.now();
await renderMedia({
composition,
serveUrl: '/path/to/bundle',
codec: 'h264',
outputLocation: '/tmp/output.mp4',
concurrency: 4, // 並列Chromiumインスタンス数
offthreadVideoConcurrency: 2, // <OffthreadVideo>のオフスレッドデコード並列数
onProgress: ({ renderedFrames, renderedDoneIn, encodedDoneIn, progress }) => {
// renderedDoneIn: フレームレンダリングフェーズが完了した時点の経過時間(ms)
if (renderedDoneIn !== null && timings.renderedDoneIn === null) {
timings.renderedDoneIn = renderedDoneIn;
}
// encodedDoneIn: エンコーディングフェーズが完了した時点の経過時間(ms)
if (encodedDoneIn !== null && timings.encodedDoneIn === null) {
timings.encodedDoneIn = encodedDoneIn;
}
process.stdout.write(`\r${Math.round(progress * 100)}% | frames: ${renderedFrames}`);
},
});
console.log('\n--- セルフホスト計測結果 ---');
console.log(`フレームレンダリング: ${timings.renderedDoneIn}ms`);
console.log(`エンコーディング: ${timings.encodedDoneIn}ms`);
console.log(`総ウォールクロック: ${Date.now() - wallStart}ms`);
renderedDoneInとencodedDoneInはそれぞれのフェーズが完了した時点の経過時間です。フレームレンダリングフェーズが支配的ならconcurrencyの増加が効きますが、エンコーディングフェーズが支配的ならconcurrencyを増やしても改善しません。ボトルネックがどちらにあるかを知らずにconcurrencyを闇雲に上げることは、コストと効果が見合いません。
concurrencyとoffthreadVideoConcurrencyの関係
concurrencyは同時起動するChromiumインスタンスの数で、各インスタンスが独立したフレーム範囲をレンダリングします。物理コア数を超えて設定すると、コンテキストスイッチのオーバーヘッドと、各インスタンスが消費するRAM(インスタンスあたり300MB〜1GB超)によるメモリ圧迫が発生します。経験則として、物理コア数の60〜80%を上限にして実測で調整します。
offthreadVideoConcurrencyは<OffthreadVideo>コンポーネントを使う場合に関係します。動画ファイルのフレームデコードをChromiumのメインスレッドから切り離して実行するプロセス数です。
// コンポジションに<OffthreadVideo>が複数重なる場合
await renderMedia({
composition,
serveUrl,
codec: 'h264',
outputLocation,
concurrency: 6, // 8コアマシンの75%
offthreadVideoConcurrency: 3, // 動画ソースが最大3本重なる場合
onProgress: ({ progress }) => {
process.stdout.write(`\r${Math.round(progress * 100)}%`);
},
});
<OffthreadVideo>のソース本数とoffthreadVideoConcurrencyが一致していれば、各ソースのデコードが並行して進みフレーム取得のストールを減らせます。超過しても効果はなく、プロセス起動コストだけが増えます。
Cloud Run(@remotion/cloudrun)との比較
Google Cloud Runはコンテナベースで動作し、Lambdaとセルフホストの中間に位置するモデルです。
import { renderMediaOnCloudRun } from '@remotion/cloudrun/client';
const wallStart = Date.now();
const cloudRunResult = await renderMediaOnCloudRun({
region: 'asia-northeast1',
serviceName: 'remotion-render',
serveUrl: 'https://storage.googleapis.com/your-bucket/sites/my-comp/',
composition: 'MyVideo',
inputProps: { title: 'テスト動画' },
codec: 'h264',
outputBucket: 'your-output-bucket',
});
console.log('出力先:', cloudRunResult.publicUrl);
console.log(`ウォールクロック: ${Date.now() - wallStart}ms`);
Cloud RunはリクエストがないときはゼロにスケールするためLambdaに似ていますが、コンテナが「ウォーム」な状態ではChromiumの再初期化コストが発生しません。Lambdaは毎回新しい実行環境でChromiumを起動するため、短いcompositionで多くのチャンクに分割すると初期化オーバーヘッドの割合が大きくなります。1チャンクあたりのレンダリング時間がChromium初期化時間(通常1〜3秒)と比較して短い場合、Cloud Runが有利になるケースがあります。
ただしCloud Runは現時点でLambdaのような超並列チャンク分割を標準サポートしていないため、長尺・高フレームレートの動画ではLambdaの並列チャンク展開の優位性が出ます。
codecの選択がコストに与える影響
codecもコストを左右する変数です。h264・h265・vp9・proresはエンコードの計算量が大きく異なり、Lambdaの実行時間(=コスト)に直接影響します。
// h264: エンコード最速・配信用途のデファクト
await renderMedia({ codec: 'h264', crf: 18, outputLocation });
// h265 (HEVC): 同品質でファイルサイズを約30%削減できるが、エンコード時間は約2倍
await renderMedia({ codec: 'h265', crf: 24, outputLocation });
// prores: エンコード自体は非常に高速だがファイルサイズが巨大(編集マスター用)
await renderMedia({ codec: 'prores', outputLocation });
// gif: フレーム数が多いとdithering処理で著しく遅くなる
await renderMedia({
codec: 'gif',
outputLocation,
everyNthFrame: 2, // フレームを間引いてGIFサイズと処理時間を削減
});
Lambda環境でh265を選ぶと、同じcompositionでも各チャンクLambdaの実行時間が延びて請求コストが上昇します。配信用途ではh264 + crf: 18〜23が実用的なバランスです。crf値はh264のエンコード速度への影響が軽微ですが、h265やvp9ではcrfを1下げるたびにエンコード時間が顕著に増加します。codec選択はframesPerLambdaと同等にコストへ影響するパラメーターです。
実践的なコスト計測ハーネスの構築
本番環境でコストを継続的に追跡するには、レンダリング関数をラップしてメトリクスを記録するパターンが有効です。
import {
renderMediaOnLambda,
getRenderProgress,
estimatePrice,
} from '@remotion/lambda/client';
import type { AwsRegion } from '@remotion/lambda';
interface RenderMetrics {
renderId: string;
wallClockMs: number;
framesPerLambda: number;
estimatedCostDisplay: string;
renderedFrames: number;
}
async function renderWithMetrics(params: {
region: AwsRegion;
functionName: string;
serveUrl: string;
composition: string;
inputProps: Record<string, unknown>;
framesPerLambda: number;
memorySizeInMb: number;
diskSizeInMb: number;
}): Promise<RenderMetrics> {
const wallStart = Date.now();
const { renderId, bucketName } = await renderMediaOnLambda({
...params,
codec: 'h264',
timeoutInMilliseconds: 120_000,
});
let progress;
do {
await new Promise((r) => setTimeout(r, 3_000));
progress = await getRenderProgress({
region: params.region,
bucketName,
renderId,
functionName: params.functionName,
});
if (progress.fatalErrorEncountered) {
throw new Error(progress.errors[0]?.message ?? 'Render failed');
}
} while (!progress.done);
return {
renderId,
wallClockMs: Date.now() - wallStart,
framesPerLambda: params.framesPerLambda,
estimatedCostDisplay: progress.costs?.displayCost ?? 'N/A',
renderedFrames: progress.renderedFrames,
};
}
// 同一compositionでframesPerLambdaを変えて比較する
const configs = [20, 60, 120] as const;
const results = await Promise.all(
configs.map((framesPerLambda) =>
renderWithMetrics({ ...sharedParams, framesPerLambda })
)
);
console.table(results.map(({ framesPerLambda, wallClockMs, estimatedCostDisplay }) => ({
framesPerLambda,
wallClockMs,
estimatedCostDisplay,
})));
このメトリクス構造をデータベースへ蓄積すると、framesPerLambdaやcompositionの複雑度に対するコストの傾向が統計として見えてきます。「速さのためにコストを払っているか、コストのために速さを犠牲にしているか」を意識的に選択できる状態が、最適化の出発点です。
環境選択のフレームワーク
三つの環境は異なるコスト特性を持ちます。
Lambdaはジョブが散発的で高並列が必要な場合に適します。framesPerLambdaを小さくすればウォールクロック時間を短縮できますが起動コストが増加します。コールドスタートを許容できる場合、アイドル時の課金がゼロという特性は強力です。
セルフホストはインスタンスが常時稼働するサービスで、レンダリングジョブが高頻度の場合に有利です。コンピューティングコストはインスタンス時間で固定されるため、単位レンダリングコストはジョブ密度に反比例します。onProgressでフェーズ別のボトルネックを特定し、concurrencyとoffthreadVideoConcurrencyを実測値でチューニングする余地があります。
Cloud Runはコンテナのウォーム状態を生かしつつスケールトゥゼロも必要な中間的ユースケースに向いています。Lambda的な超並列チャンク分割が不要な、中短尺の動画を散発的に生成するケースが最もフィットします。
Wrapping Up
Remotionのレンダリングコストは「コンピューティングの単価×実行時間×並列度」の積で決まりますが、その形は環境によって根本的に異なります。計測なしに環境を選ぶのは、stiffnessを計測せずにspringアニメーションを調整するようなものです。
estimatePrice()はframesPerLambda・メモリ・起動回数をパラメーターとして受け取り、設定変更の影響を事前にシミュレートできる。実際の請求を代替するものではないが、相対比較には十分な精度があるgetRenderProgress().costsで実際のコスト情報を取得しログに残す。これをframesPerLambdaと紐づけて蓄積することが最適化の基礎データになる- セルフホストでは
onProgressのrenderedDoneInとencodedDoneInでフェーズ別にボトルネックを特定してからconcurrencyを調整する - codecは
h264が最速で実用的なデフォルト。h265やvp9はLambdaの実行時間を著しく延ばすため、コスト計測なしに選択しない framesPerLambdaはデフォルト値のまま運用しない。composition固有の実測値に基づいてウォールクロック時間とコストのバランスを意識的に選択する
RenderCompのテンプレートカタログのようにcompositionの複雑度が多様な環境では、この計測ハーネスを各テンプレートのCI/CDパイプラインに組み込んで比較することが、レンダリングコストを継続的に管理する最短経路です。