はじめに
いよいよ2025年も終わりですね! CEOのuenoyamaです。
この記事は、開発中のQuasar Frameworkアプリケーション(Vue3ベース)で起きた、Polyfillが効いてないから起動しないよ問題を解決した話です。
会社の若い人向けに、どういう手順で解決に到達したか、参考になればと思って書きました。
何が起きたのか
当社で開発中のQuasar Frameworkアプリケーション(SPA)では、古いChromeでも動作する必要があるため、@vitejs/plugin-legacyにて、polyfills: trueの設定を入れて対応しています。
これで古いブラウザでも問題なく動作していたのですが、ある日、古いブラウザでの動作確認をしたところ、Array.at解決できず、起動に失敗していることが発覚しました。
(開発メンバーは、ふだんは最新ブラウザで確認していたので気付くのが遅れた)
その後、開発メンバーの1人が「なんでArray.atが解決できない?」を生成AIと相談して、modernPolyfills: trueを入れてpolyfills: ['es.array.at', 'es.typed-array.at']に直したら問題なく動作しました! というPull Requestを発行しました。
その時、私の中のコナンが疼きました。
「あれれー? それって設定が不足していたということだよねー? だとしたら、以前は動作していたことと合わないんじゃない?」
ステップ1 問題発生コミットを探す
とりえあず1ヶ月ぐらい前には動作していたような記憶があったので、まずはgitの1ヶ月ぐらい前のcommitをcheckoutしてresearchブランチとし、起動してみます。
そしたら何の問題もなく動作したので、あとは当該commitから最新commitまで(全部で50 commitぐらい)を、適当に3-5個ずつぐらいcherry-pickして起動を試します。
これを繰り返すと、見事に現象が発生する特定のコミットがわかりました。
ステップ2 問題発生箇所を探す
この該当コミット、修正量が少なければ良いのですが……変更したファイルは35、変更ステップは+582 -327。
グヘえ。チマチマとTry & Errorをしては時間がかかりすぎます。
なので、己のバグ嗅覚をたよりに、ざーっとコードを流して見ていきます。
ん? におう? なんだかクサイ変更があるぞ?
見つけたのは、とある設定の初期値を、グローバルオブジェクトとして持つようにしたコードでした。
const initialValue = {
....
}
const useXXXX = () => {
const value = ref(structuredClone(initialValue));
}
という感じのコードです。
におう気がしたので、さらに各フィールドの修正を確認したところ、Array.atを使っている箇所を発見し、これを添え字アクセスに変更したところ、現象が起きなくなりました。
ステップ3 仮説を立てる
事件は……終わったのか……?
いや、これではmodernPolyfills: true、polyfills: ['es.array.at', 'es.typed-array.at']を入れたら現象が起きなくなった、という疑問が解消していない。
今回、Array.atを使っている部分を置換することで現象は起きなくなったが、また違う形で問題が再燃するかもしれない。
とりあえず、modernPolyfills: true、polyfills: ['es.array.at', 'es.typed-array.at']を入れたらどうなるのかを、quasar buildが出力する結果を並べて比較してみました。
polyfill関連なので、polyfillという言葉を含む出力と、問題が発生していたindex-xxxx.jsを見てみます。
(入れる前の抜粋)
| Asset | Size | Gzipped |
|---|---|---|
| assets/index-xxxx.js | 396.29 KB | 133.09 KB |
| assets/polyfill-legacy-xxxx.js | 209.41 KB | 75.63 KB |
| assets/polyfill-xxxx.js | 222.49 KB | 81.74KB |
| assets/polyfills-legacy-xxxx.js | 96.34KB | 35.47KB |
(入れた後の抜粋)
| Asset | Size | Gzipped |
|---|---|---|
| assets/index-xxxx.js | 396.29 KB | 133.09 KB |
| assets/polyfill-legacy-xxxx.js | 209.41 KB | 75.63 KB |
| assets/polyfill-xxxx.js | 222.49 KB | 81.74KB |
| assets/polyfills-xxxx.js | 138.41 KB | 51.96KB |
| assets/polyfills-legacy-xxxx.js | 96.34KB | 35.47KB |
なんか増えとるな……
polyfill-xxxx.jsよりもサイズが小さいな……
きっとこれがmodernPolyfills: trueで生成されたファイルなんだろう……
と思った瞬間、仮説が思い付きました。
index-xxxx.jsの前にpolyfill-xxxx.jsは読み込まれてないけどpolyfills-xxxx.jsなら読み込まれるのでは!
ステップ4 仮説検証
Chrome Developer Toolのネットワークタブで、ThrottlingをSlow 3Gにして、リロードした時の読み込み順を確認します。
(入れる前の処理順)

(入れた後の処理順)

ビンゴ!
index-xxxx.jsのDOMContentLoadedの前に、polyfills-xxxx.jsのDOMContentLoadedが終わっている!
というか、index-xxxx.jsと同時にpolyfills-xxxx.jsの取得が始まっている!
追加でindex.htmlの比較をしてみたところ、入れた後のファイルには、<head>に以下が増えてました。
<script type="module" crossorigin src="/assets/polyfills-xxxx.js"></script>
なるほどね……
ステップ5 結論付ける
Vueアプリケーションでは、最初にアプリケーションのメインコードであるindex-xxxx.jsのロードを開始します。
が、modernPolyfills: trueが指定されていると、index-xxxx.jsと同じタイミングでロードを開始するようです。
つまり、modernPolyfills: trueを指定したので、index-xxxx.jsと同じタイミングでロードが始まり、ファイルサイズが小さいこともあって先にDOMContentLoadedが終わる。
index-xxxx.jsのDOMContentLoaded後にスクリプト処理が始まった時には、すでにArray.atは使える状態になっているので問題にならなかった、ということなのでしょう。
ということは、modernPolyfills: trueの対策でも問題ないように思いますが、先にDOMContentLoadedが終わっているのは、ファイルが小さいことなどが要因であり、プログラムとして保証された動作ではありません。
なので、本当の対策は以下だと結論付けて、本事件は解決となりました。
- 短期的には、グローバルオブジェクトの初期化は、Polyfillが必要なメソッドを使用しないようにする。
- 長期的には、そもそもグローバルオブジェクトを使用しない設計にする。
「真実はいつもひとつ!」
さいごに
あとから気付いたのですが、トランスパイルしたファイルが難読化されていてもJSONのフィールド名は変化しないので、そっから調査を開始したらもっと早く問題となっているArray.atの場所が特定できて、事件解決は早かったかなーと思いました。修行修行。
We're Hiring
こんな感じで、私は今でもバグハンターとしてデバッグの最前線に立ってたりします! そんな泥臭いCEOと一緒に働いてみたいなー、と興味を持ってくれた方は、HPのCONTACTからお気軽にご連絡ください! ドミノソフトでは一緒に働く仲間を募集中です!