マガジン

フォントが読み込み前に描画されて文字化けするときの対処

遊びをつくる

執筆にClaudeを使用しています。

玉とゴールが見えるゲーム模型を、小さなフレームで切り取る写真風イメージ。
記事のテーマをもとに制作したAI生成イラスト。

ブラウザゲームでテキストを表示すると、最初の一瞬だけ別のフォントで描かれ、その後ガタッと切り替わる——あるいはCanvasに描いたはずの文字が豆腐(□)になっている——という経験をした方は多いと思います。これはフォントの読み込みが完了する前にゲームの描画ループが走り出してしまうことで起きます。 JavaScriptのレンダリングエンジンはページの読み込みと並行してゲームを動かし始めるため、@font-faceや<link>で外部フォントを指定しただけでは「フォントが使えるようになった瞬間」を待ってくれません。<canvas>への描画、CSSアニメーション、WebGLのテクスチャ生成など、どの手段でテキストを扱っていても同じ問題が発生します。 この記事では、フォント未読み込みによる文字化けが起きる仕組みを整理したうえで、状況別の対処手順と、よくある落とし穴を具体的に説明します。

この記事の目次
  1. なぜ読み込み前に描画が走るのか
  2. 対処の全体方針:「待つ」か「事前に読ませる」か
  3. 「待つ」:FontFace APIとdocument.fonts.readyを使う
  4. 「事前に読ませる」:font-displayとpreloadの活用
  5. Canvasで特に起きやすい落とし穴
  6. 複数フォントを使うゲームでの管理方針
  7. 次の行動
  8. 参考資料

なぜ読み込み前に描画が走るのか

ブラウザはHTMLを解析しながら並列でリソースを取得します。DOMContentLoadedが発火した時点では、外部CSSに書かれた@font-faceのフォントファイルは「ダウンロード開始済みかもしれないが、まだ使える保証はない」状態です。window.onloadを待てば画像などのリソースは揃いますが、フォントについてはブラウザが「実際にそのフォントを描画する必要があると判断するまで取得を遅らせる」最適化を行うことがあります。

<canvas>でctx.fillText()を呼んだ時点でフォントがまだ存在しなければ、CanvasAPIはフォールバックフォント(多くの場合ゴシック体や明朝体)で描くか、フォントデータが一切存在しない文字については豆腐を描きます。CSSアニメーションでテキストを動かす場合は、フォントが切り替わった瞬間にレイアウトが崩れる「FOUT(Flash of Unstyled Text)」として現れます。

対処の全体方針:「待つ」か「事前に読ませる」か

解決策は大きく二系統に分かれます。

「待つ」系は、フォントの準備が整ってからゲームを開始するアプローチです。確実性が高く、コードの見通しもよくなります。ゲームの初期化処理が一箇所にまとまっている場合に向いています。

「事前に読ませる」系は、ブラウザのフォント取得の遅延最適化を無効化し、ページロード時点でフォントを強制的にダウンロードさせるアプローチです。既存のゲームエンジンや外部ライブラリを使っていて初期化フローに割り込みにくいときに有効です。

どちらか一方だけで解決できる場合がほとんどですが、外部フォントを複数使う大規模なゲームでは両方を組み合わせることもあります。

「待つ」:FontFace APIとdocument.fonts.readyを使う

現代のブラウザにはdocument.fontsというFontFaceSet APIが用意されています。document.fonts.readyはすべての登録済みフォントの読み込みが完了したときに解決するPromiseを返します。ゲームのエントリーポイントをこのPromise以降に置くだけで、フォント未読み込みによる問題の大半は解消されます。

手順は次のとおりです。

  1. <head>内に@font-faceまたはGoogleフォントなどの<link>を記述する。
  2. ゲームの初期化関数(init()など)をasync関数として定義する。
  3. 関数の先頭にawait document.fonts.ready;を一行追加する。
  4. その後にcanvasの取得やゲームループの開始処理を続ける。

document.fonts.readyは、フォントが何もなくても即座に解決するため、フォントを使わないシーンでも安全に呼び出せます。ブラウザ対応は主要なモダンブラウザで問題ありません(公式のMDNドキュメントで最新の対応状況を確認してください)。

個別のフォントだけを待ちたい場合は、FontFaceコンストラクタでフォントをJavaScriptから直接登録し、load()メソッドで待つ方法も使えます。CSSの@font-faceとは独立して管理できるため、ゲーム専用フォントだけを確実に読ませたいときに便利です。

「事前に読ませる」:font-displayとpreloadの活用

@font-faceのCSSにfont-display: block;を指定すると、フォントが読み込まれるまでテキストを非表示にし、読み込み完了後に表示します。ゲームのUIをCSSで構築している場合はこれだけで視覚的な文字化けを防げます。ただし<canvas>内の描画には効果がありません。Canvasは常にJavaScriptが明示的に描画を呼び出すため、CSSの表示制御が届かないためです。

<link rel="preload" as="font">を<head>に追加すると、ブラウザはHTMLの解析開始直後にフォントファイルのダウンロードを始めます。「使われるまで取得を遅らせる」最適化をバイパスできるため、document.fonts.readyを待つ時間を大幅に短縮できます。

<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>

crossorigin属性は同一オリジンのフォントにも必要な場合があります。この属性がないとキャッシュが効かず、フォントが二重にダウンロードされることがあるため、必ず付けてください。

Canvasで特に起きやすい落とし穴

Canvas APIでの文字化けには、読み込み待ち以外にも原因があることがあります。混同しやすいのでここで整理します。

フォント名の書き方のミス:ctx.fontに指定する文字列はCSSのfontプロパティと同じ書式です。"24px MyFont"のように必ずサイズと名前を両方書く必要があります。名前にスペースが含まれる場合は"24px 'My Font'"のようにクォートで囲みます。ここが間違っていると読み込み完了後も別フォントが描かれます。

最初の1フレームだけ壊れるケース:document.fonts.readyを待っているのに、ゲームループの最初のフレームだけフォールバックフォントになることがあります。これはrequestAnimationFrameがPromiseの解決より先に1回呼ばれるタイミング問題です。対策はdocument.fonts.readyの解決後に初めてrequestAnimationFrameを呼び出すよう、初期化の順序を確認することです。

外部配信フォントのCORS制限:Google FontsなどCDNから配信されるフォントをCanvasで使う場合、Canvasが「汚染された(tainted)」状態になることがあります。これはセキュリティポリシーによるもので、フォント配信元のCORSヘッダー設定とcrossorigin属性の有無が影響します。toDataURL()やピクセル操作が必要なゲームでは自前サーバーでフォントを配信するか、配信元のCORSポリシーを確認するのが安全です。

Webフォント以外の文字コード範囲の穴:日本語のゲームでよくあるのが、英数字用のフォントを指定したままひらがな・漢字を描こうとして豆腐になるケースです。フォントファイルが対応しているUnicode範囲(unicode-range)を確認し、日本語を含む場合は日本語グリフを持つフォントを選んでください。

複数フォントを使うゲームでの管理方針

フォントが1本だけであればdocument.fonts.readyで十分ですが、複数のフォントを場面ごとに切り替えるゲームでは少し工夫が必要です。

よく使われるのは、ゲーム起動時に必要なフォントをFontFaceコンストラクタで全量登録し、Promise.all([font1.load(), font2.load()])で一括待機する方法です。こうすると読み込み完了を一箇所で管理でき、後から「このフォントだけ遅れた」という問題が起きにくくなります。読み込み中はローディング画面を出しておき、Promise解決後にゲーム本体の初期化を始めるパターンが、ゲーム開発では標準的な構成です。

フォントの種類が増えるほどロード時間も伸びます。使うフォントを絞る、woff2形式でファイルサイズを抑える、preloadで並列ダウンロードを促進するなど、読み込み速度と使い勝手のバランスを意識してください。

次の行動

まずdocument.fonts.readyをゲームの初期化関数の先頭でawaitしているかどうかを確認してください。Canvasで描画している場合はその前にrequestAnimationFrameが呼ばれていないかも合わせてチェックしてください。複数のフォントを使う場合はFontFaceで一括管理するよう構成を見直し、動作確認ができたらアソバースへ投稿してみてください。

参考資料

  • MDN Web Docs — FontFaceSet: ready プロパティ
  • MDN Web Docs — FontFace API
  • MDN Web Docs — CSS @font-face / font-display
  • MDN Web Docs — link rel="preload"
  • HTML Living Standard — The canvas element(CORSと汚染状態の仕様)