R RenderComp
remotion lambda cost-optimization typescript rendering

Remotion Lambdaのコスト計算式と買い切りの損益分岐点

執筆: RenderComp チーム 編集方針

Remotionは動画を「確定的なフレームシーケンス」として扱います。useCurrentFrame() が返す整数は常に同じピクセルを生成するため、フレーム1からフレーム900まで、どの区間でも独立してレンダリングできます。この性質がクラウドレンダリングを合理的にします。並列度を上げれば時間を短縮でき、下げればコストを抑えられる、明示的なトレードオフが存在します。

しかしそのトレードオフには罠があります。Remotion Lambdaを使ったクラウドレンダリングはAWS LambdaのGB秒単位で課金され、ビデオ生成SaaSは「出力動画1分あたり」という直感的な単価を提示しています。この2つの課金体系を並べると、どちらが安いか即座には判断できません。

本記事では、GB秒を「出力1分あたりコスト」へ変換する式を段階的に導出し、TypeScriptで計測・比較できるユーティリティを構築します。さらに、買い切りテンプレートを自社インフラで動かすモデルとSaaSの損益分岐点を計算する式も示します。


Remotion Lambda のコスト構造

Remotion Lambdaは1本の動画をframesPerLambdaフレームずつに分割し、複数のLambda関数へ並列ディスパッチします。AWS Lambdaの課金式はシンプルです。

GB秒 = メモリ(GB) × 実行時間(秒)
課金 = GB秒 × GB秒単価

ただし、並列実行されるLambda関数の数だけGB秒が加算されます。10本のLambdaを同時に走らせれば、単一実行の10倍のGB秒を消費します。壁時計時間は約1/10に短縮されますが、コストの合計は変わりません。

framesPerLambda とウォームアップコストの非線形な関係

framesPerLambdaを小さくすると起動数が増え、フレームが細かく並列化されます。直感的には「並列度が上がれば速くなる、でもコストは同じ」と思いがちですが、実際は起動ごとに固定コストがかかります

Remotion Lambdaは各Lambda関数の起動時にPuppeteerのブラウザコンテキストを初期化します。このウォームアップはフレーム数によらず数秒かかり、GB秒として課金されます。

// framesPerLambda とウォームアップの合計コストを試算する
type LambdaCostParams = {
  totalFrames: number;
  framesPerLambda: number;
  memorySizeMb: number;
  warmupSeconds: number;      // ブラウザ初期化の固定コスト(実測値を入れる)
  secondsPerFrame: number;    // 1フレームのレンダリング時間(実測値を入れる)
};

function estimateGbSeconds(p: LambdaCostParams): number {
  const invocations = Math.ceil(p.totalFrames / p.framesPerLambda);
  const memoryGb = p.memorySizeMb / 1024;

  // 各Lambda関数の実行時間 = ウォームアップ + フレーム数 × 1フレーム時間
  const secondsPerInvocation =
    p.warmupSeconds + p.framesPerLambda * p.secondsPerFrame;

  return invocations * memoryGb * secondsPerInvocation;
}

// 30fps × 60秒 = 1800フレームの例
// ウォームアップ5秒、1フレーム0.5秒(実測値はコンポジション依存)
const base: LambdaCostParams = {
  totalFrames: 1800,
  memorySizeMb: 1024,
  warmupSeconds: 5,
  secondsPerFrame: 0.5,
  framesPerLambda: 0, // 後で差し替え
};

for (const fpl of [20, 60, 120, 180]) {
  const gbSec = estimateGbSeconds({ ...base, framesPerLambda: fpl });
  const invocations = Math.ceil(1800 / fpl);
  console.log(`fpl=${fpl}: ${invocations}回起動 → ${gbSec.toFixed(1)} GB秒`);
}
// fpl=20:  90回起動 → 1350.0 GB秒
// fpl=60:  30回起動 →  645.0 GB秒
// fpl=120: 15回起動 →  397.5 GB秒
// fpl=180: 10回起動 →  320.0 GB秒

ウォームアップが5秒ある状況では、framesPerLambda: 20framesPerLambda: 180でGB秒が4倍以上異なります。コストを優先するなら、framesPerLambdaは大きめに設定するのが原則です。ウォームアップが短いコンポジションや速度優先の要件では小さい値が合理的です。この値はベンチマークなしに決められないため、後述の実測アプローチを組み合わせてください。


1分あたりコストへの正規化

SaaSの「出力1分あたり」課金と比較するため、Lambda課金を同じ単位に変換します。

C_per_min = (ΣGB秒 × P_gbs) / D_output_min

ΣGB秒は全Lambda関数の合計GB秒、P_gbsはGB秒あたりの単価(クラウドプロバイダーの料金表を参照)、D_output_minは出力動画の長さ(分)です。

問題はΣGB秒が事前に正確に計算できない点です。コンポジションの複雑さによって1フレームあたりの描画時間が大きくブレます。そこで実際にレンダリングしてgetRenderProgressが返すコスト推定値を使います。

import {
  renderMediaOnLambda,
  getRenderProgress,
} from "@remotion/lambda/client";

type MeasureParams = {
  region: string;
  functionName: string;
  serveUrl: string;
  composition: string;
  framesPerLambda: number;
  memorySizeInMb: number;
  // 出力分数計算用(compositionの設定値を渡す)
  durationInFrames: number;
  fps: number;
};

async function measureCostPerOutputMin(
  p: MeasureParams,
): Promise<{ estimatedCostUsd: number; costPerOutputMin: number }> {
  const { renderId, bucketName } = await renderMediaOnLambda({
    region: p.region,
    functionName: p.functionName,
    serveUrl: p.serveUrl,
    composition: p.composition,
    codec: "h264",
    inputProps: {},
    framesPerLambda: p.framesPerLambda,
    memorySizeInMb: p.memorySizeInMb,
  });

  let progress = await getRenderProgress({
    renderId,
    bucketName,
    functionName: p.functionName,
    region: p.region,
  });

  while (!progress.done) {
    await new Promise((r) => setTimeout(r, 2000));
    progress = await getRenderProgress({
      renderId,
      bucketName,
      functionName: p.functionName,
      region: p.region,
    });
  }

  const outputMin = p.durationInFrames / p.fps / 60;
  const estimatedCostUsd = progress.costs.estimatedCost;

  return {
    estimatedCostUsd,
    costPerOutputMin: estimatedCostUsd / outputMin,
  };
}

progress.costs.estimatedCostはRemotion Lambdaが内部で計算した推定値です。AWS Cost Explorerの請求と数%の誤差が生じる場合があるため、サンプルを複数回取って中央値を使うと外れ値(Lambda cold startスパイク)の影響を抑えられます。


Lambda メモリとCPU割り当ての関係

AWSの仕様上、Lambda関数のvCPU割り当てはメモリに比例します。1769MBが1vCPU相当で、2048MBは約1.16vCPUになります。CPUバウンドなレンダリングではメモリを増やすとフレームあたりの描画時間が短くなります。

課金はメモリ×時間なので、理論上は完全線形スケーリングならコストが変わりません。しかし実際はPuppeteerのメモリプレッシャーが解消されることで線形を超えた速度向上が得られることがあり、トータルGB秒が下がるケースがあります。

// メモリ設定別のコストを実測して比較する
async function benchmarkMemoryTiers(baseParams: MeasureParams) {
  const tiers = [1024, 2048, 3008] as const;
  const results: Record<number, number> = {};

  for (const memMb of tiers) {
    const { costPerOutputMin } = await measureCostPerOutputMin({
      ...baseParams,
      memorySizeInMb: memMb,
    });
    results[memMb] = costPerOutputMin;
    console.log(`${memMb}MB: $${costPerOutputMin.toFixed(5)}/出力分`);
  }

  // コストが最小のメモリ設定を選ぶ
  const optimal = tiers.reduce((best, tier) =>
    results[tier] < results[best] ? tier : best,
  );
  console.log(`最適メモリ: ${optimal}MB`);
  return optimal;
}

3008MBで1024MBより安くなるコンポジションは、アニメーション層が多く並列デコードがボトルネックになっているケースに多い傾向があります。まず1024MBと2048MBを比較し、2048MBでGB秒が実際に減っていれば3008MBも試す、という順序が効率的です。


買い切りテンプレートとの損益分岐点

テンプレートを一度購入して自社Lambdaで動かすモデルでは、初期費用P_templateを支払えば以降は実行コストC_lambdaのみになります。SaaSは都度R_saas(出力1分あたり)がかかります。

損益分岐点D_breakは次の等式から導けます。

P_template + C_lambda × D_break = R_saas × D_break
D_break = P_template / (R_saas - C_lambda)

R_saas > C_lambdaである限り、D_break分の動画を生成した時点でテンプレートのコストが回収されます。

type PricingModel = {
  templatePriceUsd: number;
  lambdaCostPerOutputMin: number; // measureCostPerOutputMin() の実測値
  saasCostPerOutputMin: number;   // SaaSの料金表から取得
};

function calcBreakeven(m: PricingModel): number | null {
  const saving = m.saasCostPerOutputMin - m.lambdaCostPerOutputMin;
  if (saving <= 0) {
    // Lambda側が割高——buy-onceの経済的メリットなし
    return null;
  }
  return m.templatePriceUsd / saving;
}

function calcCumulativeCost(
  m: PricingModel,
  outputMin: number,
): { saasTotal: number; buyOnceTotal: number } {
  return {
    saasTotal: m.saasCostPerOutputMin * outputMin,
    buyOnceTotal: m.templatePriceUsd + m.lambdaCostPerOutputMin * outputMin,
  };
}

// --- 使用例(価格はプレースホルダー。実際の料金表と実測値を代入してください) ---
const model: PricingModel = {
  templatePriceUsd: 49,
  lambdaCostPerOutputMin: 0.012, // 実測値
  saasCostPerOutputMin: 0.08,    // SaaS料金表
};

const breakeven = calcBreakeven(model);
if (breakeven !== null) {
  console.log(`損益分岐点: 出力${breakeven.toFixed(0)}分で回収`);

  // 任意の生成量でのコスト比較
  [200, 500, 1000, 2000].forEach((min) => {
    const { saasTotal, buyOnceTotal } = calcCumulativeCost(model, min);
    const diff = saasTotal - buyOnceTotal;
    console.log(
      `${min}分時点 — SaaS: $${saasTotal.toFixed(2)} / 買い切り: $${buyOnceTotal.toFixed(2)} (差: $${diff.toFixed(2)})`,
    );
  });
}

R_saasC_lambdaの2〜3倍程度であれば、数百分の出力で回収できるケースが多いです。SaaSがインフラコストを限界まで圧縮して提供している場合、小規模利用ではSaaSの方が有利なこともあります。この計算は料金改定のたびに再実行する運用フローを組んでおくと判断がブレません。


コンポジションの複雑さがコストに与える影響

lambdaCostPerOutputMinの精度は、コンポジションの複雑さに強く依存します。コストが増大しやすいパターンを把握しておきましょう。

import { spring, interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill, Sequence } from "remotion";

// コスト増大パターン: stiffness が高いspring を複数同時使用
const HeavyCard: React.FC<{ delay: number }> = ({ delay }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // stiffness: 300 はデフォルト100の約3倍の収束計算が必要
  const opacity = spring({ frame: frame - delay, fps, config: { stiffness: 300, damping: 20 }, from: 0, to: 1 });
  const y = spring({ frame: frame - delay, fps, config: { stiffness: 300, damping: 20 }, from: 40, to: 0 });

  return (
    <div style={{ opacity, transform: `translateY(${y}px)` }}>
      {/* コンテンツ */}
    </div>
  );
};

// 12枚のカードをずらして表示する構成
// → 各フレームで最大12個のspringを並行計算
const GridComposition: React.FC = () => (
  <AbsoluteFill>
    {Array.from({ length: 12 }, (_, i) => (
      <Sequence key={i} from={i * 5}>
        <HeavyCard delay={0} />
      </Sequence>
    ))}
  </AbsoluteFill>
);

このような構成では単純なテキストアニメーションと比べて1フレームあたりの描画時間が3〜5倍になることがあります。framesPerLambdaを調整するより、stiffnessを下げるか、アクティブなSpringの同時数を減らす方が根本的なコスト削減になります。

コンポジションを変更したら必ず再計測してください。最適化前後でGB秒が大きく変わります。


計測値の管理

同じコンポジションを繰り返しレンダリングする場合、1フレームあたりの実行時間は安定します。実測結果を蓄積し損益分岐計算に再利用する仕組みを持つと、意思決定の精度が上がります。

type RenderCostSample = {
  compositionId: string;
  memorySizeMb: number;
  framesPerLambda: number;
  sampledAt: string;           // ISO 8601
  estimatedCostUsd: number;
  outputMin: number;
};

function medianCostPerOutputMin(samples: RenderCostSample[]): number {
  const costs = samples
    .map((s) => s.estimatedCostUsd / s.outputMin)
    .sort((a, b) => a - b);
  const mid = Math.floor(costs.length / 2);
  return costs.length % 2 === 0
    ? (costs[mid - 1] + costs[mid]) / 2
    : costs[mid];
}

中央値を使うのは、Lambda cold startによるスパイクが平均値を引き上げるためです。10サンプル以上あれば中央値でかなり安定した推定が得られます。


まとめ

Remotion LambdaのGB秒課金を「出力1分あたり」に正規化するポイントは3つです。

1. framesPerLambdaはウォームアップコストと並列度のトレードオフを決める。ウォームアップが支配的な場合は大きめの値がコスト効率を高めます。速度優先なら小さく、コスト優先なら大きく、実測して判断してください。

2. メモリ設定は計測前に固定しない。1024MBと2048MBを同じコンポジションで実測し、どちらがGB秒を少なくするかを確認してから設定を決めます。複雑なコンポジションほどメモリ増量の効果が大きい傾向があります。

3. 損益分岐点は動的に再計算する。SaaS料金の改定や自社インフラコストの変化で比較結果は変わります。calcBreakeven()を定期実行するスクリプトを持つと判断がブレません。

RenderCompカタログのテンプレートはレンダリング負荷があらかじめチューニングされており、1フレームあたりの実行時間が比較的安定しています。新規コンポジションを一から設計するより損益分岐計算の出発点として使いやすいのも、こうした最適化が入っているためです。

生成量が読めない初期フェーズはSaaSで実績を積み、蓄積した出力分数とcalcBreakeven()の結果が交差したタイミングで移行を検討する——これが現実的な判断フローです。

販売中

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

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

料金プランを見る →