Q. 古いブラウザ用のアプリでライブラリをアップデートしたら動作しなくなった
Polyfillがアップデートしたライブラリに適用されておらず、古いブラウザで利用できないメソッドが呼び出されランタイムエラーが発生していた。Polyfillを適用する設定を見直すことで対応した。
- # 状況
- # 調査:エラーログを読む
- # 調査:ビルドされたコードを読む
- # 調査:ビルド設定を読む
- # 解説:ライブラリにPolyfillが適用されないケース
- # 対処
状況
Chromeブラウザの非常に古いバージョンで動作することが想定されるアプリで、リリースを行ったところ動作しなくなったという報告を見かけた。
調査:エラーログを読む
Datadogのエラーログを確認したところ、無数のエラーログに紛れて次のログが流れているのを見つけた。
o.replaceAll is not a function
IPアドレスでフィルタリングを行いユーザーを識別すると、いずれもこのログを起点として他のエラーが出ていることが確認できた。
4,5年前にはreplaceAll
は、ほとんどのブラウザで利用出来るようになっており不思議に思ったが、試しにログをブラウザ毎にグルーピングすると、アプリの全てが7,8年前のブラウザで利用されていることが分かった。
調査:ビルドされたコードを読む
アプリのコードを調べるとreplaceAll
は利用されておらず、直近のリリース内容もほとんどアプリで利用されているライブラリを更新するだけのものであった。
そこで最終的に生成されたコードを読むと、直近で更新されたライブラリのコードにreplaceAll
が含まれており、またPolyfillが適用されていないことが分かった。
調査:ビルド設定を読む
このアプリでは、@babel/preset-env
でuseBuiltIns
をusage
に設定することで、Polyfillを自動的に適用するような設定がされていた。
しかし、Polyfillを適用するターゲットとなるブラウザを指定するbrowserslistの設定に問題があった。 次のように当時テンプレ的に利用されていた記述がそのまま設定されていた。
{
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
この設定には、2つ明らかな問題がある。
- 時代と共にターゲットブラウザが変化するため、今回のような利用側のブラウザが更新されないアプリでは、何も変更していないのに再ビルドするだけで動作しなくなる可能性がある。
- 実際はChromeブラウザの特定のバージョンレンジでしか利用されていないにも関わらず、IEなどの不要なPolyfillも適用されている。
解説:ライブラリにPolyfillが適用されないケース
@babel/preset-env
を利用してPolyfillを適用する場合、useBultins: 'usage'
が最も適した選択肢であることが多い。
コード解析が必要なためビルド時間が若干長くなる傾向にあるものの、ターゲットブラウザの指定に基づいてPolyfillを適用されるためバンドルサイズが最適化される傾向にあり、エントリーポイントでの明示的な import 'core-js/stable';
などのインポートが不要でありヒューマンエラーも発生しにくい。
特徴 | useBuiltIns: 'usage' | useBuiltIns: 'entry' |
---|---|---|
動作原理 | コード内で実際に使用されている機能に基づいてポリフィルを自動追加。 | エントリーポイントで明示的にインポートされたポリフィルから、ターゲット環境で不要なものを削除。 |
開発者の手間 | 低(手動でのポリフィルインポート不要) | 高(エントリーポイントでの明示的なインポートが必要) |
バンドルサイズ | 最適化されやすい(無駄なポリフィルが入りにくい) | 比較的最適化される(不要なものを削除するため) |
ビルド時間 | 若干長くなる傾向(コード解析のため) | 比較的短い傾向(コード解析が不要なため) |
また、トランスパイル対象に含まれていればライブラリに対してもPolyfillが適用されるため、ライブラリによってPolyfillが適用されないコードが追加されるといった問題も回避しやすい。
しかし、ライブラリが既にES5にトランスパイルされている場合などはPolyfillが適用されず、今回のケースはこれに該当していた。
対処
動作環境がかなり限定されることに加えて、ライブラリのコードに対してもPolyfillを適用したいため、browserslistの指定を実体に合わせた上でuseBuiltIns: 'entry'
を利用するような変更を提案した。
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": 3,
"targets": "chrome 64"
}
]
]
}
ただし、useBuiltIns: 'entry'
では、エントリーポイントでimport "core-js"
など明示的にポリフィルをインポートする必要があるため注意。
import "core-js"
このアプリはWebpackでビルドされているため--verbose
オプションを付けてビルドを行い、適用されるPolyfillを確認できる。
The corejs3 polyfill entry has been replaced with the following polyfills:
es.symbol.description { "chrome":"60" }
es.symbol.async-iterator { "chrome":"60" }
es.array.flat { "chrome":"60" }
es.array.flat-map { "chrome":"60" }
...
esnext.string.replace-all { "chrome":"60" }
...
この変更により、微々たる数値だがビルド時間は2s、バンドルサイズは2kb改善された。これはuseBuiltIns
の変更によりコード解析が行わなくなったことと、過剰なターゲットブラウザの削除により不要なPolyfillが削除されたことによると思われる。