定義
旧ブラウザでアプリを動かしたいときにやるべきことは 二つ あり、それぞれ別物である。一つは新しい 構文(例: a?.b、a ?? b)を旧ブラウザでも読める古い構文へ機械が変換する トランスパイル(transpile)。もう一つは旧ブラウザに そもそも存在しない関数/API(例: structuredClone()、Array.prototype.at)を JavaScript で再実装して埋める ポリフィル(polyfill)。
肝心なのはこれである: トランスパイラは構文だけを変える。存在しないランタイム関数は作ってくれない。 そのため構文をすべて下げても、存在しない関数を呼んだ瞬間に TypeError: ... is not a function が起きる。
なぜ必要か
旧ブラウザでアプリが「理由もなく丸ごと死ぬ」バグのよくある根っこが、この二つの混同である。チームが @babel/preset-env で構文だけを下げて「これで旧ブラウザ対応は終わった」と思い込むが、実際にはコードが structuredClone() を呼んだ瞬間に死ぬ、という具合だ。構文は完璧に通っていても、その関数自体が旧ブラウザには存在しないからである。
二つの問題を区別しておくと診断が速くなる。
- 構文(syntax)の問題: 古いパーサが新しい構文を読めず、ファイル全体が
SyntaxErrorで死ぬ。最初のエラー以降、そのスクリプトは一行も実行されない。 - ランタイム API の問題: 構文は通るが、存在しない関数を呼び出した時点で
TypeErrorが起きる。
動作原理
- 対応下限(例: 特定の古いバージョン)を 一か所 に宣言する — 通常は
browserslist。トランスパイルのターゲットとポリフィルのターゲットがこの値を共有していないとずれる。 - 構文レイヤー:
@babel/preset-envにターゲットを与えると、そのブラウザが読めない構文だけを選んでダウンレベルする。サードパーティ(node_modules)まで変換パスに載せないとライブラリの?.も下がらない。 - API レイヤー:
core-js-compatがターゲット一覧を入力に「そのブラウザに足りない標準 API の一覧」を計算する。その一覧でポリフィルバンドルを作る(提案段階のesnext.*API は通常除外)。 - 回帰防止: レポが実際に使う API がポリフィルのエントリに含まれているかをテストで強制する。手で管理すると新しく使った API が漏れる。
// 構文: babel が古い構文へ下げる(以下は例)
const name = user?.profile?.name ?? "guest";
// ランタイム API: babel はこれを作ってくれない → ポリフィルが必要
const copy = structuredClone(data);
const last = list.at(-1);
const upper = text.replaceAll("a", "b");
実務での適用
- コンソールの最初のエラーが
SyntaxErrorなら、トランスパイル設定(ターゲット・node_modules の包含)を見る。 TypeError: X is not a functionなら、その API がポリフィルバンドルにあるかを見る。- ポリフィルのソースは人ではなく
core-js-compatにターゲットから導出させる。 - 対応下限を変えるときは
browserslistとポリフィルのビルドターゲットを 一緒に 修正する。
トレードオフ
| 選択 | 利点 | コスト |
|---|---|---|
| トランスパイルのみ | 設定が単純 | 存在しない API 未解決 → ランタイム死 |
| トランスパイル + ポリフィル | 構文・API 両方カバー | バンドルサイズ・ビルド複雑度の増加 |
| ポリフィルを手作業で管理 | ツール不要 | 一覧のドリフト、新 API の漏れ |
使うべきでない場合
- すでに最新のみ対応と決めたプロジェクトに重いポリフィルを常時含める場合 — 不要なコスト。
- node_modules を無条件に全部トランスパイル対象にしてビルドが遅くなる場合 — 問題のあるパッケージだけ載せる。
よくある失敗
- 「babel を入れたから旧ブラウザ対応は終わり」と思ってポリフィルを漏らす。
- アプリコードだけをダウンレベルし、サードパーティライブラリの新しい構文を残す。
- トランスパイルのターゲットとポリフィルのターゲットをそれぞれ別の値にして、カバー範囲がずれる。
関連概念
- differential-polyfill-loading — ポリフィルを旧ブラウザにだけ条件付きでロードする
- hydration-failure-dead-handlers — バンドルが死ぬとハイドレーションが不発になりハンドラが丸ごと死ぬ
- browser-compat-interaction-smoke — 構文・API の回帰を実際に捕まえるスモークテスト