レガシーブラウザ向けのビルドオプションを剪定する

この記事はドワンゴ Advent Calendar 2023 の 20 日目の記事です。

はじめに

こんにちは。今年の 10 月に教育事業本部にフロントエンドエンジニアとして入社したユーンです。

この記事では、 2016 年から新陳代謝を続けている N予備校 Web フロントエンドのコードベースにおいて、 2019 年ぶりにビルドターゲットの設定を見直した話をします。

N予備校 Web フロントが約 8 年近くも新陳代謝を続けてきたあゆみについては、チームの古株である berlysia さんの以下の記事をご覧ください。

blog.nnn.dev

現状確認と課題設定

話はチーム内で以前から core-js のサイズが大きいと認知されていたことから始まります。(前提知識: @babel/preset-env は core-js を使って transform と同時に polyfill の挿入を実現しています。)

まず見直すにあたり、課題箇所の特定から行います。上記の通り課題感はありましたが、エビデンスを取ることは大切なので webpack bundle analyzer を使って確認します。

見直し前の bundle analyzer の結果。core-js の比重がある程度あることがわかる

また babel や webpack の config をみると IE11 をサポートしていた時代の名残から、 regenerator-runtime が残っていることも確認できました。

regenerator-runtime というのは IE11 や古いバージョンの Safari をはじめとした async-await を対応していないブラウザ向けに必要だった機構です。拙著ですがこちらでも少し説明をしていますので参考までに。

そこで今回の目的として、「不要となった core-js を外す」だけでなく「サポート対象ブラウザを見直し、即したサポート実装にする」ことにスコープを広げることにしました。

具体的な調査の取り組み

まずビルドターゲットに関してです。N予備校ではサポート対象ブラウザを定めていますが、開発に大きな負担にならない限りはできる限り多くのユーザに価値を届けたいものであるため、 Google Analytics から実データを確認します。

幸いにして Chrome や Safari の新しめのバージョンが多く、 IE11 からのアクセスはないことがわかりました。 これらを元に、サポート対象を以下に指定すべく、 .babelrctargets に以下のように設定しました。

{
  "android": "111",
  "chrome": "108",
  "firefox": "115",
  "ios": "15",
  "safari": "14"
}

次に、 @babel/preset-envdebug option を有効にしてビルドを実行し、出力からヒントを得られるようにします。

debug option が有効になっていると、以下のように preset で挿入される plugin と、由来するブラウザバージョンが出力されます。

Using plugins:
  transform-unicode-sets-regex { android, chrome < 112, firefox < 116, ios, safari < tp }
  proposal-class-static-block { ios < 16.4, safari < 16.4 }
  proposal-private-property-in-object { safari < 15 }
  proposal-class-properties { safari < 14.1 }
  proposal-private-methods { safari < 15 }
  syntax-numeric-separator
  syntax-nullish-coalescing-operator
  syntax-optional-chaining
  syntax-json-strings
  syntax-optional-catch-binding
  transform-parameters { ios < 16.3, safari < 16.3 }
  syntax-async-generators
  syntax-object-rest-spread
  proposal-export-namespace-from { safari < 14.1 }
  syntax-top-level-await
  syntax-import-meta

これらは現段階で特段意識する必要があるわけではないですが、どの plugin が使われているか把握することは今後の見直しのために必要です。ここについては次項でも触れます。

次に core-js についてです。.babelrc を確認すると、 core-js の挿入を指定する useBuiltIns option に entry が指定されていました。

これは core-js による polyfill を各ファイルに必要に応じてではなく、全ての polyfill を一括で挿入するという指定です。

(参考: 公式ドキュメント)

このままでは影響範囲が不明であり削除しても問題ないか判断が付かないため、 useBuiltInsusage を指定します。

これは各ファイルを babel が解析し、必要に応じて polyfill を自動的に挿入してくれるというものです。

例えば、 "targets": { "ie": 11 } を指定している場合だと、各ファイルごとに挿入が必要な polyfill が debug で以下のように出力されます。

The corejs3 polyfill added the following polyfills:
  es.array.includes { "ie":"11" }
  es.string.includes { "ie":"11" }
  es.string.starts-with { "ie":"11" }
  es.function.name { "ie":"11" }
  ...

上記のビルドターゲットで挿入される polyfill があるかを確認するために useBuiltInsusage に変更しビルドすると全ファイルで以下のようにな debug 出力となり、幸いにも挿入が必要な polyfill がないことが分かりました。

Based on your code and targets, the regenerator polyfill did not add any polyfill.

これにより、現在のサポートターゲットですでに core-js が不要であり、安心して外すことが可能ということが分かりました。

最後に regenerator runtime についてです。 これは async-await が generator に変換された際に必要となるものです。

そのため、上記で出力した使用される plugin の中に transform-async-to-generator transform-regenerator proposal-async-generator-function 等がないことが確認できました。

即ち regeneratorRuntime に依存するコードが埋め込まれないため削除しても問題ないことが分かりました。

結果

上記の結果を元に、以下のように @babel/preset-env の option を書き換えました。

modules: false,
- useBuiltIns: 'entry',
- corejs: 3,
- targets: { ... }, // 一応伏せます
+ useBuiltIns: false,
+ targets: { chrome: 108, safari: 14, firefox: 115, ios: 15, android: 111 },

また webpack の entry point に直接挿入されていた regenerator-runtime も削除しました。

- import 'regenerator-runtime/runtime';

その結果出力された bundle analyzer の結果は以下の通りとなり、無事 core-js 分がまるっと削減されています。

見直し後の bundle analyzer。core-js 分が削減されている

また、今後の見直しのために、以下のようにコメントを残して上記の設定変更を終えることにしました。

/*
 * 上記のうち、 syntax 系は typescript で解決できるため、意識する必要はない。
 * 主に safari のバージョンアップによって、刺す必要のある transform plugin が少なくなった際、個別に指定することによって preset-env は削除可能となる。
 * また、サポート対象外でも該当の構文を使用していなければ、その transform plugin を刺さずとも、 preset-env を削除しても良い。
 * (e.g. transform-unicode-sets-regex, transform-class-static-block)
 * 現状でも個別に transform plugin を刺せば解決するが、 preset-env の debug option でどの plugin が刺されているか確認できるので、preset-env での運用を継続する。
 */

背景が複雑なものなので、自然言語混じりであってもコード中に記録を残しました。役に立つ日が来ることを願っています。

なお、まだ本番環境へリリースはされていないので、ドキドキしながらリリースを待ちます。

今後の方針

上記の精査により、 babel 特有のビルド事情に依存していないことが明確となったことが、次のステップの検討材料となりました。

具体的には、まずは babel のみを外し ts-loader に置き換えることでビルド環境の依存を減らすことが検討できます。

今後置き換える予定のバンドラとして十分ノウハウの蓄積されてきている vite や Webpack との互換性の高い Rspack などがあります。

そちらでは TypeScript の解釈に tsc を使用しているわけではないため、上記のような ts-loader ではなく、 SWC を事前に採用して段階的に移行していく等のプランを採ることも可能です。

現状のビルド環境がクリティカルに開発に支障をきたしているわけではないどころか十分安定して快適に動いているため移行の優先度は低めなので大掛かりに着手していないというのが正直なところではあります。

とはいえ、手が空いた際に影響範囲少なく、さくっと小さく改善できるように複数のプランを日頃から検討して用意しておけるに越したことはありません。

例えば CI のビルドを早くしたいだとか、ローカルのメモリ使用量を減らしたいだとかの欲求が高まった際に、ビッグバン的に乗り換えるのではなく、地続きに改善できることが持続可能な開発に繋がるのではないかと思います。

終わりに

今回は、最近流行りの vite に乗り換えるといった一見目を引くような対応ではなく、現在使用している技術のまま最短時間で影響を少なくビルドターゲットを見直すことを目指しました。

何もかもを新しく置き換えれば良いというわけではなく限られたリソースや費用対効果を鑑み、現状に合わせた選択をしていくことこそがエンジニアリングの真髄だと私は思っているため、今回このような(ともすれば少し時代遅れかもしれない)改善を記事にしました。

We are hiring!

N予備校の Web フロントエンドチームでは、無理なく継続的な開発をしていく手段を模索する仲間を募集しています。

カジュアル面談も行っています。 お気軽にご連絡ください!

カジュアル面談応募フォームはこちら

www.nnn.ed.nico

開発チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。

speakerdeck.com