マガジン

画像の読み込みは順番通りではない:非同期の見え方

遊びを見つける

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

大きな荷物で詰まった細い道を測り、軽い部品へ置き換えて流れを整える模型。
記事のテーマをもとに制作したAI生成の写真風イメージ。

画像を8枚、1枚目から8枚目の順にページへ渡す場面を考えてみます。コードの上では、1番から8番まで整然と並んでいます。ところが「届いた」という合図を順に記録すると、3番が先に来て、1番が後から来る並びになっているかもしれません。タイトル画面に使う画像が、背景より遅れて現れるといった崩れ方もありえます。 この記事は、その「届く順番」を読者自身が記録で確かめるための解説です。結果の数字はあえて載せません。環境が違えば結果も違うので、手元で取った記録だけが根拠になるためです。代わりに、一枚のHTMLで到着順をログに残す手順と、記録の読み方、ゲームで起きやすい失敗を順に示します。

この記事の目次
  1. なぜ「順番通り」にならないのか
  2. 到着順を記録する最小ページ
  3. 記録の取り方と読み方
  4. 通信経由の画像で確かめる
  5. ゲームに置き換えると起きる失敗
  6. アソバースの作品として見るときの視点
  7. 次の行動
  8. 参考資料

なぜ「順番通り」にならないのか

ブラウザは、画像の取得を頼まれても、その場で終わるまで待ちません。取得は裏で進み、終わったときに「読み込み完了」の合図(loadイベント)が届きます。この「頼む」と「終わる」が別の瞬間になる仕組みが非同期です。頼んだ順に終わる保証は、仕組みの上にはありません。

到着順を入れ替える要因は、一般に次のようなものです。

  • ファイルサイズが違うと、小さい画像のほうが早く届きやすくなります。
  • 通信経路の混み具合や、同時に受け取れる本数の上限で、待たされる画像が出ます。
  • すでに端末に保存されている画像(キャッシュ)は、ほぼ待たずに終わります。
  • ファイルを受け取った後に、絵として使える形へ展開する処理(デコード)の重さも画像ごとに違います。

要因が重なるので、同じページを開き直しても、毎回同じ並びになるとは限りません。「たまたま毎回順番通りに見えた」状態が、いちばん危険です。順番を前提にしたコードが、手元では動いてしまうからです。

到着順を記録する最小ページ

外部の画像ファイルを用意しなくても、大きさの違う画像をその場で作って試せます。次のHTMLをorder.htmlのような名前で保存し、ブラウザで開いてください。サーバーも外部ライブラリも要りません。

html
<!doctype html>
<meta charset="utf-8">
<pre id="log"></pre>
<script>
const sizes = [1600, 60, 1200, 40, 900, 200, 1400, 30];
const out = document.getElementById('log');
const say = t => out.textContent += t + '\n';

function makeUrl(n) {
  return new Promise(resolve => {
    const c = document.createElement('canvas');
    c.width = c.height = n;
    const g = c.getContext('2d');
    for (let i = 0; i < 300; i++) {
      g.fillStyle = 'hsl(' + Math.random() * 360 + ' 70% 50%)';
      g.fillRect(Math.random() * n, Math.random() * n, n / 5, n / 5);
    }
    c.toBlob(b => resolve(URL.createObjectURL(b)));
  });
}

(async () => {
  const urls = [];
  for (const n of sizes) urls.push(await makeUrl(n));
  const t0 = performance.now();
  let order = 0;
  urls.forEach((u, i) => {
    const img = new Image();
    img.onload = () => {
      order++;
      say('到着' + order + '番目 / 要求' + (i + 1) + '番目 / '
        + sizes[i] + 'px / ' + (performance.now() - t0).toFixed(1) + 'ms');
    };
    img.onerror = () => say('失敗 要求' + (i + 1) + '番目');
    img.src = u;
  });
  say('要求を8件出し終えました');
})();
</script>

このページは、色つきの四角を散らした正方形の画像を、一辺が1600、60、1200……と大きさを変えて8枚作ります。8枚のsrcを1番から順に設定し、loadが起きるたびに「到着何番目か」「要求何番目か」「経過ミリ秒」を1行ずつ書き足します。

一点だけ、記録を取る前に確認できることがあります。「要求を8件出し終えました」の行は、どの画像の到着行よりも必ず上に出ます。loadの合図は、今動いているコードがすべて終わった後で届くためです。この行の位置が、「頼む」と「終わる」が別の瞬間であることの目に見える証拠になります。

記録の取り方と読み方

一度見ただけで判断せず、次の単位で記録を取ります。

  1. ページを再読み込みするたびに、ログを1回分として扱います。
  2. 1回分につき、「到着の番号」と「要求の番号」を並べて書き留めます。
  3. これを10回ほど繰り返し、並びが変わるかを見ます。
  4. 余裕があれば、ブラウザを変える、他のタブを閉じて開き直す、といった条件違いでも取ります。

読み方の目安は次のとおりです。

  • 10回とも到着順と要求順が同じ、読み取れること:この環境ではそろっただけです。順番を前提にしてよい根拠にはなりません
  • 小さい画像が大きい画像より先に出る回が多い、読み取れること:サイズが到着順に効いている可能性があります
  • 回ごとに並びが違う、読み取れること:順番に頼るコードは、手元でたまたま動いているだけかもしれません
  • 「失敗」の行が出た、読み取れること:失敗時の処理を、ゲーム側で用意する必要があります

起こりやすい誤りも挙げておきます。1回だけ見て「いつも順番通り」と結論づけること、経過時間の小数点以下の差を意味のある差と読むこと、そして自分のパソコンだけで確かめて終わることです。

このページは端末内で画像を作るため、ネットワークを通る画像より、並びがそろいやすい可能性があります。つまり、並びが入れ替わらなかったとしても、通信経由の画像で同じとは限りません。そこで、通信経由の場合も確かめます。

通信経由の画像で確かめる

自作ゲームに画像が何枚か入っているなら、そのまま調査対象にできます。ブラウザの開発者ツールを開き、ネットワークの記録を表示した状態でページを再読み込みしてください。各画像の要求が、開始時刻と所要時間つきで並びます。ここで見るのは次の3点です。

  • 要求を出した順と、完了した順が一致しているか
  • 同じ時刻に並行して進んでいる要求が何本あるか
  • 通信速度を落とす機能(開発者ツールに備わっています)を使うと、並びがどう変わるか

通信を遅くする設定は、端末内の画像では再現できない状況を作れます。回線の細い環境でプレイされる場面を、手元で想像できるようになります。速度を落とす場合も、1回で判断せず、同じ手順で数回記録します。

ゲームに置き換えると起きる失敗

到着順が揃わないことは、ゲームでは次のような形で表に出ます。

  • 「最初に届いた画像をキャラクターA、次に届いた画像をキャラクターBとして扱う」と書いてしまい、絵が入れ替わる。
  • 全部届く前に最初の画面を描き、画像の場所が空白のまま始まってしまう。
  • 1枚の失敗に気づかず、ずっと空白のまま進んでしまう。

対策の基本は、順番ではなく名前(または番号)で画像を結びつけ、必要な画像がそろってから始めることです。上のコードのiのように、要求した時点の番号を各画像に持たせておけば、到着順が変わっても、どの画像かを取り違えません。そろうのを待つには、一般にPromise.all(複数の完了を待つ機能)が使えます。画像1枚ごとに「届いたら完了、失敗したら失敗」と知らせるPromiseを作り、それをまとめて待つ形です。失敗時にも代わりの表示を出す処理を書いておくと、空白のまま止まる事態を避けられます。

アソバースの作品として見るときの視点

アソバースの投稿では、HTMLまたはZIPをアップロードできます。入口は直下のindex.htmlで、画像などの素材は相対パスで指定します。アップロードは20MiB以内、ZIP展開後は合計100MiB・1,000ファイル以内、1ファイル30MiB以内という上限があります。素材が多く重い作品ほど、読み込み完了までに差が開く場面が増えうるため、順番に頼らない作りが効いてきます。投稿は「入力→表示の確認→公開」の3段階なので、公開前の表示の確認で、画像が出そろう前の見え方も点検できます。確認画面でどこまで見えるかは、実際に投稿画面を開いて確かめてください。

公式作品13本についての確認済みの事実は、どれも独立したHTMLで、外部通信なしに動くことです。この設計から読み取れるのは、素材の取得が外部サーバーの応答に左右されない作りを目指している、ということです。ただし、各作品が画像ファイルを使っているか、使うならどの形式かは、確認済み事項にありません。この部分は、作品を開き、開発者ツールのネットワークの記録で外部への要求が並ばないかを見て、確かめてください。なお、カバー画像は生成したアートワークで、ゲーム画面のスクリーンショットではありません。カバーの見え方とゲーム内の画像は、別のものとして区別しておきます。

次の行動

まず上のHTMLを保存し、10回ぶんの到着順を紙かメモに書き出してください。続いて、自分のゲームか好きな作品で、ネットワークの記録を開いて同じ観察をしてみてください。並びが入れ替わるのを一度でも見たら、「順番に頼らず、そろってから始める」書き方に直す準備が整います。

参考資料

  • MDN Web Docs「HTMLImageElement」「Promise」「Performance.now()」「開発者ツールのネットワークモニター」