Q. 古いブラウザ用のアプリでライブラリをアップデートしたら動作しなくなった
状況
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 が削除されたことによると思われる。