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} />)
}
動作原理:
getLayout プロパティをチェックgetLayout 関数がページコンポーネントを受け取り、完全なレイアウトツリーを返す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 (必要な依存関係のみ)
主要な最適化ポイント:
// 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>
}
| 手法 | 複雑度 | パフォーマンス | 柔軟性 | 学習曲線 |
|---|---|---|---|---|
| getLayout | 中 | 高 | 高 | 中 |
| HOCs | 高 | 中 | 高 | 高 |
| _app.js ルーティング | 低 | 高 | 低 | 低 |
| Context ベース | 高 | 中 | 高 | 高 |
| ラッパーコンポーネント | 低 | 低 | 低 | 低 |
App Router の利点:
loading.js と error.js による組み込みの状態管理getLayout パターンの利点:
パフォーマンス比較:
測定された改善効果:
Netflix の事例:
// 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:
Hulu:
Sonos:
// 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
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;
};
_app.tsx に getLayout パターンを実装高影響・低労力:
中影響・中労力:
高影響・高労力:
getLayout パターンは、Next.js Pages Router において強力なパフォーマンス最適化とアーキテクチャの柔軟性を提供します。適切に実装すれば、以下の利点が得られます:
App Router が新しい代替手段を提供する一方で、getLayout パターンの理解は React のレンダリング最適化とコンポーネントライフサイクル管理への深い洞察を提供します。Pages Router アプリケーションでは、プロジェクトの開始時から getLayout を実装することで、アプリケーションのスケールに応じて最大限のパフォーマンス利点とアーキテクチャの柔軟性を維持できます。