R RenderComp
remotion lambda cloud-run rendering cost-optimization

Remotionレンダリングコスト:Lambda・セルフホスト・Cloud Run比較

執筆: RenderComp チーム 編集方針

Remotionで動画を生成するとき、コストの本質は「決定論的なフレーム計算をどの計算資源でいつ実行するか」という一点に集約されます。30fps・60秒の動画は1,800フレームの独立したReactレンダリングサイクルとして分解でき、その並列度と実行環境がコストの形を決定します。

ウォールクロック時間を最小化すればコストが最大になるケースがあり、逆にコストを下げようとすれば処理時間が延びます。Lambdaのようなサーバーレスモデルとセルフホストではコストのドライバーが根本的に異なるため、「どちらが安いか」という問いに対する正確な答えは計測なしには出せません。自分のcompositionの複雑度、ジョブの発生頻度、許容できるウォールクロック時間。この三変数の組み合わせが正解を決めます。

この記事では、@remotion/lambdaestimatePrice()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/lambdaestimatePrice()というユーティリティ関数を提供しています。実際の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/rendererrenderMedia()は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`);

renderedDoneInencodedDoneInはそれぞれのフェーズが完了した時点の経過時間です。フレームレンダリングフェーズが支配的ならconcurrencyの増加が効きますが、エンコーディングフェーズが支配的ならconcurrencyを増やしても改善しません。ボトルネックがどちらにあるかを知らずにconcurrencyを闇雲に上げることは、コストと効果が見合いません。

concurrencyoffthreadVideoConcurrencyの関係

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もコストを左右する変数です。h264h265vp9proresはエンコードの計算量が大きく異なり、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のエンコード速度への影響が軽微ですが、h265vp9では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でフェーズ別のボトルネックを特定し、concurrencyoffthreadVideoConcurrencyを実測値でチューニングする余地があります。

Cloud Runはコンテナのウォーム状態を生かしつつスケールトゥゼロも必要な中間的ユースケースに向いています。Lambda的な超並列チャンク分割が不要な、中短尺の動画を散発的に生成するケースが最もフィットします。


Wrapping Up

Remotionのレンダリングコストは「コンピューティングの単価×実行時間×並列度」の積で決まりますが、その形は環境によって根本的に異なります。計測なしに環境を選ぶのは、stiffnessを計測せずにspringアニメーションを調整するようなものです。

  • estimatePrice()framesPerLambda・メモリ・起動回数をパラメーターとして受け取り、設定変更の影響を事前にシミュレートできる。実際の請求を代替するものではないが、相対比較には十分な精度がある
  • getRenderProgress().costsで実際のコスト情報を取得しログに残す。これをframesPerLambdaと紐づけて蓄積することが最適化の基礎データになる
  • セルフホストではonProgressrenderedDoneInencodedDoneInでフェーズ別にボトルネックを特定してからconcurrencyを調整する
  • codecはh264が最速で実用的なデフォルト。h265vp9はLambdaの実行時間を著しく延ばすため、コスト計測なしに選択しない
  • framesPerLambdaはデフォルト値のまま運用しない。composition固有の実測値に基づいてウォールクロック時間とコストのバランスを意識的に選択する

RenderCompのテンプレートカタログのようにcompositionの複雑度が多様な環境では、この計測ハーネスを各テンプレートのCI/CDパイプラインに組み込んで比較することが、レンダリングコストを継続的に管理する最短経路です。

販売中

1,000以上のRemotionテンプレートを一括入手

買い切り(一括払い)・サブスクなし・生涯アップデート無料。TypeScript製。

料金プランを見る →