Noh | エンジニア向け情報共有コミュニティ
Signup / Login

Nexjsアプリのパフォーマンスを上げるためにやったこと

nextjs
react

投稿日: 2024/06/15

更新日: 2024/06/18

yosiです。
好きなハンバーガーはベーコンレタスバーガー、好きなチェーンはモスとフレッシュネスです。

僕の運営しているNohでは、機能追加を最優先で開発を行ってきたのですが、ちょっとパフォーマンスが気になってきたので今回改善することにしました。
こういうちょっと面倒な作業は思い立ったが吉日、自分の気持ちが変わらないうちにやってしまいます。

前提

Nohのパフォーマンスを考えるうえで、関係する大きな要素は主に2つです。

1つ目はホスティングに使っているCloud Runのコールドスタートです。
これについては最小インスタンス数を上げることで改善ができます。しかし、運用費用は抑えたいので、今のところは変更しません。収益構造がもう少し改善したらやる予定ではいます。
したがってこの記事では触れません。

2つ目はアプリケーション自体のパフォーマンスです。
今回はこちらの改善をしていきます。

PageSpeed Insights を確認

まずは気軽に確認できるPageSpeed Insightsを確認してみます。

まずトップページ

SSRしているのでFirst Contentful Paintなどは非常に良好ですね。
しかし、Total Blocking Time(TBT)はかなり悪いです。(TBTはタスクの処理時間が 50 ミリ秒を上回った場合のコンテンツの初回ペイントから操作可能になるまでの合計時間)
特にNext.jsではリンククリック時にJSを使ってSPA的にページ遷移するので、この待ち時間はページ遷移できずストレスになります。

改善前のトップページのレポート

次に記事を表示しているページ

改善前の記事ページのレポート

より悪いですね。
実際にアクセスしてみるとそこまでストレスは感じないですが、TBTはやはり改善が必要でしょう。

@next/bundle-analyzer でJavaScript モジュールのサイズを分析する

@next/bundle-analyzer を使うことで、各モジュールのサイズと依存関係の視覚的なレポートを生成できます。

https://nextjs.org/docs/app/building-your-application/optimizing/bundle-analyzer

導入するにはまず @next/bundle-analyzer をインストールします。
公式ドキュメントに-Dオプションはないですが、開発環境で十分なのでつけています。

npm i -D @next/bundle-analyzer

next.config.js に設定を追加します。

next.config.js

+ const withBundleAnalyzer = require("@next/bundle-analyzer")({ + enabled: process.env.ANALYZE === "true", + }) /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, } - module.exports = nextConfig + module.exports = withBundleAnalyzer(nextConfig)

これでもうレポートを作成できますが、npm scriptを追加しておくと便利です。

package.json

{ ... "scripts": { "analyze": "ANALYZE=true npm run build",

このコマンドを実行するとレポートが作成されます。

npm run analyze

.next/analyze/にいくつかファイルが生成されますのでfile://wsl.localhost/Ubuntu/home/.../.next/analyze/client.htmlのようなパスで直接ブラウザで開いて確認します。

改善1 SyntaxHighlighter関連の読み込みを遅延&読み込むモジュールを減らす

@next/bundle-analyzer の分析結果を見ると react-syntax-highlighter関のコードが大きいことがわかります。ちなみにrefractorも react-syntax-highlighterの内部で使われてるモジュールです。

この部分をチャンク分割するとトップページでは効果ありそうです。

react-syntax-highlighter関連の読み込みを遅延

react-syntax-highlighter は遅延読み込みに対応してくれてます。

- import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" + import { PrismAsync as SyntaxHighlighter } from "react-syntax-highlighter"

PrismAsyncLightというものもあり、refractorのモジュールをさらに細かいチャンクに分割してくれます。ただ、そちらは私のアプリケーションと相性が良くなかったので使いませんでした。

読み込むモジュールを減らす

Prismの使ってないモジュールが使われてることにも気づきました。

インポートの部分を変更して対応します。

- import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism" + import vscDarkPlus from "react-syntax-highlighter/dist/cjs/styles/prism/vsc-dark-plus"

かなり小さくなりました。

改善1の結果を確認

再度PageSpeed Insightsを確認してみます。

まずはトップページ。

パフォーマンスの数字は72→74と改善しました。
TBTについても2070ms→1490msと改善しました。

次に記事ページ。

パフォーマンスの数字は69→70、TBTについても5040ms→3810msと改善しました。

両方のページで多少は改善しましたが、まだまだ遅い状態です。

改善2 reactfireの削除

reactfireから Tanstack Query (React Query) への移行

Nohではreactfireを使っていました。
reactfireはFirebaseの処理をReact Hooksに落とし込んでくれていて、特にFirestoreの状態管理で便利だったので使用していました。

しかしモジュールサイズを確認するとかなり大きく、Parsed sizeで217.5kbありました。また、歴史あるPackageではありますが、近年アップデート頻度が落ちていて今後のサポートに不安がありました。

Nohがreactfireを使っているコードが少なかったこともあり、reactfireの削除を決めました。
代わりの状態管理には Tanstack Query (React Query) を使用しました。@next/bundle-analyzer で確認するとTanstack Query (React Query) のサイズはParsed sizeで12.56kbでした。

改善2の結果

ほぼ変わりませんでした。

TBTの計測結果が1500msくらいぶれるので判断が難しい。

Chromeの検証ツール(Web Inspector)のperformanceタブでの分析

ここまで多少の改善はできましたが、まだTBTに問題があります。特に記事ページのTBTは問題でしょう。
Next.jsで使われてるチャンク分割の設定はよくできてると思います。いたずらに変更しても大きな改善は見込めない気がします。

より詳細な分析に移ろうと思います。

Chromeの検証ツール(Web Inspector)のperformanceタブでの分析

現状最も大きな問題は記事ページのTBTが非常に遅いことです。
したがって記事ページに絞って調査しようと思います。

ここからは開発環境にアクセスして計測しています。
Chromeの検証ツールのperformanceタブを開いて計測を行うとい画像のようになりました。

赤の矢印でマークした部分を見てください。
赤の斜線になってますが、これはメインスレッドをブロックしているロングタスクを表してます。
そして、そこをクリックした後に「Bottom-Up」タブで詳細を見れますが、refractorやPrism関連の処理がかなりの時間を占めていることがわかります。

refractorやPrismはreact-syntax-highlighterに使われているPackageで、ソースコードを表示するときのシンタックスハイライトに使われています。
便利なPackageですが、さすがにリソースを使いすぎてます。
試しにreact-syntax-highlighterへの依存を消してみると332.8ms→161.3msになり、半分くらいになります。

検証ツールのLighthouseでも確認

検証ツールのLighthouseでも確認でreact-syntax-highlighterの有無で差が出るか検証します。

react-syntax-highlighter有りのときTBTは3270ms、無しのときTBTは1620msになりました。
原因はこいつでした。

改善案

いくつか改善案は思いつきます。

  • App Routerに移行してReact Sever Component(RSC)でレンダリングする

    • 現在はPages Routerを使用しているのですが、RSCを使うようにすればクライアント側でのレンダリングがなくなるのでかなり改善が見込めます。良い案です。しかし私のアプリケーションはApp Routerに未対応のPackageを複数使っているため難しいです。
  • シンタックスハイライトの変換処理部分をサーバーサイドで行う

    • 例えばshikiに移行してcodeToHastcodeToHtmlを使ってサーバーサイドで計算して、フロントエンドでは表示に関する処理だけ行う案ですね。悪くない案ですが、react-markdownを使ってる関係上、マークダウン内にあるコードブロックをgetServerSideProps内で取得できません。それをやるには巨大なリファクタと車輪の再開発が待っています。私のアプリケーションには合っていません。
  • Web Workerを使ってメインスレッドとは別スレッドで処理

    • 重い処理を別スレッドで実行させることで、メインスレッドで動くUI処理をブロックしなくなります。これによってTBTの改善が見込めます。悪くない案で私のアプリケーションには合ってそうですが、処理やコードの複雑性が増します。
  • react-syntax-highlighterを軽量なPackageに変更

    • shikiを使ってみましたが、同程度の処理時間はかかっていました。やはりこういった処理はどうしても負荷が高いみたいです。信頼性や使用感を含めて検討すると候補も少なく難しいです。

他にもありますが、よさそうな案はこんなものでしょう。

App Routerに移行が一番良い案だと思います。ただ、使っている他のPackageがApp Routerに対応してない問題があるので、少し時間がかかりそうです。
少し中途半端ですが、この記事はここまでにしておきます。続報を記事にできたら追記しますね。

yosi

Noh を作ってるエンジニア。