画像が最初の1回だけ表示されない:読み込み完了前の描画
遊びをつくる執筆にClaudeを使用しています。

タイトル画面を開くと、背景の画像が出ていない場面を考えてみます。ページを再読み込みすると、今度は何事もなかったように表示されます。コンソールにエラーはなく、画像のファイル名も合っています。自分のパソコンでは毎回正しく見えるのに、公開してから「最初だけ背景がない」と言われる。この症状は、非同期の読み込みと描画の順番のずれから起きていることが多くあります。 この記事で確かめるのは、画像の読み込みを「待つ」方法と、「待たない」方法のどちらを選ぶかです。待つ方法は、読み込みが終わってから描画や開始を行います。待たない方法は、未完了を前提に毎フレーム描き直し、読み込みが終わった時点で自然に絵が出るようにします。症状の仕組みを見たうえで、2つの書き方、使い分けの基準、自分で再現して確かめる手順、アソバースに上げるときの確認点を順に整理します。
この記事の目次
なぜ「最初の1回だけ」起きるのか
img.src = "bg.png" と書いた時点では、画像はまだ手元にありません。ブラウザは裏で取得とデコード(圧縮された画像を描ける形に展開する処理)を進め、終わった時点で load イベントが発生します。JavaScript はその完了を待たずに次の行へ進みます。
canvas の drawImage を取得の途中で呼ぶと、描く画像がまだ使えない状態のため、何も描かれないことがあります。壊れた画像(読み込み失敗)を渡した場合は例外が出る仕様もありますが、「まだ読んでいる最中」は例外にならず、静かに何も起きません。エラーが出ないので、原因が見つけにくくなります。
「最初の1回だけ」になる理由は、キャッシュの存在です。一度取得した画像はブラウザが保持するので、2回目以降は読み込みが早く終わります。すると描画の呼び出しに間に合い、表示されるようになります。
この性質は、開発中の環境にも関わります。手元のローカル環境は通信が速く、キャッシュも効いています。そのため、症状は初めて遊ぶ人の遅い回線でだけ出る形になりやすいです。「自分の環境では再現しない」ことが、見逃しの主な原因です。
待つ方法:読み込みを Promise で包む
最も素直なのは、画像ごとに「読み込み完了で解決される Promise」を作り、全部そろってからゲームを始める方法です。
<canvas id="c" width="160" height="100"></canvas>
<script>
const ctx = document.getElementById('c').getContext('2d');
const SRC = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="160" height="100"><rect width="160" height="100" fill="#2a6f97"/></svg>');
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('load failed'));
img.src = src;
});
}
async function start() {
ctx.fillText('読み込み中…', 10, 50);
try {
const img = await loadImage(SRC);
ctx.clearRect(0, 0, 160, 100);
ctx.drawImage(img, 0, 0);
} catch (e) {
ctx.fillText('読めませんでした', 10, 50);
}
}
start();
</script>要点は3つあります。
- onload と onerror を src の代入より前に書くことです。順序が逆だと、すでに完了していた場合にイベントを取り逃がす可能性があります。
- 失敗の経路を用意することです。onerror がないと、ファイル名の間違いで永遠に「読み込み中」のまま止まります。
- 画像が複数あるときは、Promise.all([loadImage(a), loadImage(b)]) で全部の完了を待てます。
HTMLImageElement には decode() というメソッドもあり、デコードまで終えてから描画できます。大きな画像で、描画の瞬間にひっかかるのを避けたい場合の選択肢です。使えるかどうかは、遊んでもらう環境に合わせて確認してください。
この方法の弱点は、待っている間の画面です。何も出さずに止まると、読み込みの失敗と区別がつきません。「読み込み中」の表示を先に出し、失敗時は文面を変えて、止まっている理由が見えるようにします。
待たない方法:未完了を前提に毎フレーム描く
もう一つは、読み込みを待たずに始める方法です。ゲームが毎フレーム画面を描き直す作りなら、画像の準備ができているかを毎回確かめ、できていなければ代わりの絵を描きます。
<canvas id="c" width="160" height="100"></canvas>
<script>
const ctx = document.getElementById('c').getContext('2d');
const SRC = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="160" height="100"><rect width="160" height="100" fill="#2a6f97"/></svg>');
let sprite = null;
const img = new Image();
img.onload = () => { sprite = img; };
img.src = SRC;
function frame() {
ctx.clearRect(0, 0, 160, 100);
if (sprite) {
ctx.drawImage(sprite, 0, 0);
} else {
ctx.fillStyle = '#cccccc';
ctx.fillRect(0, 0, 160, 100);
}
requestAnimationFrame(frame);
}
frame();
</script>読み込みが終わると sprite に画像が入り、次のフレームから本物が描かれます。描画の呼び出しが1回きりでも成り立つ「待つ方法」と違い、この方法は「描き直し続ける」ことが前提です。
ここでの落とし穴は、描画が1回きりの画面にこの書き方を持ち込むことです。たとえばクリア画面を一度だけ描き、その後は再描画しない作りだと、代わりの四角が描かれたまま画面が止まります。未完了の判断を入れるなら、完了時に再描画を呼ぶか、毎フレーム描く作りにそろえる必要があります。
使い分けの判断基準
2つの方法は、画面の作りと画像の重要度で選びます。
- 向く画面、待つ:タイトル、静的な画面、一度だけ描く画面、待たない:毎フレーム描き直すゲーム本編
- 向く画像、待つ:最初の画面に必須の画像、待たない:後半のステージなど、後から必要になる画像
- 失敗時の挙動、待つ:失敗を検知して開始前に止められる、待たない:代わりの絵のまま進行する
- 注意点、待つ:読み込み中と失敗の表示が必要、待たない:代わりの絵で遊びが成立するか確認が必要
迷ったときの目安を3つ挙げます。
- 画像がないとゲームが成立しない(キャラクターの見た目が操作の手がかりになるなど)なら、待ちます。
- 画像が装飾なら、待たずに始めて、後から差し替わっても遊びが崩れない作りにします。
- 両方を組み合わせることもできます。最初の画面に必要な画像だけを待ち、後半の画像は裏で読み込みを始めておきます。
自分で再現して確かめる手順
この症状は、確かめずに直したつもりになりやすいものです。手元で意図的に「遅い初回」を作って検証します。
準備するものは、ブラウザの開発者ツールと、実際の画像ファイルを使ったゲームのコピーです。上のコードはファイル取得を伴わないので、SRC を画像ファイルへの相対パスに置き換えて試します。
- 開発者ツールの「ネットワーク」を開き、キャッシュを無効にする設定(「Disable cache」など)を入れます。
- 回線速度の制限(スロットリング)を、遅い設定にします。
- ページを再読み込みして、最初の画面を観察します。
- 同じ操作を5回繰り返し、毎回「画像が表示されたか」「表示までの様子」を記録します。
- 制限を外し、キャッシュを有効に戻して、同じ5回を行います。
記録は、条件(遅い/通常)と回数ごとに、表示された・されなかった、の2値で十分です。
結果の読み方は次のとおりです。
- 遅い条件で表示されない回があり、通常条件では出るなら、読み込みの完了前に描画している可能性が高いです。描画を呼ぶ箇所が onload の内側にあるかを確認します。
- 遅い条件でも毎回出るなら、その画像に関する原因は別にあります。他の画像、パスの誤り、描画順(背景を後から塗りつぶしていないか)も疑います。
- 修正後は、同じ手順をやり直し、遅い条件で全回表示されることを確かめます。
起こりやすい誤りは、1回の確認で判断することです。キャッシュの状態や回線の揺れで結果が変わるため、条件を固定して複数回確認します。
アソバースに上げるときの確認点
アソバースへの投稿には、画像の扱いに関係する決まりがあります。
- 入口は、ZIPの場合は直下の index.html です。画像などの素材は相対パスで参照します。
- アップロードは20MiB以内で、ZIP展開後は合計100MiB・1,000ファイル以内、1ファイル30MiB以内です。重い画像を多く含めるほど、初回の読み込みは長くなります。
- ゲームはsandbox付きiframeで、メインサイトとは別のオリジンから配信されます。ローカルでは動いたパスが、公開後は参照できなくなっていないかを、公開前の表示確認で見ます。
- サービスワーカーは対象外です。キャッシュ制御をサービスワーカーに頼る設計は使えません。
外部通信なしで完結させたい場合は、小さな画像をSVGやdata URIとしてHTMLの中に埋め込む選択肢もあります。ファイル取得を挟まないので、取得待ちを減らせます。ただしHTMLが大きくなる点と、デコードの待ち自体は残る点は、あらかじめ理解しておきます。
公式作品の13本は、独立したHTMLで外部通信なしに動くと案内されています。各作品が画像をどう持ち、どの順で描くかは確認済みの情報にないため、「設計から読み取れる」とは言えません。気になる作品は、開発者ツールで読み込みの様子を見て確かめてください。
次の行動
まず、自分のゲームで最初の画面に使う画像を洗い出し、遅い回線とキャッシュ無効の条件で5回ずつ再読み込みして、表示の有無を記録してください。表示されない回があれば、「その画像が必須か」で待つ方法と待たない方法を選び、修正後に同じ手順を繰り返します。確認が済んだら、アソバースの投稿画面にある表示確認で、公開後の相対パスと読み込みの様子を見てから公開してください。
参考資料
- HTML Living Standard(WHATWG):画像要素と canvas の仕様
- MDN Web Docs:HTMLImageElement、CanvasRenderingContext2D.drawImage()、HTMLImageElement.decode()

