nextjs-pages-router-getLayout-pattern.md 12 KB

Next.js Pages Router における getLayout パターン完全ガイド

getLayout パターンの基本概念と仕組み

getLayout パターンは、Next.js Pages Router におけるページごとのレイアウト定義を可能にする強力なアーキテクチャパターンです。このパターンを使用することで、各ページが独自のレイアウト階層を静的な getLayout 関数を通じて定義できます。

技術的な仕組み

getLayout パターンは React のコンポーネントツリー構成を活用して動作します:

// pages/dashboard.tsx
import DashboardLayout from '../components/DashboardLayout'

const Dashboard = () => <div>ダッシュボードコンテンツ</div>

Dashboard.getLayout = function getLayout(page) {
  return <DashboardLayout>{page}</DashboardLayout>
}

export default Dashboard

// pages/_app.tsx
export default function MyApp({ Component, pageProps }) {
  const getLayout = Component.getLayout ?? ((page) => page)
  return getLayout(<Component {...pageProps} />)
}

動作原理:

  1. Next.js がページを初期化する際、getLayout プロパティをチェック
  2. getLayout 関数がページコンポーネントを受け取り、完全なレイアウトツリーを返す
  3. React の差分アルゴリズムがコンポーネントツリーの同じ位置を維持し、効率的な差分更新を実現

パフォーマンス向上の具体的なメリット

レンダリング回数の削減

getLayout パターンの最大の利点は、ページ遷移時のレイアウトコンポーネントの再マウント防止です。React の差分アルゴリズムは、コンポーネントツリーの同じ位置に同じタイプのコンポーネントが存在する場合、そのインスタンスを再利用します。

実測データ(Zenn.dev の事例):

実装前:
├ /_app      97.7 kB (全ページで Recoil を含む)
├ /articles  98 kB
├ /profile   98 kB

実装後:
├ /_app      75 kB (22.7 kB 削減)
├ /articles  75.3 kB (最適化されたバンドル)
├ /profile   98.3 kB (必要な依存関係のみ)

メモリ効率の改善

主要な最適化ポイント:

  • 状態の永続化: 入力値、スクロール位置、コンポーネント状態がナビゲーション間で保持
  • イベントリスナーの永続性: イベントハンドラーの再アタッチ回避
  • DOM 参照の安定性: サードパーティ統合用の DOM ノード参照の維持

実装のベストプラクティス

TypeScript での型安全な実装

// types/layout.ts
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'
import type { ReactElement, ReactNode } from 'react'

export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
  getLayout?: (page: ReactElement) => ReactNode
}

export type AppPropsWithLayout = AppProps & {
  Component: NextPageWithLayout
}

// pages/_app.tsx
import type { AppPropsWithLayout } from '../types/layout'

export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
  const getLayout = Component.getLayout ?? ((page) => page)
  return getLayout(<Component {...pageProps} />)
}

ネストレイアウトの実装

// utils/nestLayout.ts
export function nestLayout(
  parentLayout: (page: ReactElement) => ReactNode,
  childLayout: (page: ReactElement) => ReactNode
) {
  return (page: ReactElement) => parentLayout(childLayout(page))
}

// pages/dashboard/profile.tsx
import { nestLayout } from '../../utils/nestLayout'
import { getLayout as getBaseLayout } from '../../components/BaseLayout'
import { getLayout as getDashboardLayout } from '../../components/DashboardLayout'

const ProfilePage: NextPageWithLayout = () => {
  return <div>プロフィールコンテンツ</div>
}

ProfilePage.getLayout = nestLayout(getBaseLayout, getDashboardLayout)

状態管理の最適化

// レイアウトごとのコンテキスト分割
const AuthLayout = ({ children }) => (
  <AuthProvider>
    <UserProvider>
      {children}
    </UserProvider>
  </AuthProvider>
)

const PublicLayout = ({ children }) => (
  <ThemeProvider>
    {children}
  </ThemeProvider>
)

// 各ページで適切なレイアウトを選択
Page.getLayout = (page) => <AuthLayout>{page}</AuthLayout>

バッドプラクティスと実装時の落とし穴

避けるべきアンチパターン

❌ レイアウトの再作成

// 悪い例:レイアウトの永続性が失われる
const BadPage = () => {
  return (
    <Layout>
      <div>ページコンテンツ</div>
    </Layout>
  )
}

// ✅ 良い例:getLayout パターンを使用
const GoodPage = () => <div>ページコンテンツ</div>
GoodPage.getLayout = (page) => <Layout>{page}</Layout>

❌ _app.tsx での条件付きレンダリング

// 悪い例:レイアウトの再マウントを引き起こす
function MyApp({ Component, pageProps, router }) {
  if (router.pathname.startsWith('/dashboard')) {
    return <DashboardLayout><Component {...pageProps} /></DashboardLayout>
  }
  return <Component {...pageProps} />
}

メモリリークの防止

// ✅ 適切なクリーンアップ
const Layout = ({ children }) => {
  useEffect(() => {
    const handleResize = () => { /* 処理 */ }
    
    window.addEventListener('resize', handleResize)
    
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  return <div>{children}</div>
}

他のレイアウト管理手法との比較

Pages Router 内での比較

手法 複雑度 パフォーマンス 柔軟性 学習曲線
getLayout
HOCs
_app.js ルーティング
Context ベース
ラッパーコンポーネント

Next.js 13+ App Router との比較

App Router の利点:

  • ビルトインのレイアウトネスティング
  • ファイルシステムベースの直感的な構造
  • 自動的な状態永続化
  • loading.jserror.js による組み込みの状態管理

getLayout パターンの利点:

  • 明示的なレイアウト制御
  • 成熟した安定したパターン
  • シンプルなメンタルモデル
  • 優れた TTFB パフォーマンス

パフォーマンス比較:

  • TTFB: Pages Router が App Router より最大 2 倍高速
  • 開発サーバー: Pages Router がより安定
  • バンドルサイズ: getLayout により選択的な読み込みが可能

SEO と SSR/SSG への影響

Core Web Vitals への影響

測定された改善効果:

  • LCP (Largest Contentful Paint): レイアウトの永続化により改善
  • INP (Interaction to Next Paint): JavaScript 実行時間の削減
  • CLS (Cumulative Layout Shift): レイアウトシフトの除去

Netflix の事例:

  • Time-to-Interactive が 50% 削減
  • JavaScript バンドルサイズが 200KB 削減
  • デスクトップユーザーの 97% が高速な First Input Delay を体験

SSR/SSG との統合

// SSR との完全な互換性
export async function getServerSideProps() {
  const data = await fetchData()
  return { props: { data } }
}

function Page({ data }) {
  return <div>{data.content}</div>
}

Page.getLayout = (page) => <Layout>{page}</Layout>

実際のプロジェクトでの活用例

企業での実装事例

Netflix:

  • ログアウト済みホームページで Time-to-Interactive を 50% 削減
  • 戦略的なプリフェッチで後続ページロードを 30% 改善

Hulu:

  • Next.js による統一されたフロントエンドアーキテクチャ
  • CSS-in-JS の自動コード分割を実装

Sonos:

  • ビルド時間を 75% 短縮
  • パフォーマンススコアを 10% 改善

パフォーマンス測定と最適化

測定ツールの設定

// next.config.js - Bundle Analyzer の設定
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer(nextConfig);

// 使用方法
// ANALYZE=true npm run build

React DevTools Profiler の活用

import { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration, baseDuration) {
  console.log({ id, phase, actualDuration, baseDuration });
}

<Profiler id="LayoutProfile" onRender={onRenderCallback}>
  <MyLayout>{children}</MyLayout>
</Profiler>

最適化テクニック

メモ化の実装:

import { memo, useMemo, useCallback } from 'react'

const Layout = memo(({ children, menuItems }) => {
  const processedMenu = useMemo(() => 
    menuItems.filter(item => item.visible).sort(), 
    [menuItems]
  );
  
  const handleNavigation = useCallback((path) => {
    router.push(path);
  }, [router]);
  
  return (
    <div>
      <Navigation items={processedMenu} onNavigate={handleNavigation} />
      {children}
    </div>
  );
});

動的インポートによるコード分割:

import dynamic from 'next/dynamic';

const DynamicSidebar = dynamic(() => import('../components/Sidebar'), {
  loading: () => <SidebarSkeleton />,
  ssr: false
});

const Layout = ({ children }) => (
  <div>
    <Header />
    <DynamicSidebar />
    <main>{children}</main>
  </div>
);

パフォーマンスバジェットの実装

export const PERFORMANCE_BUDGETS = {
  layoutRenderTime: 16, // 60fps のための 16ms
  memoryUsage: 50 * 1024 * 1024, // 50MB
  bundleSize: 200 * 1024, // 200KB
  firstContentfulPaint: 2000, // 2秒
};

const measureLayoutPerformance = (layoutName, renderFn) => {
  const start = performance.now();
  const result = renderFn();
  const duration = performance.now() - start;
  
  if (duration > PERFORMANCE_BUDGETS.layoutRenderTime) {
    console.warn(`Layout ${layoutName} がレンダーバジェットを超過: ${duration}ms`);
  }
  
  return result;
};

実装チェックリスト

初期設定

  • TypeScript の型定義を設定
  • _app.tsx に getLayout パターンを実装
  • React DevTools をインストール
  • Bundle Analyzer を設定

最適化の優先順位

高影響・低労力:

  • レイアウトコンポーネントに React.memo を実装
  • Bundle Analyzer で大きな依存関係を特定
  • Context Provider をレイアウトごとに分割

中影響・中労力:

  • 非クリティカルなレイアウトコンポーネントに動的インポートを実装
  • Suspense 境界を追加してストリーミングを改善
  • 自動パフォーマンス監視を設定

高影響・高労力:

  • 状態管理アーキテクチャの再設計
  • 包括的なプログレッシブエンハンスメントの実装
  • 高度なパフォーマンスバジェットシステムの作成

まとめ

getLayout パターンは、Next.js Pages Router において強力なパフォーマンス最適化とアーキテクチャの柔軟性を提供します。適切に実装すれば、以下の利点が得られます:

  1. パフォーマンスの向上: 不要な再レンダリングの削減とバンドルサイズの最適化
  2. ユーザー体験の向上: 状態の永続化とスムーズなページ遷移
  3. アーキテクチャの柔軟性: ページごとのレイアウトカスタマイズとパフォーマンスの維持
  4. メモリ効率: コンポーネントの再利用による最適なリソース使用

App Router が新しい代替手段を提供する一方で、getLayout パターンの理解は React のレンダリング最適化とコンポーネントライフサイクル管理への深い洞察を提供します。Pages Router アプリケーションでは、プロジェクトの開始時から getLayout を実装することで、アプリケーションのスケールに応じて最大限のパフォーマンス利点とアーキテクチャの柔軟性を維持できます。