VANNAN GAMES.

← 制作ノート一覧

16MBの画像が、1MBの音楽を5秒待たせていた

フルスタック!

初回だけBGMが鳴らない。自分のスマホで何度もそうなりました。前に音の罠を4つ書いたばかりで、もう地図は描けたつもりでいたのです。自動再生ブロック、マナーモード、要素ごとの解錠、宣言する順番。全部潰した。それでも鳴らない。

五つ目の罠は、音の処理の中には無かったという話です。

疑っていなかった場所

行き詰まって画面を眺めていて、ふと思い当たりました。カード画像を読み終わってから音が鳴っているのでは、と。

盲点でした。ずっと再生の側だけを見ていたのです。play()が拒否されていないか、AudioContextが動いているか、解錠は済んでいるか。全部OKでした。だから次は端末の設定を疑っていた。ロードそのものが遅れている可能性を、一度も考えていなかったのです。

測ってみました。本番のフルスタック!を開いて、リソースの開始と完了を並べただけです。

開始完了
画像53枚 16.2MB1,364ms6,658ms
タイトルBGM 1.0MB1,467ms7,119ms

1MBのファイルの完了に、5.6秒かかっています。単体なら1秒もかからない量です。しかもこれは光回線のPCでの数字。

犯人は隣の行にいました。カードイラスト43枚と敵10体、合わせて16.2MB。こいつらが先に走り出して、帯域を全部持っていっていたのです。

全部読ませたい、でも音は先に鳴らしたい

厄介なのは、画像を後回しにしたいわけではないことでした。このゲームはカードを引いた瞬間に絵が出ないと格好がつきません。読み込みが遅れて記号のまま表示されるくらいなら、多少待ってでも最初に全部落としておきたい。実際そういう設計にしてあります。

つまり「画像は全部読む。ただし音より後で」という順番の指定がほしい。

それ、ちゃんとありました。fetchPriority です。画像の Image オブジェクトに low を、音の fetchhigh を渡すだけ。読み込む量は変わりません。並ぶ順番が変わるだけです。

const i = new Image();
i.fetchPriority = "low";      // 音より後でいい
i.src = "img/card_gugu.png";

fetch(bgmUrl, { priority: "high" });   // こっちが先

もうひとつ、開始のタイミングも直しました。画像の先読みはスクリプトのトップレベルで走ります。BGMの取得はその後ろに書いてあった。数行前に動かしただけで、読み込み順の先頭がBGMになりました。171.3ms対172.8ms。1.5msの勝ちですが、勝ちは勝ちです。

白状すると

この問題、最初に気づいてから4日かかっています。その間にやったのは、オーディオセッションの宣言順を直し、AudioContextの生成を遅らせ、診断パネルを作り、先読みを入れる。どれも必要な改善でしたが、症状は消えませんでした

なぜかというと、私が「音が鳴らない」を「音の処理の問題」だと決めつけていたからです。言葉に引きずられていました。実際には「音のファイルが届いていない」で、それはネットワークの問題です。同じ症状でも層が違う。

診断パネルを作ったとき、私はオーディオの内部状態ばかり並べていました。AudioContextの状態、デコードの進捗、最後の再生経路。そこに buffered を足したのは、帯域を疑ったあとです。最初から入れていれば「0秒のまま増えない」が見えて、もっと早く気づけました。

ついでに見つかったもの

同じ日、DevToolsでソースを開いていて、日本語が全部化けているのに気づきました。

本番のJSが Content-Type: application/javascript で返っていて、charsetの指定がありませんでした。ファイルはUTF-8なのに、宣言していないからブラウザが別の文字コードで読む。ゲーム画面はHTMLの meta charset を引き継ぐので正常、ソースを直接開いたときだけ化ける、という状態です。

うちは難読化していません。コンソールに隠しメッセージを置いて、デバッグ用のフックも残してあります。読まれる前提で作っているのに、肝心のソースが読めない。配信設定に3行足して直しました。

どちらも、ふだん見ていない場所でした。作った本人は、画面が動いていれば満足してしまう。

教訓というほどでもないこと

「Xが動かない」と言われたとき、Xの実装を読みに行くのは自然な反応です。でも動かない理由がXの外にあることは、わりとあります。今回は、音が鳴らない理由が画像にありました。

次に同じ場所で詰まったら、まず「そのファイル、届いてますか」を見ます。performance.getEntriesByType("resource") をコンソールに打つだけで、開始と完了が全部出てきます。5分の作業でした。4日かけたのに。