Remotionのカスタムフォント読み込み完全ガイド — Google Fonts・セルフホスト・ちらつき対策
Remotionのカスタムフォント読み込み完全ガイド — Google Fonts・セルフホスト・ちらつき対策
「指定したフォントが反映されない」「動画の最初の1秒だけ別のフォントで描画されて、途中でパッと切り替わる」——Remotionプロジェクトをいくつか運用したことがあれば、どちらかには必ず遭遇しているはずです。フォント問題がRemotionの頻出トラブルであるのには明確な理由があります。動画レンダラーにおけるフォント読み込みは、通常のWebページとは根本的に動作が違うからです。
この記事では、Remotion 4.x におけるフォント読み込みの全経路——@remotion/google-fonts、@remotion/fonts によるセルフホスト、手動の delayRender() パターン——を整理し、ちらつき・豆腐文字・「ローカルとLambdaで見た目が違う」といった症状ごとのトラブルシューティングまで解説します。
決定論的フレームレンダリングではフォント読み込みが「別物」になる理由
通常のWebサイトでは、フォントの読み込み遅延は見た目の問題にすぎません。ブラウザはまずフォールバックフォントで描画し、Webフォントが届いた時点で差し替えます。ユーザーはほとんど気づきません。
Remotionではそうはいきません。動画の各フレームは、ヘッドレスChromiumページを特定のフレーム番号でスクリーンショットしたものです。フレーム0がキャプチャされる時点でカスタムフォントの読み込みが完了していなければ、フォールバックフォントが動画ファイルに恒久的に焼き込まれます。後から差し替わることはありません——ピクセルはすでにエンコード済みだからです。
ここから2つの典型的な失敗モードが生まれます。
- フォールバックのちらつき。 レンダリング途中でフォントの読み込みが完了し、最初のNフレームだけフォールバック書体で描画された後、動画が目に見えて「切り替わる」。並列レンダリングではチャンクごとにブラウザインスタンスが立ち上がるため、ちらつきが動画の途中に現れることさえあります。
- 静かな誤フォント。 パスの誤り・family名の不一致・ネットワーク障害などでフォントが最後まで読み込まれず、動画全体がChromiumのフォールバックで静かにレンダリングされる。エラーは出ず、「なんとなく見た目が違う」納品物だけが残ります。
どちらの修正も概念は同じです。すべてのフォントが準備完了になるまでフレームキャプチャをブロックすること。Remotionはこれを delayRender() / continueRender() として公開しており、公式のフォントパッケージは内部でこれを呼んでくれます。開発者の仕事は、正しいパッケージを使い、ブロックを迂回してしまうパターンを避けることです。キネティックタイポグラフィやテキストリビールのような「書体そのものが主役」の動画では、この重要性は倍増します。
@remotion/google-fonts による型安全な読み込み
@remotion/google-fonts はフォントファミリーごとに型付きのimportを提供するパッケージです。
npx remotion add @remotion/google-fonts
ファミリーを読み込み、返された fontFamily 文字列を使います。
import { loadFont } from '@remotion/google-fonts/Inter';
const { fontFamily } = loadFont('normal', {
weights: ['400', '700'],
subsets: ['latin'],
});
export const Headline: React.FC<{ text: string }> = ({ text }) => {
return (
<div style={{ fontFamily, fontSize: 90, fontWeight: 700 }}>
{text}
</div>
);
};
押さえておくべき点は3つです。
- レンダリングを自動でブロックします。
loadFont()は内部でdelayRender()ハンドルを登録するため、フォントが利用可能になる前にフレームがキャプチャされることはありません。明示的にawaitしたい場合のためのwaitUntilDone()関数も返します。 weightsとsubsetsを必ず指定してください。 引数なしのloadFont()はそのファミリーの全スタイル・全ウェイト・全サブセットをダウンロードします。ネットワークリクエストが膨れ上がり、レンダータイムアウトの定番原因になります。- レンダリングのたびに外部サーバーからフォントを取得します。 新しいブラウザインスタンスが立ち上がるたび——ローカルレンダリング、CI、Lambdaの各チャンク——毎回Googleのサーバーからファイルをダウンロードし直します。
この最後の点が決定的です。レンダリングパイプラインが第三者のフォントサーバーに依存するということは、外部障害点を抱え込むということです。オフラインでレンダリングできず、ビルドの再現性が保証されず、障害やファイアウォール設定ひとつで動画が完成しなくなります。繰り返しレンダリングするプロジェクトや販売物では、セルフホストを既定にしてください。 手間はほとんど変わりません。次のセクションがその方法です。
@remotion/fonts + loadFont によるセルフホスト(本番の既定)
Remotion 4.0.164 以降で使える @remotion/fonts パッケージは、プロジェクト内に置いたフォントファイルを読み込みます。
npx remotion add @remotion/fonts
.woff2 ファイルを public/ フォルダに置き、staticFile() で参照します。プロジェクトで使うフォントを一括で読み込む専用の fonts.ts モジュールを作るのがきれいなパターンです。
// src/fonts.ts
import { loadFont } from '@remotion/fonts';
import { staticFile } from 'remotion';
export const brandFont = 'Inter';
export const fontsReady = Promise.all([
loadFont({
family: brandFont,
url: staticFile('fonts/Inter-Regular.woff2'),
weight: '400',
style: 'normal',
}),
loadFont({
family: brandFont,
url: staticFile('fonts/Inter-Bold.woff2'),
weight: '700',
style: 'normal',
}),
]);
@remotion/fonts の loadFont() も内部で delayRender() を処理するため、ブロック処理を自分で配線する必要はありません。副作用のためにモジュールをimportするだけで、フレームキャプチャ開始前にフォントの準備が保証されます。
import {
AbsoluteFill,
Sequence,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
import { brandFont } from './fonts'; // importした時点でフレーム0より前に読み込みが始まる
const FALLBACK = '-apple-system, "Segoe UI", Roboto, sans-serif';
const AnimatedLine: React.FC<{ text: string }> = ({ text }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = spring({
frame,
fps,
config: { mass: 0.6, stiffness: 160, damping: 14 },
});
const translateY = interpolate(enter, [0, 1], [40, 0]);
const opacity = interpolate(enter, [0, 1], [0, 1], {
extrapolateRight: 'clamp',
});
return (
<div
style={{
fontFamily: `${brandFont}, ${FALLBACK}`,
fontSize: 84,
fontWeight: 700,
color: '#ffffff',
transform: `translateY(${translateY}px)`,
opacity,
}}
>
{text}
</div>
);
};
export const TitleCard: React.FC = () => {
return (
<AbsoluteFill
style={{
background: '#0a0a0a',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Sequence layout="none">
<AnimatedLine text="セルフホストフォントは" />
</Sequence>
<Sequence from={12} layout="none">
<AnimatedLine text="フレーム0より前に準備完了" />
</Sequence>
</AbsoluteFill>
);
};
カスタムファミリーの後ろに続くフォールバックスタックに注目してください。万一フォントの読み込みに失敗しても、-apple-system, "Segoe UI", Roboto, sans-serif のようなシステムスタックなら破綻せず品位を保てます。日本語テキストなら "Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif のようなスタックを使います。
なぜ .woff2 なのか。広くサポートされたフォーマットの中で最小であり、RemotionのChromiumがネイティブに扱えるからです。手元に .ttf や .otf しかない場合は変換してください。ファイルが小さいほどレンダリング開始が速くなります。チャンクごとに独立してフォントを読み込むLambdaでは特に効きます。
delayRender() パターン — フォント準備完了までレンダリングを止める
上記の2パッケージは delayRender() を代わりに呼んでくれますが、プリミティブを理解しておくと、独自のアセットサーバーからの取得・propsに応じた条件付き読み込み・明示的なエラーハンドリングなど、カスタムな挙動が必要になったときに困りません。手動パターンではWeb標準の FontFace APIを使います。
import { cancelRender, continueRender, delayRender, staticFile } from 'remotion';
const handle = delayRender('Loading BrandSans');
const brandSans = new FontFace(
'BrandSans',
`url('${staticFile('fonts/BrandSans-Regular.woff2')}') format('woff2')`,
);
brandSans
.load()
.then(() => {
document.fonts.add(brandSans);
continueRender(handle);
})
.catch((err) => {
cancelRender(err); // 誤ったフォントで納品するくらいなら、レンダリングを明示的に失敗させる
});
delayRender() のルールは以下の通りです。
- すべてのハンドルは必ず解決すること。 成功時は
continueRender(handle)、失敗時はcancelRender(err)を呼びます。未解決のハンドルはタイムアウトでレンダリングを失敗させます。 - デフォルトのタイムアウトは30秒。 大きなフォントや遅い回線で足りない場合は、呼び出しごとに
delayRender('Loading font', { timeoutInMilliseconds: 60000 })で延長するか、レンダーフラグ--timeoutでグローバルに引き上げます。 - ハンドルにはラベルを付けること。 ラベル文字列はタイムアウトエラーに表示されるため、「どこかのdelayRenderが未解決」が「フォント読み込みで詰まっている」に変わります。
- 読み込みは
useEffectではなくモジュールスコープで実行すること。delayRender()ハンドルのないuseEffectは、最初のフレームがすでにキャプチャされた後に走る可能性があります。これがフォールバックちらつきの単独最多の原因です。モジュールスコープでの読み込みは、どのコンポーネントが描画されるよりも先に始まります。
@remotion/google-fonts を使っていて「フォント準備完了後に処理を続けたい」(テキストの寸法測定など)場合は、ブロック機構を再発明せず、返り値の waitUntilDone() プロミスを使ってください。
ウェイト・サブセット・バリアブルフォント
実際に使うウェイトをすべて読み込む——そしてそれ以上は読み込まない。 静的フォントではウェイトごとにファイルが分かれています。fontWeight: 700 を指定したのに400のファイルしか読み込んでいない場合、Chromiumはレギュラーのアウトラインを太らせた疑似ボールドを合成します。描画はされますが微妙に崩れており、本物のボールドで作られたデザインカンプとは一致しません。
サブセットは収録される文字の範囲を決めます。 subsets: ['latin'] はダウンロードを小さく保ちますが、latin-ext のアクセント付き文字・キリル文字・CJKはフォールバックグリフか豆腐(□)になります。動画が実際に表示する言語に合わせてサブセットを選んでください。想定外の文字を含みうる動的データにも注意が必要です。
バリアブルフォントはウェイト問題を1ファイルに畳み込みます。 バリアブルの .woff2 は1ファイルでウェイト範囲全体をカバーし、FontFace のウェイト範囲記述子で宣言できます。
const interVariable = new FontFace(
'InterVariable',
`url('${staticFile('fonts/InterVariable.woff2')}') format('woff2')`,
{ weight: '100 900' },
);
document.fonts.add() の後は、100〜900のどの fontWeight も本物の補間アウトラインで描画されます。interpolate() で fontWeight をフレームごとにアニメーションさせる「ウェイトモーフィング」も可能です。静的フォントにはできない芸当です。
日本語・CJKフォントの戦略 — ファイルサイズとサブセット化
CJKフォントはまったくの別階級です。日本語フルフォントは1ウェイトで数千グリフを抱え、woff2にしても数MB級になるのが普通です。優先順に3つの戦略があります。
1. レンダリングするテキストにサブセット化する。 コンポジションのテキストが既知(または有限のデータセット由来)なら、fonttoolsの pyftsubset で実際に使うグリフだけに絞り込みます。
pip install fonttools brotli
pyftsubset NotoSansJP-Regular.ttf \
--text-file=all-strings-used.txt \
--flavor=woff2 \
--output-file=public/fonts/NotoSansJP-subset.woff2
実際の台本テキストをカバーするサブセットは、フルフォントより2桁小さくなることも珍しくありません。データ駆動のパイプラインでは、データソースからサブセットを再生成するビルドステップを組んでおくとよいでしょう。
2. 文字集合の標準にサブセット化する。 テキストが動的で範囲を限定できない場合(ユーザー入力・APIデータ)は、標準的なカバレッジに絞ります。かな+常用漢字で現代日本語テキストの大部分をカバーできます。これをメインファイルとして同梱し、稀少グリフはフォールバックを許容します。
3. フルフォントを同梱してタイムアウトを延ばす。 最も単純で最も重い方法です。ローカルレンダリングなら許容範囲ですが、分散レンダリングでは全チャンクが読み込みコストを支払うため、delayRender のタイムアウト延長とセットで使います。
@remotion/google-fonts でCJKファミリーを使う場合の補足: Google FontsはCJKフォントを数十のunicode-rangeスライスに分割配信するため、Remotionでは「ネットワークリクエストが多すぎる」警告が出ることがあります。まさにこのケースのために ignoreTooManyRequestsWarning オプションが存在しますが、これは同時に「セルフホストのサブセットに切り替えたほうがよい」というシグナルでもあります。
トラブルシューティング — ちらつき・豆腐・Lambda差異
最初の数フレームだけ違うフォントで、その後ちらつく。
フォント読み込みがレンダリングをブロックしていません。定番の犯人は、delayRender() ハンドルなしの useEffect 内での読み込み、またはブロック機構ゼロの手書きCSS @font-face です。@remotion/fonts か前述の手動 delayRender パターンで、モジュールスコープに読み込みを移してください。
動画全体がフォールバックフォントでレンダリングされ、エラーも出ない。
順に3点確認します。まずfamily名——style={{ fontFamily }} の文字列は loadFont() に渡した family と完全一致している必要があります。次にファイルパス——staticFile() のパスのtypoは404になりますが、FontFace.load() はそれをrejectされたプロミスとしてしか表面化しないことがあります。だからこそcatchして cancelRender するパターンが「静かに続行」に勝ります。最後に、読み込みモジュールが実際にレンダリング対象のコンポジションからimportされているかを確認します。
一部の文字が豆腐(□)になる、または別書体で描画される。
読み込んだサブセットにそのグリフが含まれていません。subsets: ['latin'] のまま人名やデータにアクセント付き文字・CJKが混ざるケースが典型です。追加サブセットを読み込むか、セルフホストのファイルを広いカバレッジでサブセット化し直します。
ローカルでは正しいのにLambdaでは違う。 手元のマシンが、レンダリング環境には存在しないフォントを解決しています。ローカルでは、コードで一切読み込んでいなくてもOSにインストール済みのフォントが描画に使われます。Lambdaランタイムに載っているのは最小限のフォントセット——数ウェイトのNoto Sans、Noto Color Emoji、スクリプト別のNotoフォント少々——だけで、それ以外はすべてコードで明示的に読み込む必要があります。原則はひとつ、自分のマシン以外でシステムフォントの存在を当てにしないこと。環境差異の詳細はRemotion Lambdaレンダリングガイドも参照してください。
delayRenderタイムアウトでレンダリングが失敗する。
30秒のデフォルトに収まらない巨大フォント(サブセット化していないCJKフォントで頻発)か、死んだネットワークパスが原因です。フォントをサブセット化し、リモートサーバーではなくバンドルから読み込めるようセルフホストする。timeoutInMilliseconds の引き上げは最後の手段にしてください。
テンプレートを配布・販売するときのフォントライセンス
フォントの読み込みは技術の問題ですが、フォントの配布は法律の問題です。Remotionテンプレートを販売したりソースコードをクライアントに納品したりする場合、public/fonts/ のフォントファイルは成果物と一緒に移動します。
- オープンライセンス(SIL OFLなど)。 InterやNotoファミリーのようなフォントは、商用・非商用を問わず、より大きな著作物の内部に同梱して再配布できるようライセンスされています。ライセンステキストをフォントファイルの隣に置いておくこと。なお、OFLはフォントファイル単体での販売を禁じています。
- 商用フォント。 標準的なデスクトップライセンスに、購入者へのフォントファイル再配布権が含まれることはまずありません。テンプレートのデザインが商用書体に依存する場合は、ファイルを含めずに出荷します。
fontFamilyを明確なシステムフォールバックスタック付きで宣言し、購入者がどこでライセンスを取得できるかをドキュメント化し、購入者自身のコピーをpublic/fonts/に置いてもらう構成です。 - ホスティング型サービスのフォント。 ホスティングサービスが配信するファイルは「配信」のためにライセンスされているのであって、「抽出」のためではありません。ネットワークタブからwoff2を抜き出してコミットしてはいけません。
テンプレート作者の実務的なデフォルトは、OFLライセンスのフォントを土台にし、セルフホストし、ライセンスファイルをリポジトリに含めることです。購入者にとって摩擦ゼロ、作者にとって法的曖昧さゼロになります。
よくある質問(FAQ)
Q: @remotion/google-fonts や @remotion/fonts を使うとき、delayRender() を自分で呼ぶ必要はありますか?
不要です。どちらのパッケージも内部で delayRender() ハンドルの登録と解決を行います。手動の delayRender() が必要になるのは、素の FontFace APIやカスタムのfetchなど、独自の仕組みでフォントを読み込む場合だけです。
Q: 自分のPCにインストール済みのフォントをそのまま使ってはだめですか?
ローカルのプレビューとレンダリングでは動きます——そして、Lambdaや他のメンバーのマシンなど、プロジェクトが別の場所でレンダリングされた瞬間に壊れます。フォントは必ずプロジェクト内のファイルから明示的に読み込んでください。
Q: フォントファイルのフォーマットは何にすべきですか?
可能な限り woff2 です。一般的なフォーマットの中で最小で、RemotionのChromiumが完全にサポートしています。.ttf / .otf はfonttools(--flavor=woff2)で変換できます。
Q: loadFont() はコンポーネント内とモジュールスコープ、どちらで呼ぶべきですか?
モジュールスコープです。importされたファイルのトップレベルで読み込めば、どのフレームのキャプチャよりも先に開始されます。コンポーネント本体で毎レンダー呼ぶのは無駄が多く、useEffect 内は初期フレームのキャプチャ後に走りうるため積極的に危険です。
Q: フォント読み込みはレンダリングの決定論性を損ないませんか?
損ないません——それこそがブロック機構の目的です。フレームキャプチャ開始前にフォントの読み込みが完了していれば、全フレームが同一のフォントデータで描画され、再レンダリングしても同一のピクセルが得られます。
Q: フォントは何書体まで使っていいですか?
ファミリー・ウェイト・サブセットが増えるほど、レンダリングが起動する全ブラウザインスタンスのスタートアップ時間が延びます。多くのコンポジションでは「2ファミリー×各2〜3ウェイト」が健全な上限です。それを超えるならバリアブルフォントを検討してください。
まとめ
- Remotionはフレームを決定論的にキャプチャするため、フォントはフレームキャプチャ前に読み込み完了していなければならない。さもないとフォールバックフォントが動画に焼き込まれる。
@remotion/google-fontsは手軽だが、レンダリングのたびに外部サーバーへ取りに行く。使うならweightsとsubsetsで必ず絞る。@remotion/fonts+staticFile()によるセルフホストが本番品質の構成。ネットワーク依存なし・再現可能なレンダリング・オフラインでもLambdaでも動く。- 両パッケージともレンダリングを自動でブロックする。カスタム読み込みでは
FontFaceとdelayRender()/continueRender()/cancelRender()を組み合わせる。 - Chromiumの疑似ボールドに頼らず本物のウェイトファイル(またはバリアブルフォント)を読み込む。サブセットは実際のテキストに合わせ、CJKフォントは積極的にサブセット化する。
- 自分のマシン以外でシステムフォントを当てにしない。配布するテンプレートにフォントファイルを含める前に再配布権を確認する。
セットアップごと省略するなら RenderComp →
RenderCompのテキスト主体のRemotionテンプレート——キネティックタイトルカード、ローワーサード、字幕システム——は、動作するフォント読み込みが配線済みの編集可能なTypeScriptソースで提供されます。モジュールをひとつ書き換えるだけで、自前のセルフホストフォントに差し替えられます。