# Next.js Pages Router における getLayout パターン完全ガイド ## getLayout パターンの基本概念と仕組み getLayout パターンは、Next.js Pages Router における**ページごとのレイアウト定義を可能にする強力なアーキテクチャパターン**です。このパターンを使用することで、各ページが独自のレイアウト階層を静的な `getLayout` 関数を通じて定義できます。 ### 技術的な仕組み getLayout パターンは React のコンポーネントツリー構成を活用して動作します: ```typescript // pages/dashboard.tsx import DashboardLayout from '../components/DashboardLayout' const Dashboard = () =>
ダッシュボードコンテンツ
Dashboard.getLayout = function getLayout(page) { return {page} } export default Dashboard // pages/_app.tsx export default function MyApp({ Component, pageProps }) { const getLayout = Component.getLayout ?? ((page) => page) return getLayout() } ``` **動作原理:** 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 での型安全な実装 ```typescript // types/layout.ts import type { NextPage } from 'next' import type { AppProps } from 'next/app' import type { ReactElement, ReactNode } from 'react' export type NextPageWithLayout

= NextPage & { 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() } ``` ### ネストレイアウトの実装 ```typescript // 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

プロフィールコンテンツ
} ProfilePage.getLayout = nestLayout(getBaseLayout, getDashboardLayout) ``` ### 状態管理の最適化 ```typescript // レイアウトごとのコンテキスト分割 const AuthLayout = ({ children }) => ( {children} ) const PublicLayout = ({ children }) => ( {children} ) // 各ページで適切なレイアウトを選択 Page.getLayout = (page) => {page} ``` ## バッドプラクティスと実装時の落とし穴 ### 避けるべきアンチパターン **❌ レイアウトの再作成** ```typescript // 悪い例:レイアウトの永続性が失われる const BadPage = () => { return (
ページコンテンツ
) } // ✅ 良い例:getLayout パターンを使用 const GoodPage = () =>
ページコンテンツ
GoodPage.getLayout = (page) => {page} ``` **❌ _app.tsx での条件付きレンダリング** ```typescript // 悪い例:レイアウトの再マウントを引き起こす function MyApp({ Component, pageProps, router }) { if (router.pathname.startsWith('/dashboard')) { return } return } ``` ### メモリリークの防止 ```typescript // ✅ 適切なクリーンアップ const Layout = ({ children }) => { useEffect(() => { const handleResize = () => { /* 処理 */ } window.addEventListener('resize', handleResize) return () => { window.removeEventListener('resize', handleResize) } }, []) return
{children}
} ``` ## 他のレイアウト管理手法との比較 ### Pages Router 内での比較 | 手法 | 複雑度 | パフォーマンス | 柔軟性 | 学習曲線 | |------|--------|----------------|--------|----------| | getLayout | 中 | 高 | 高 | 中 | | HOCs | 高 | 中 | 高 | 高 | | _app.js ルーティング | 低 | 高 | 低 | 低 | | Context ベース | 高 | 中 | 高 | 高 | | ラッパーコンポーネント | 低 | 低 | 低 | 低 | ### Next.js 13+ App Router との比較 **App Router の利点:** - ビルトインのレイアウトネスティング - ファイルシステムベースの直感的な構造 - 自動的な状態永続化 - `loading.js` と `error.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 との統合 ```typescript // SSR との完全な互換性 export async function getServerSideProps() { const data = await fetchData() return { props: { data } } } function Page({ data }) { return
{data.content}
} Page.getLayout = (page) => {page} ``` ## 実際のプロジェクトでの活用例 ### 企業での実装事例 **Netflix:** - ログアウト済みホームページで Time-to-Interactive を 50% 削減 - 戦略的なプリフェッチで後続ページロードを 30% 改善 **Hulu:** - Next.js による統一されたフロントエンドアーキテクチャ - CSS-in-JS の自動コード分割を実装 **Sonos:** - ビルド時間を **75% 短縮** - パフォーマンススコアを **10% 改善** ## パフォーマンス測定と最適化 ### 測定ツールの設定 ```javascript // 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 の活用 ```javascript import { Profiler } from 'react'; function onRenderCallback(id, phase, actualDuration, baseDuration) { console.log({ id, phase, actualDuration, baseDuration }); } {children} ``` ### 最適化テクニック **メモ化の実装:** ```typescript 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 (
{children}
); }); ``` **動的インポートによるコード分割:** ```typescript import dynamic from 'next/dynamic'; const DynamicSidebar = dynamic(() => import('../components/Sidebar'), { loading: () => , ssr: false }); const Layout = ({ children }) => (
{children}
); ``` ### パフォーマンスバジェットの実装 ```javascript 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 を実装することで、アプリケーションのスケールに応じて最大限のパフォーマンス利点とアーキテクチャの柔軟性を維持できます。