こんにちはー。突然ですが、聞いてくださいよー。
Babelのバージョンアップしたら「Chatworkのルーム切り替えが重くなった」と社内で言われてしまいました。 みんなの仕事の効率を悪くするわけにもいかないので、戻すしかありません。Babelの更新って、本当に怖いですよねー。
そんなわけで、こんにちは。フロントエンド開発部のひむら(id:eiel)です。
さて、この話自体は少し前のことなのですが、その際に原因を特定する余裕がなく、Babelの更新は後回しになっていました。 ルーム切り替え自体が歴史的経緯もあって、「とーっても」*1難易度が高くなっていて、最悪これを改善すれば更新できるだろうと期待もしてたりもしました。 ところが、うっかり再発させてしまったので、ここで気合をいれて改善することにしました。
今日はその話を記録しておきます。
要約
@babel/preset-env を更新したらパフォーマンスが劣化したので、@babel/preset-envのlooseオプションをtrue
に設定した。
全体の流れ
- @babel/preset-envを更新するとルーム切り替えが遅くなることがわかる
- ターゲットからIE11を外すと問題が起きないことがわかる
- IE11を有効にすることで有効になるpluginを確認する
- 問題となるpluginを特定する
- 解決方法を考え、設定を行う
経緯
そもそも @babel/preset-envを更新した経緯ですが……事故がおきました。 共有漏れと確認漏れです。つまり、運用のミスです。
Babelを更新するとルーム切り替えが遅くなることがわかっていたにも関わらず、更新してしまいました。
そして「とーってもルーム切り替えが遅い」という指摘が社内から上がり、リリース前に気づく事ができました。 この時も一旦revertしたのですが、アップデートされないように予防策を練る必要があります。 とはいえ、根本改善しておくほうがトータルで良いだろうと判断して、調査してみることにするのでした。
だいたいこんな感じの時系列です。
- 2018年頃 ts-loader babel-loaderの併用状態になる
- 2020年夏 併用をやめるためにts-loaderからbabel-loaderに移行する
- 2020年夏 babelの更新を行うが問題が発生。一部もとに戻す
- 2021年3月 @babel/preset-envをアップデートしてしまい問題が発生。元に戻す
もともとChatworkでは、ts-loaderとbabel-loaderが併用されていていました。
そのとき私はフロントエンドチームにいなかったので経緯は把握していませんが、Babelを通していないnpm パッケージがいて、動作させるためにBabelを利用するのが目的のようです。
時は流れ、TypeScriptのトランスパイルがBabelでできるようになりました。 将来的にはBabelでトランスパイルができる状態を維持できているのが良いと考え、ts-loaderに別れを告げました。 ということで、Babelのバージョンを上げることの重要度が増していました。 そこでBabelのバージョンを上げにいったのですが、問題が起きてしまったのです。
そんなわけで、アップデートはしばらく保留したものの、アップデートをしてしまうという運用で問題を起こしてしまいました。
原因の特定
2020年の問題発覚時点で@babel/preset-envが怪しいと突き止めていたので、@babel/preset-envと@babel/polyfill だけアップデートできていない状況でした。*2
開発者ツールのPerformanceでプロファイルを取得したりもしたのですが、わかりやすく処理が重くなっているところがありませんでした。 あえて気になった点があるとするなら、sort関数で使うsortFnの実行時間が増えている傾向がありましたが、全体として微々たるもので決定打はありません。 状況からして「基本的な部分でのオーバーヘッドが増えたのだろう」と考えました。 そこまで推測はしたのものの、その時点での調査はそこで終了になりました。
さて今回の話です。
IE11関連の話題も活発になってきていたため「IE11無効にしたら問題が起きるのか気になる」という話題もあり、そこから試すことにしました。 IE11をターゲットにすると有効になるpluginはとても多いです。 問題がおきなければ、IE11のサポート終了を待つという選択肢も出てきます。
試しにIE11をターゲットからはずす
結論を述べるとIE11をターゲットから外すと問題が改善することがわかりました。
といわけで、以下2点のことがわかりました。
- IE11をターゲットにすることで有効になるプラグインに問題がある可能性が高い
- 自分の環境でも問題をかろうじて体感できる
ルーム切り替えが重いといえども、重さがわかる人が非常に限られていました。 手元の環境で問題が再現できなければ改善できたかどうか判断がとても手間です。 自分の環境で再現できるならだいぶ解決しやすくなります。
さて、IE11をターゲットから外すと改善するなら「ChatworkがIE11のサポートをやめるのを待つ」という選択肢もあると書きました。 しかし、いつになるかわかりません。いつまで待たなければいけないかわかりません。いつまでもは待てません。
まだまだできることがありそうなので、調査は続行です。
補足 IE11を @babel/preset-envのtargetから外す
.browserlistrc
を利用しているので、そこからIE11以上の設定を外して確認しました
以下はdiffです。
-ie >= 11
問題となるbabelのpluginを特定する
IE11をターゲットから外すと改善することがわかったので、IE11で有効になるpluginを確認することにしました。 そして、これらのpluginを無効にしていくことで問題になるpluginを特定します。*3
恥ずかしながら有効なpluginを確認する方法を見つけられなかったので、情報源になりそうなjsonから抜き出しました。
babel/plugins.json at main · babel/babel · GitHub
具体的なことは以下の記事にメモっています。
有効になるpluginの一覧を入手したので、.babelrcのexcludesに記述します。 そして、半分に分けて有効にし、重いと感じるものを絞りこんでいきます。 確認は、productionビルドを行い、何度もルーム切り替えをおこない、微妙な違いを自分の感覚で判断します。
「早いときはこの背景色の変化がクリックイベントに吸い付いてくる感じ」と内心思いながらの判断でした。(伝わる気がしない)
ちょっとつらい作業でしたが、結果、以下の2つが怪しいと判断しました。
あらためて思うと、パフォーマンスを計測する自動テストを用意した上でやると効率がよかったかもしれません。 とはいえ、実際のルーム切り替えの体験とどれくらい対応しているのかわからないので、有効なものが作れるかどうかは定かではありません。今後の課題です。
@babel/plugin-transform-unicode-regex をどうするか
transform-unicode-regex ですが正規表現リテラルでuフラグを利用していると正規表現が長く展開されてしまうようです。 利用しているところがあれば問題ありませんし、いざとなればIE11だけフラグをはずしてしまえばよさそうです。
といっても使っているところがなかったので、これは私の誤検知の可能性が高いです。そもそも使っているとIE11ではエラーになります。
@babel/plugin-transform-classes をどうするか
これは無効にすることが難しいです。
徐々に関数コンポーネントに書き換えてはいますが、コンポーネントだけではなく、ドメインモデルやドメインロジックがクラスで実装されているところがあります。*4
@babel/plugin-transform-classes を過去のバージョンを維持するという手も試してみましたが Parameter Properties が効かないという問題がおきました。 いろいろやってみたり、調べた結果 looseオプションを有効にするのが効果があることがわかりました。 looseオプションについてすごく簡単にかくと、完全にはclassの動作を再現しないけど、軽量な実装へと変換するようになります。
補足 特定のpluginを過去のバージョンで利用する
特定のpluginだけ特定のバージョンを指定するのはなんやかんやあって難しいです。 なんやかんやは長くなりそうなので割愛させていただきます。
私が試した方法は、preset-envの設定でexcludeし、個別に使いたいバージョンのパッケージをnpm install して pluginに記述するという方法です。
具体的には、こんな感じです。
module.exports = { presets: [ [ '@babel/preset-env', { exclude: [ '@babel/plugin-transform-classes', ], }, ], '@babel/preset-react', '@babel/preset-typescript', ], plugins: [ '@babel/plugin-transform-classes', ], ],
たいへんだったこと
問題になるpluginを探すとき、あるpluginを無効にするとトランスパイルができなくなることがあり、合わせて無効にしないといけないpluginがありました。 これがなかなか面倒で把握しきれなくなってしまいました。 仕方ないので、トランスパイルができるのを確認したらコミットするようにしました。
git log を抜粋するとこんな感じです。*5
17:03:49 2021 +0900 ビルドできた 16:59:09 2021 +0900 ビルドできた 16:57:44 2021 +0900 ビルドできた 16:53:21 2021 +0900 ビルドできた 16:51:55 2021 +0900 ビルドできた 16:37:43 2021 +0900 ビルドできた 16:20:27 2021 +0900 ビルドできた 16:16:58 2021 +0900 ビルドできた 15:56:59 2021 +0900 ビルドできた 15:48:00 2021 +0900 ビルドできた 15:45:54 2021 +0900 ビルドできた 15:39:14 2021 +0900 ビルドできた
おわりに
思うこと
Babelの更新は影響範囲が大きくちょっと怖いです。
すこしでも不安をへらすために、パフォーマンスを確認する自動テストが欲しいと感じました。 しかし、実際の利用する際に影響でる範囲がどのレベルなのか、そのテストで検知できるのかは不明です。 実際の体験はユーザが持つデータで変わります。 とはいえ、目安にはなるのでしょう。
よくなったこと
一応リターンもかいておきます。
- StorybookでTypeScriptのParameter Propertiesが動かないという問題が起きてたのが治りました。
- Optional Chaining と Nullish Coalescing が解禁されました*6
わかったこと
パッケージの更新はバージョンの差が開けば開くほど、原因の切り分けが面倒です。 リリースされるたびに行っていれば、リリースノートや変更内容から特定しやすくなるでしょう。 差が大きければすべてを把握するのは難しいです。
バージョンを少しづつあげれば良いと感じるかもしれませんが、preset-envが依存するパッケージなので、たいへん面倒です。 たとえ、 preset-envを最新バージョンを指定しなくてもpluginは最新のものがインストールされてしまいます。
こういったアプリケーションの関心と外れる部分は、コストを抑え、いつでも先手を打てる状態が望ましいでしょう。 対策として、Renovateを導入をしていますが、ちょっと別の問題が起きており奮闘中だったりします。
まとめ
利用していたバージョンは書きたいところですが、なんやかんや調査不足でこの記事には記載しておりません。ご了承ください。
ライブラリの更新というのは危険を伴うので、ついつい保留しがちだったりします。 「動くものをいじるな」という話も昔はありました。今は自動テストがありますが、不安なものは不安です。 また、パフォーマンスに関する自動テストができていません。 不安といえども、セキュリティ的なリスクもありますし、ますます更新が難しくなります。 バージョンアップは必要です。 RenovateやDependabotを活用して効率よく先手を打っていくことは、とーっても大切だと感じました。
ということで、アプリケーションを開発しながら、片手間で開発環境を改善するのはなかなか大変です。 フロントエンドチームではフロントエンドの開発環境をもっとよくしてやるぜ!といったエンジニアを募集しています。
おしまい。