Next.jsのAppRouterではコンポーネントに'use client'付与することでServer ComponentとClient Componentの境界を指定することができ適切にこの境界を定めることによってパフォーマンスを最適化することができるのですが開発ではとりあえず機能要件満たすことに注力して「hooksやWeb API使いたくなった時にとりあえずuse client付与していく」という雑な実装をしてしまっていたのでパフォーマンス検証アプリを作って理解を深めるため色々実験してみました。
前提
Server ComponentとClient Componentについて
Next.jsのAppRouterではデフォルトではコンポーネントはServer Componentとして扱われサーバ側のリソースを使ってDOMを構築します。
一方use client指定した場合はブラウザのリソースを使ってスクリプトを実行してDOMを構築します。
ほぼ間違いなくブラウザのリソースよりもサーバ側のリソースの方が潤沢なので重い計算を実行する場合はuse serverの方がパフォーマンスが良くなります。
ただしServer ComponentではサーバサイドでDOMが構築されるのでコードの中でlocalStorageのようなWeb APIにアクセスしたりユーザの操作による状態の変化を検知することが必要な関数を実行することができずそれらを使用したい場合はClient Componentとして扱う必要があります。
また注意点としてはuse client指定した場合そこからレンダリングする小componentはuse client指定しなくともClient Componentとして扱われます。
Server ComponentとClient Componentでどの程度パフォーマンスに差が出るか実験
2つcomponentを作ってそれぞれClient ComponentとServer Componentのpage.tsxからレンダリングするコードを作ってVercelにデプロイし挙動を確かめてみました。
- 無駄に重い計算をスクリプトで実行して結果の数字を配列にpushして画面に表示するコード
- 予めプロジェクトのフォルダに保存してある画像を大量に画面表示するコード
先に結果
1の無駄に重い計算をスクリプトで実行して結果の数字を配列にpushして画面に表示するコード→そもそもServer Componentでないとデプロイできず
2の大量に画像を表示するコード→Server ComponentでもClient Componentでもあまりパフォーマンス変わらず
なぜこのような結果になったか使ったコードも記載しつつ考えてみます。
無駄に重い計算をスクリプトで実行して結果の数字を配列にpushして画面に表示する実験
用意したコード
iterations変数の値分ループして対象の数字にsqrtやsin関数で無駄に計算かけて配列にpushして結果を画面に表示するコードです。
heavy_computation.tsx
const iterations = 1000 const results = [] export default function HeavyComputation() { const startHeavyComputation = () => { for (let i = 0; i < iterations; i++) { const result = Math.sqrt(i) * Math.sin(i) if (i % (iterations / 10) === 0) { results.push(result) } } } startHeavyComputation() return ( <> <h1>Heavy Computation</h1> <p>Results: {results.join(', ')}</p> </> ) }
これをpage.tsxで表示してvercelにデプロイしてみます。
// このuse clientをつけたり外したりして動作検証しました 'use client' import HeavyComputation from '../../components/heavy_computation' export default async function UseClientMobilityAlbum() { return ( <> <HeavyComputation /> </> ) }
結果としてはiterationsの数を10とかに減らそうがuse client記述するとVercelのデプロイでエラーになりました。
このように計算量が多くて(といっても計算量ほぼO(iterations)ですが)ユーザの操作やブラウザのストレージ内容に依存しないcomponentはServer Componentとして表示するのが適切、、というか変にオプションいじったりしなければ強制的にそういった実装に誘導されるようです。
大量に画像を表示する実験
画像表示ってなんとなく重そうだし結果に差が出るんじゃ?くらいの軽い想定で着手しましたがこれは結果としてあまり差はありませんでした。
用意したコード
public/tokyo_mobility_show/mobility_show1.jpgのような画像が連番で20個Nextのプロジェクトに登録されておりそれをひたすら表示するコンポーネントです
// 検証のためnext/imgを外してパフォーマンスを下げる // import img from "next/img" export default function HeavyMobilityshowAlbum() { const totalImages = 20 const images = Array.from({ length: 100000 }).map((_, index) => ({ src: `/tokyo_mobility_show/mobility_show${(index % totalImages) + 1}.jpg`, width: 400, height: 400, })) return ( <> <h1>Album</h1> {images.map((img, i) => ( <img key={i} src={img.src} width={img.width} height={img.height} loading="eager" /> ))} </> ) }
呼び出し側
'use client' import HeavyMobilityshowAlbum from '../../components/heavy_mobility_show_album' export default async function UseServerMobilityAlbum() { return ( <> <HeavyMobilityshowAlbum /> <HeavyMobilityshowAlbum /> <HeavyMobilityshowAlbum /> <HeavyMobilityshowAlbum /> <HeavyMobilityshowAlbum /> </> ) }
このコードをuse client指定あるなしで実行してGoogleのPageSpeed Insightsでパフォーマンス計測しましたが結果ほぼ変わらず(ブラウザのキャッシュ削除しても同じ
なぜ変わらなかったのか
よくよく考えると計算量は大したことないしimgタグのsrcとしてVercelのサーバパスを指定してそれぞれの画像ごとに1つずつクライアント側でgetリクエスト送ってるだけでサーバコンポーネントだろうがネットワークのパフォーマンスも同じなはず。というので差分が出る要素なさそうです。
ブラウザの開発ツールでネットワークタブ開いた結果
まとめ
- スクリプトの計算量が多い場合Server Componentにした方がいい。。というか今回の件だとそもそもbuildできない
- なんでもserver componentにすればパフォーマンスがよくなるわけではない。画像表示とか一見重そうだが計算量は対して使ってないし必要なネットワーク量も実は差がないコンポーネントでは差が出ない
終わり