Airシフトにおけるパフォーマンス改善とリファクタリング事例

Airシフトにおけるパフォーマンス改善とリファクタリング事例

はじめに

リクルートテクノロジーズの 辻 健人です. GitHub では maxmellon で活動しています.
今回は,『Airシフト』における パフォーマンス改善とリファクタリングの事例を一つずつ紹介したいと思います.

『Airシフト』とは

『Airシフト』は,やりとりも作成も楽になるシフト管理サービスです.
直感的に操作できるシンプルな画面で,カンタンにシフト作成が行えます.

技術スタックとしては, React/Redux ,チャット機能に WebSocket, SSR やユーザ認証,ファイルダウンロードにBFFアーキテクチャを採用しています.

『Airシフト』はシフト表の作成や管理ができるだけでなく,大きな機能の一つとして 『シフトボード』を活用したチャットを行うことも可能です.
例えば,急用で別のスタッフにシフトを変わってもらうときや,シフトの希望を提出する締め切り前にチャットを活用してリマインドを送信するといったことができます.

店長が使う『Airシフト』と,スタッフが使う『シフトボード』を組み合わせることによって,シフトを連携したりシフト表を共有したりと店長もスタッフも, 効率よくシフトの管理ができる

パフォーマンス事例

チャットのキー入力のインタラクションを改善する

『Airシフト』のチャット機能においてパフォーマンスの課題が見つかったので,修正した事例を紹介します.

Text の入力は,その他のインタラクションよりもパフォーマンスが重要で,レスポンスが少し遅れただけで入力の反応の悪さや,カクつきをユーザーが敏感に感じてしまう部分です.
また, React では入力された内容を JavaScript で扱う (Controlled-From) ようにして実装することが多く,意図せずパフォーマンスの劣化が起きてしまうことがあります.

入力のパフォーマンスの重要性は, React の公式ドキュメントでも触れられています.

研究によって、ホバーやテキスト入力といった操作は非常に短い時間で処理される必要があり、一方でクリックやページ遷移は少し時間がかかっても遅いと感じられずに済む、ということが分かっています。 (https://ja.reactjs.org/) より)

チャット機能では,複数行のメッセージが入力できるように,複数行に対応した textarea を用意する必要がありました.

開発者が使っているPCだと入力のラグなどを感じずにスムーズに入力できていたのですが, CPU を slowdown させた状態でキー入力を行うと,反応が非常に悪いという問題が見つかりました.

上の動画からは、実際に slowdown させた状態のキー入力のパフォーマンスの悪さがわかります.
文字を連続で入力したとき,すぐには入力した結果が反映されていません.
プロファイリングしてみると,

ご覧のように,KeyUpイベントやKeyDownイベントが見事に jank しているのがわかります.

更に深掘りしてみるために,1文字入力するのに所要している時間を bottom-up で調べてみると,

入力された文字に対して, textarea のサイズを調節する layout と autolinkify という関数が最も大きなボトルネックになっていることがわかりました.
autolinkify は,入力された文字列に URL が含まれている場合はaタグ化するというものですが,1文字入力するたびにそれが実行されていることが明らかになりました.

そもそもautolinkify は1文字入力されるたびに実行する必要はなく,ある程度の入力が終わってから一度だけ実行すればいいので, debounce することで改善が見込めるでしょう.
またLayout が多発しているのは, 入力毎に textarea の高さを再計算しているのが原因です.これも一文字一文字に対して実行する必要はありません.改行が入力されたときや,文字数が多く横幅が足りず,次の行に折り返されたときのみ再計算すればよい話です.

実際に CPU を slowdown させた状態で触ってみると,より違いがわかります.

React で作られた form のパフォーマンス改善には色々なアプローチがあります.

例えば,入力された文字の管理を React 側でしないという方法があります.(Uncontrolled-Form)
実際に, その方法はドキュメント(https://ja.reactjs.org/docs/uncontrolled-components.html)でも紹介されていたり, react-hooks-form(https://github.com/react-hook-form/react-hook-form) というライブラリでは,それ前提の実装になっていたりします.

しかし今回は, onChange 内で実行されている関数を一部 debounce (文字の反映自体, onChange 自体は debounce していない) することによって対処しました.
そして,入力された文字に対して操作をしたいので, Controlled From のままパフォーマンス改善をしました.

重要なのは,実際のユーザーが使う環境でストレスなく文字が入力できるパフォーマンスで機能を提供できているかどうか です.確かに Uncontrolled-From のほうが多くの場面で高速に動作しますが,その速度の差がユーザーにとって優位な差であるかどうかは,常に考える必要があるでしょう.

リファクタリング事例

コンポーネントをTS化するためのビジュアルリグレッションの導入

『Airシフト』は,3年前に開発が始まり,当初は TypeScript が現在ほど主流になるともわかっていなかったため,JavaScript で実装されていました.そして,リリースから2年が経過し,実装された機能をより堅実に運用するための仕組みや方法を検討していく必要が生じました.

当時,オブジェクトに対するnullチェック漏れがエラートラッキングツールでたびたび検出されるという課題がありました。しかしこうしたチェック漏れは,TypeScriptのような型システムの導入と, API のスキーマをキチンと型で定義することにより,開発時の段階でほとんど防げる見込みがありました. 近頃ではTypeScript が一般的に 活用されることが多くなりましたが,その大きな要因の一つとして,安全にオブジェクトを参照できることが挙げられると思っています.

null のチェック漏れによってエラーが起きた具体的なケース

そこに端を発し,コーディング規約をベースにして人が品質を担保するという安心な世界観から,TypeScript を活用した静的解析・型システムで均一に品質を担保するという安全な世界観に移行していきたいと考えるようになりました.
React-Nativeで作られた『Airシフト メッセージ用アプリ』での導入実績もあり,チームメンバからも TypeScript を導入したいという声が強まってきました.

ユーティリティ関数や, Redux にまつわる各種レイヤに関しては単体テストがあるため, JavaScript から TypeScript へ移行することによるデグレのリスクをある程度防ぐことができていました.しかし,Component に関してはテストコードが用意できておらず,特に見た目のデグレが起きてしまう懸念があり,移行の妨げの一要素となっていました.

その課題を解決すべく, storybook(https://github.com/storybookjs/storybook) と reg-suit(https://github.com/reg-viz/reg-suit) を組み合わせることにより,安全にコンポーネントをリファクタリングする環境を用意しました.

reg-suit は,任意の画像を比較するために活用できるツールで,スクリーンショットを取る単位を変えることで様々な運用ができます.
『Airシフト』では,storybook と組み合わせて活用し,コンポーネント単位での比較に焦点を当てました.これにより,コンポーネント各種の安全なリファクタリングが実現できました.

コンポーネントの見た目の差分が見つかったときは,このように様々な方法で見た目を比較することができ,
意図して見た目を変更したものか, 依存コンポーネントが書き変わったことなどが起因して崩れてしまったのか, UIから簡単に確認できます.

これにより,チームメンバの力を借りて,末端のコンポーネントからどんどん TypeScript 化を行うことができています.
デグレを防ぐことはもちろん,cssなどをいじったときに意図せずコンポーネントが壊れる事象も検出することができます.

またコンポーネントにおいて,特にロジック以外の見た目の部分は,テストコードによるデグレの検出が難しくなることが課題でした.今回はその課題を,reg-suit というツールで解決しようと試みました.

↑ のスクリーンショットは, CI で reg-suit を実行し, TypeScript に移植したあとのコンポーネントにおいて,実際に見た目のデグレが発生しないないことが検出できている様子です.

とはいえ決して,ビジュアルリグレッションだけを導入すれば良いというわけではありません。リファクタリングで重要なのは, いかに安全にソースコードを書き換えることができるか,またいかに書き換えたあとの機能をデグレさせずに移行させる安全な仕組みを導入するかだと考えています.

  • ビジュアルリグレッションで見た目が壊れる心配をせず,マークアップ部分をリファクタリングする
  • TypeScript の導入によって,型情報から安全に参照できているかを検査する
  • 単体テストを活用して,実振る舞いに影響がないことを保証する
  • lint などを使って,アンチパターンな書き方を事前に防止する

これらをすべて組み合わせることによって,はじめて安全な運用が実現できると考えています.

まとめ

パフォーマンス改善

今回のパフォーマンス改善では以下のことが大事という大きな学びを得ました.

  • いくつかある改善手法から目的を達成するのに最もコストが低く効果が大きいものを選定すること
  • 改善後の速度がユーザーにとって優位な差であるかどうかを考えることが大切

そしてチーム全体がただ機能を作るだけではなく, 提供している機能をユーザーが心地よく扱えるかどうか,継続的に考えていくことが大切でしょう.

リファクタリング

検出しているエラーによると, 型システムの導入によってアプリケーションの信頼性を向上させられる見込みがあったので,非常に大きな JavaScript のソースコード群を TypeScript へ移植することにしました.
そこで, コンポーネントの移植を安全に行うために,ビジュアルリグレッションを実現するツールとして reg-suit を導入しました.
まとめると,

  • TypeScript を導入する事によって, Objectを安全に参照することが可能
  • Visual Regression を導入することによって, testコードが書けない見た目の部分を安全に担保できるように

壊れたことを仕組みによって自動で検出できればソースコードを安全に編集することができるということが,コンポーネントの移植を通して実際に確認することができました.

おまけ

私達はこのようなパフォーマンス改善やリファクタリングを,『Airシフト』に限らず様々なプロジェクトで実施しています.
今回のキー入力の改善や TypeScript 移行も,いくつか進めている改善活動のうちの一つにすぎません.実際のサービスを使って,私達とハッカソン形式でリアルな改善活動にトライしてみませんか?

イベントでは,実際にサービスを開発しているメンバから,この記事で紹介した事例以外についても直接聞く事ができます.

皆さんのご参加お待ちしています!