画面の表示が遅い原因を調査したら Nuxt I18n にコントリビュートしていた – kickflow Tech Blog

翻訳ファイルを直列読み込みから並列読み込みに変更した時のイメージ画像。緑色の光が横一線に走るサイバー風の背景に、右側へ伸びるデジタル回路と四角形のバーが並ぶ抽象的なイラスト。周囲に  Approver、Tickets、Workflow、チケット、承認者など英語と日本語・韓国語・中国語の単語が半透明で散りばめられている。

こんにちは、プロダクト開発本部でエンジニアをしている秋山です。

kickflow ではほぼ毎日何らかのリリースを行っており、機能アップデートもハイペースです。今年はこの記事の執筆時点で 58 件のアップデートを行っています。

プロダクトは日毎に改善が進んでいるなか、「初回アクセス時に画面がなかなか表示されずに困っている」という報告が挙がりました。以前はそこまで遅くなかったのに、ここ最近表示の遅さが顕著になったという意見もありました。

kickflow では非機能要件としての表示速度指標が明確に定められておらず、個々の感覚に頼る部分が多い状態でした。そのため、運用の中で何らかのリリースを起点として、意図せずパフォーマンスが劣化してしまった可能性があります。

初回表示以降は問題が目立って発生しないことから、キャッシュが存在しない初回アクセス時のファイルロードに問題がある状態が考えられますが、まずは「推測するな、計測せよ」の格言のもと、実態調査に取り掛かりました。

Core Web Vitals とは

フロントエンドのパフォーマンス指標としては Core Web Vitals を用います。

Core Web Vitals の指標図解。Largest Contentful Paint(LCP)、Interaction to Next Paint(INP)、Cumulative Layout Shift(CLS)の3見出しと、それぞれの Good/Needs improvement/Poor のしきい値バーが横並びで示されている。
web.dev の Web Vitals (https://web.dev/articles/vitals)より引用

Core Web Vitals は Google が提唱しているユーザー体験指標です。
Largest Contentful Paint(LCP)、Interaction to Next Paint(INP)、Cumulative Layout Shift(CLS)の3つの指標があり、その中でも今回は初回表示が遅い原因の調査には LCP を参照します。
LCP はページが読み込まれてから最初にユーザーが認識するコンテンツの描画が完了するまでの時間を示し、この時間が長ければ長いほど体感的な画面の表示が遅いということになります。
推奨値は 2.5 秒以下で、4.0 秒以下を目標とすることが望ましいとされています。

また LCP はサーバー(API やデータベース)のレスポンス速度、関連ファイルの転送量、JavaScript の実行時間、CSS の解析量、DOM レンダリング量、ユーザー環境の回線スピードやクライアント端末の CPU 性能など、複合的な要素の影響を受けます。数値がブレやすいため、複数回計測したうえで平均を取る必要があります。Core Web Vitals は 75 パーセンタイルでの評価を推奨しています。

LCP を計測する

手っ取り早く LCP を計測するには Chrome DevTools の Performance を使います。
Performance タブの Environment settings で CPU:4x slowdownNetwork:Fast 4GDisable network cache を有効にします。昨今の開発者向けの環境は通信環境は高速で安定していますし、PC は高性能なものが多いので、普通に動かしている分には何の問題なく使えてしまいます。CPU と Network にスロットリングをかけることで、パフォーマンス影響がシビアになり、ボトルネックの判断がしやすくなります。
この状態で該当ページを開くと Local metrics に Core Web Vitals の値が表示されます。

Chrome DevTools の Performance パネルのスクリーンショット。左に Local metrics で LCP 12.65s、CLS 0、INP 0ms が表示され、右に Next steps と環境設定、下部に Record ボタンが見える。CPU は 4x slowdown、Network は Fast 4G に設定。

そして計測の結果、LCP はおおよそ 12 秒となりました。
Core Web Vitals では LCP が 4 秒以上となるとユーザー体験として致命的な問題となる数値で、その3倍以上の時間がかかっている状態なので、これは表示が遅いと言われても仕方がない結果です。当面の目標を「Needs improvement」の閾値である 4 秒以下まで下げることとし、原因を探っていきます。

原因の判明

では、kickflow のパフォーマンス低下の原因を深掘りしていきます。
このまま、Record and reload を実行すると詳細を計測できます。

Network グラフを見ると、明らかに異常な数の直列読み込みファイル群が存在していることが分かります。これが明らかに通信の渋滞を引き起こし LCP を押し上げている原因になっています。

Performance 記録のタイムライン。約 14,000ms までの直列読み込みされているネットワークリクエストのグラフが表示されている。

何でこのような状況になってしまったのでしょうか? またこのファイルたちは何者なのか?
このファイルの中身を確認したところ、これは多言語の翻訳ファイルでした。

kickflow は日本語・英語・韓国語・中国語(簡体字/繁体字)の 5 言語に対応しています。翻訳は JSON ファイルによって管理され、1 言語につき 28 ファイルの定義ファイルが存在しており、デフォルト言語は日本語で、定義のない翻訳は英語でフォールバックする設定になっています。
また、フロントエンドに Nuxt を利用しており、多言語機能は Nuxt I18n を使ってサービスを提供しています。Nuxt I18n で複数の翻訳ファイルを指定するときは nuxt.config.ts に以下のように書くのが定石です。

export default defineNuxtConfig({
  i18n: {
    locales: [
      {
        code: 'ja',
        files: ['ja-1.json', 'ja-2.json', 'ja-3.json', ],
      },
      {
        code: 'en',
        files: ['en-1.json', 'en-2.json', 'en-3.json', ],
      },
    ],
  },
})

しかし、この locales[].files[] で翻訳ファイルを複数指定をすると、 Nuxt I18n では直列読み込みになってしまうようです。しかも英語によるフォールバック指定があるため、日本語環境のユーザーがアクセスすると、日本語と英語の計 56 個の翻訳ファイルが直列で読み込まれる状態になっていました。

先ほど「以前はそこまで遅くなかったのに、ここ最近で顕著に遅くなった」という意見があったのを思い出し、コミット履歴から以前の翻訳ファイルの構成を調べてみました。すると、以前は 1 言語 1 ファイルの構成であったものの、生成 AI による多言語翻訳対応を導入するタイミングで、翻訳ファイルを分割したことが分かりました。

生成 AI による多言語対応に関しては以下の記事で詳しく解説されているので興味のある方はご参照ください。

tech.kickflow.co.jp

翻訳ファイルはサイズが大きかったため、生成 AI のコンテキスト消費を抑えて翻訳精度を上げるためにファイルの分割が必要だったのです。
この実装時に翻訳ファイルを分割し locales[].files[] で読み込みを指定した結果、Nuxt I18n の挙動によりファイルの読み込みが直列化され、LCP 悪化の結果につながっていました。

翻訳ファイルを従来どおり 1 言語 1 ファイルに戻せばパフォーマンスの問題は解決しますが、そうすると今度は AI の翻訳処理に影響が出てしまいます。そこで当面はソースを分割のままにし、ビルド時に 1 ファイルへ結合する方針で対処しました。

改善後の Performance 記録のタイムライン。Insights に LCP 4.10s と表示され、直列読み込みが解消され、約 4,500ms までのネットワークリクエストのグラフが表示されている。

これで、修正後の LCP は 4.1 秒となり本来の状態に戻ったことで初期表示速度は改善されました。

また、今後はリリース起点による意図しないパフォーマンス劣化が起きないように、Sentry で Core Web Vitals の数値を監視をする運用ルールを追加することになりました。

OSS に貢献しよう

そもそも Nuxt I18n の翻訳ファイル読み込みが直列ではなく並列であれば、ここまでパフォーマンスに影響は出なかったと考えられます。Nuxt I18n は Star 2,000 超で歴史も長く Nuxt モジュールの中でも人気の高いパッケージですが、こうした比較的起こりやすいケースが残っているのは少々意外な気もしています。

今回この問題に遭遇し調査と改善方法が判明している状況なので、普段から便利に使わせていただいている Nuxt とそのモジュール群に対して恩返しの意味も込めて、パフォーマンス改善のプルリクエストを発行しておきました。

私は英語が得意ではないのですが、生成 AI のおかげで大いに助けられました。
英語によるコントリビューションの敷居を限りなく下げてくれます。

github.com

上記 Pull Request は無事にマージ・リリースさました。Nuxt I18n v10.2.1 以降では  locales[].files[]  を使用しても、同様の問題に起因するパフォーマンス劣化は発生しなくなりました。

速いは正義

改善後 LCP は 4.1 秒になりましたが Core Web Vitals としては依然 Poor 判定であり、表示速度としてはまだ遅い状態です。より速く画面を表示させるべく、追加で以下のようなパフォーマンス改善を行いました。

  • Nuxt が自動で追加する prefetch を抑制して通信量を最適化
  • Resource Hints を使った通信の投機的な先読み
  • ベンダーライブラリを見直しや、Nuxt のグローバルプラグインを Composable に置き換えるなどして、エントリーファイルのバンドルサイズを削減
  • 初期表示に必要な非同期処理の最適化

それぞれの具体的な内容は細か過ぎるのでここでは省略しますが、軽微な改善をたくさん積むことで最終的に現在は LCP は 3 秒 にまで改善され、当初の目標値であった Needs improvement ラインは達成しました!

パフォーマンス改善後の Performance 記録のタイムライン。Insights に LCP 3.06s と表示され、約 3,000ms までのネットワークリクエストのグラフが表示されている。

表示速度が速くなることで一番恩恵を受けるのはサービスを利用するユーザーであることは当然なのですが、加えて E2E テストの実行効率も上がります。
kickflow では特に E2E テストには力を入れており、Autify や Cypress でたくさんのテストケースを実行しています。

tech.kickflow.co.jp

tech.kickflow.co.jp

これらのテスト実行時間が短縮され、間接的に業務効率の改善にも繋がることになりました。

おわりに

今回のパフォーマンス改善で一定の効果を得ることはできましたが、パフォーマンスチューニングはまだ道半ばです。
kickflow は Single Page Application (SPA) 構成となっており、それゆえの制限はありますが、まだ改善の余地があります。

  • さらなるバンドルサイズの削減
  • 翻訳ファイルの読み込み戦略の見直し
  • 認証処理のタイミング調整
  • リダイレクト処理のタイミング調整

などを行えば、Core Web Vitals の LCP Good ラインである 2.5 秒を切ることができると考えています。

快適に kickflow を利用していただけるよう今後も継続的にパフォーマンス改善を進めていきますので、これからもどうぞよろしくお願いします!

We are hiring!

kickflow(キックフロー)は、運用・メンテナンスの課題を解決する「圧倒的に使いやすい」クラウドワークフローです。

kickflow.com

サービスを開発・運用する仲間を募集しています。株式会社kickflowはソフトウェアエンジニアリングの力で社会の課題をどんどん解決していく会社です。こうした仕事に楽しさとやりがいを感じるという方は、カジュアル面談・ご応募お待ちしています!

careers.kickflow.co.jp




元の記事を確認する

関連記事