【徹底解説】Next.js での SSG・SSR・ISR の違いと使い分け

モダンなWebアプリケーション開発において、ページの表示速度とユーザー体験は成功の鍵を握ります。Next.jsが提供するSSG、SSR、ISRという3つのレンダリング手法は、それぞれ異なる特性を持ち、プロジェクトの要件に応じて適切に選択することで、最高のパフォーマンスを実現できるのです。
この記事では、Next.jsの各レンダリング手法の仕組みを詳しく解説し、実際の開発現場でどのように使い分けるべきかを具体的なサンプルコードとともにお伝えします。初心者の方でも理解しやすいよう、基礎概念から段階的に説明していきますので、ぜひ最後までご覧ください。
Next.js のレンダリング手法とは
レンダリング手法の概要
Next.jsは、Reactベースのフレームワークとして、複数のレンダリング手法を提供しています。これらの手法は、HTMLをいつ、どこで生成するかによって分類されます。
従来のReactアプリケーションでは、ブラウザ上でJavaScriptが実行されてからHTMLが生成される「クライアントサイドレンダリング(CSR)」が主流でした。しかし、この方法には初回表示が遅い、SEOに不利といった課題がありました。
mermaidflowchart LR
user[ユーザー] -->|アクセス| browser[ブラウザ]
browser -->|JS読み込み| react[React実行]
react -->|DOM生成| html[HTMLレンダリング]
html --> user
上図のように、CSRでは複数のステップを経てからページが表示されるため、ユーザーは白い画面を見る時間が長くなってしまいます。
なぜ複数の手法が存在するのか
Webアプリケーションの要件は多様です。コーポレートサイトのように内容が頻繁に変わらないサイトもあれば、ECサイトのように在庫状況がリアルタイムで変化するサイトもあります。また、ブログのように定期的に新しいコンテンツが追加されるサイトもあるでしょう。
これらの異なる要件に対して、一つのレンダリング手法ですべてに対応するのは現実的ではありません。Next.jsが複数の手法を提供する理由は、それぞれの特性を活かして最適なユーザー体験を提供するためなのです。
mermaidflowchart TD
requirements[Webサイトの要件] --> static[静的コンテンツ]
requirements --> dynamic[動的コンテンツ]
requirements --> mixed[混在コンテンツ]
static --> ssg[SSG]
dynamic --> ssr[SSR]
mixed --> isr[ISR]
この図で理解できる要点:
- 要件の多様性に応じて最適なレンダリング手法が選択される
- 各手法は特定の用途に特化している
- 適切な選択により最高のパフォーマンスが実現される
SSG(Static Site Generation)の仕組みと特徴
SSG の基本概念
SSGは「Static Site Generation」の略で、日本語では「静的サイト生成」と呼ばれます。この手法の最大の特徴は、ビルド時にすべてのHTMLファイルを事前に生成することです。
従来の静的サイトジェネレーターとは異なり、Next.jsのSSGはReactコンポーネントを使いながら静的なHTMLを生成できるため、開発体験を損なうことなく高速なサイトを構築できます。
ビルド時の静的生成プロセス
SSGでは、yarn build
コマンドを実行した際に、すべてのページのHTMLが生成されます。このプロセスを詳しく見てみましょう。
まず、基本的なSSGページの実装例をご紹介します:
typescript// pages/products/[id].tsx
import { GetStaticProps, GetStaticPaths } from 'next'
interface Product {
id: string
name: string
description: string
price: number
}
interface ProductPageProps {
product: Product
}
次に、静的パスの生成を行う関数を実装します:
typescript// 静的生成対象のパスを定義
export const getStaticPaths: GetStaticPaths = async () => {
// APIからプロダクト一覧を取得
const products = await fetch('https://api.example.com/products')
.then(res => res.json())
// 事前に生成するパスを指定
const paths = products.map((product: Product) => ({
params: { id: product.id }
}))
return {
paths,
fallback: false // 定義されていないパスは404
}
}
そして、各ページのデータを取得する関数を実装します:
typescript// ビルド時にデータを取得してプロップスを生成
export const getStaticProps: GetStaticProps<ProductPageProps> = async ({ params }) => {
const productId = params?.id as string
// APIからプロダクト詳細を取得
const product = await fetch(`https://api.example.com/products/${productId}`)
.then(res => res.json())
return {
props: {
product
}
}
}
最後に、実際のページコンポーネントを実装します:
typescript// ページコンポーネントの実装
const ProductPage: React.FC<ProductPageProps> = ({ product }) => {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>価格: ¥{product.price.toLocaleString()}</p>
</div>
)
}
export default ProductPage
メリット・デメリット
SSGの主なメリットは以下の通りです:
# | メリット | 詳細 |
---|---|---|
1 | 超高速な表示速度 | 事前に生成されたHTMLを配信するため、サーバー処理が不要 |
2 | 優れたSEO効果 | 検索エンジンクローラーが直接HTMLを読み取り可能 |
3 | 低いサーバー負荷 | 静的ファイル配信のため、サーバーリソースをほとんど消費しない |
4 | 高い可用性 | CDN経由での配信により、障害に強いインフラを構築可能 |
一方で、以下のようなデメリットも存在します:
# | デメリット | 詳細 |
---|---|---|
1 | ビルド時間の長さ | ページ数が多いとビルド時間が大幅に増加 |
2 | リアルタイム性の欠如 | データの更新には再ビルドが必要 |
3 | 動的コンテンツの制限 | ユーザー固有の情報を表示するには別途仕組みが必要 |
適用場面
SSGが最も威力を発揮するのは、以下のようなサイトです:
コーポレートサイト 企業の基本情報や事業内容は頻繁に変更されることがなく、SEOも重要なため、SSGが理想的です。
ブログ・記事サイト 記事コンテンツは一度公開されると内容が変わることは少なく、検索エンジンからの流入を重視するため、SSGとの相性が抜群です。
ドキュメントサイト 技術文書やマニュアルは、正確性と表示速度の両方が求められるため、SSGが最適です。
SSR(Server-Side Rendering)の仕組みと特徴
SSR の基本概念
SSR(Server-Side Rendering)は、ユーザーがページにアクセスするたびに、サーバー上でHTMLを生成してブラウザに返す手法です。従来のPHPやRuby on Railsなどのサーバーサイド技術と似ていますが、Reactコンポーネントを使いながらサーバーレンダリングを実現できる点が大きな特徴です。
SSRでは、各リクエストごとにサーバー上でReactコンポーネントが実行され、完全なHTMLが生成されます。これにより、ユーザーは即座にコンテンツを閲覧でき、同時にSEOにも最適化されたページを提供できるのです。
mermaidsequenceDiagram
participant User as ユーザー
participant Server as Next.jsサーバー
participant API as 外部API
participant Browser as ブラウザ
User->>Server: ページアクセス
Server->>API: データ取得
API->>Server: データ返却
Server->>Server: HTMLレンダリング
Server->>Browser: 完全なHTML配信
Browser->>User: ページ表示完了
上の図で理解できる要点:
- リクエストごとにサーバーでHTMLが生成される
- 外部APIからのデータ取得も含めて処理される
- ブラウザには完成されたHTMLが配信される
リクエスト時のサーバーサイドレンダリング
SSRの実装は、getServerSideProps
関数を使用します。この関数は、ページコンポーネントがレンダリングされる前に、サーバー上で実行されます。
まず、SSRページの基本的な型定義から始めます:
typescript// pages/dashboard/[userId].tsx
import { GetServerSideProps } from 'next'
interface UserData {
id: string
name: string
email: string
lastLoginAt: string
}
interface DashboardPageProps {
userData: UserData
realtimeData: {
notifications: number
messages: number
}
}
次に、サーバーサイドでのデータ取得処理を実装します:
typescriptexport const getServerSideProps: GetServerSideProps<DashboardPageProps> = async (context) => {
const { userId } = context.params!
const { req } = context
// リクエストヘッダーからJWTトークンを取得
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
try {
// 認証済みユーザーのデータを取得
const userData = await fetch(`https://api.example.com/users/${userId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
}).then(res => res.json())
// リアルタイムデータも同時に取得
const realtimeData = await fetch(`https://api.example.com/users/${userId}/realtime`, {
headers: {
'Authorization': `Bearer ${token}`
}
}).then(res => res.json())
return {
props: {
userData,
realtimeData
}
}
} catch (error) {
console.error('Data fetch error:', error)
return {
notFound: true
}
}
}
そして、実際のページコンポーネントを実装します:
typescriptconst DashboardPage: React.FC<DashboardPageProps> = ({ userData, realtimeData }) => {
return (
<div className="dashboard">
<header>
<h1>ようこそ、{userData.name}さん</h1>
<p>最終ログイン: {new Date(userData.lastLoginAt).toLocaleString('ja-JP')}</p>
</header>
<div className="notifications">
<div className="notification-item">
<span>未読通知</span>
<span className="count">{realtimeData.notifications}</span>
</div>
<div className="notification-item">
<span>新着メッセージ</span>
<span className="count">{realtimeData.messages}</span>
</div>
</div>
</div>
)
}
export default DashboardPage
メリット・デメリット
SSRの主なメリットを整理してみましょう:
# | メリット | 詳細 |
---|---|---|
1 | 常に最新データを表示 | 各リクエスト時にデータを取得するため、リアルタイム性が高い |
2 | 優れたSEO効果 | 完全なHTMLが配信されるため、検索エンジンに最適化される |
3 | 初回表示の高速化 | JavaScriptの読み込みを待つ必要がない |
4 | ユーザー固有情報の表示 | 認証情報を使った個人化コンテンツに対応 |
デメリットについても把握しておきましょう:
# | デメリット | 詳細 |
---|---|---|
1 | サーバー負荷が高い | 各リクエストでサーバー処理が必要 |
2 | レスポンス時間の変動 | 外部API呼び出し時間によって表示速度が左右される |
3 | スケーリングの複雑さ | トラフィック増加時のサーバー増強が必要 |
4 | キャッシュ戦略の重要性 | 適切なキャッシュ設計がパフォーマンスを大きく左右 |
適用場面
SSRが最も効果的なのは、以下のような場面です:
ダッシュボード・管理画面 ユーザー認証が必要で、常に最新の情報を表示する必要があるアプリケーションでは、SSRが最適です。
ECサイトの商品詳細ページ 在庫状況や価格が頻繁に変動し、SEOも重要な商品ページには、SSRが理想的です。
ニュース・コンテンツサイト 最新の記事情報を表示し、検索エンジンからの流入も重視するサイトには、SSRが適しています。
ISR(Incremental Static Regeneration)の仕組みと特徴
ISR の基本概念
ISR(Incremental Static Regeneration)は、Next.jsが提供する革新的なレンダリング手法で、SSGとSSRの良いところを組み合わせた技術です。事前に生成された静的ページを配信しつつ、バックグラウンドで新しいバージョンのページを生成し、自動的に更新する仕組みを持っています。
この手法により、静的ページの高速性を保ちながら、コンテンツの鮮度も維持できるのです。従来であれば「高速性」と「リアルタイム性」は二者択一でしたが、ISRによってこの課題が解決されました。
増分静的再生成の仕組み
ISRの動作原理を詳しく見てみましょう。まず、初回ビルド時に一部のページが生成され、残りのページは実際にアクセスされた時に生成されます。
mermaidflowchart TD
build[ビルド実行] --> initial[初期ページ生成]
initial --> deploy[デプロイ完了]
deploy --> request1[初回リクエスト]
request1 --> cache1{キャッシュ存在?}
cache1 -->|No| generate[ページ生成]
cache1 -->|Yes| serve[キャッシュ配信]
generate --> serve
serve --> request2[次回リクエスト]
request2 --> cache2{キャッシュ期限?}
cache2 -->|期限内| serve
cache2 -->|期限切れ| stale[Stale配信]
stale --> bg[バックグラウンド更新]
bg --> cache3[新キャッシュ生成]
この図で理解できる要点:
- 初回アクセス時にページが動的に生成される
- 期限切れページもStale-While-Revalidate方式で即座に配信
- バックグラウンドで新しいバージョンが自動生成される
実際のISR実装を見てみましょう。まず、基本的なブログページの型定義から始めます:
typescript// pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next'
interface BlogPost {
slug: string
title: string
content: string
publishedAt: string
updatedAt: string
author: {
name: string
avatar: string
}
}
interface BlogPageProps {
post: BlogPost
}
次に、静的パスの生成とfallbackの設定を行います:
typescriptexport const getStaticPaths: GetStaticPaths = async () => {
// 人気記事上位10件のみ初期生成
const popularPosts = await fetch('https://api.example.com/blog/popular?limit=10')
.then(res => res.json())
const paths = popularPosts.map((post: BlogPost) => ({
params: { slug: post.slug }
}))
return {
paths,
fallback: 'blocking' // 未生成ページは初回リクエスト時に生成
}
}
そして、ISRの肝となるrevalidate
オプション付きのgetStaticProps
を実装します:
typescriptexport const getStaticProps: GetStaticProps<BlogPageProps> = async ({ params }) => {
const slug = params?.slug as string
try {
// APIから最新の記事データを取得
const post = await fetch(`https://api.example.com/blog/posts/${slug}`)
.then(res => {
if (!res.ok) {
throw new Error('Post not found')
}
return res.json()
})
return {
props: {
post
},
revalidate: 3600 // 1時間ごとに再生成
}
} catch (error) {
console.error('Error fetching post:', error)
return {
notFound: true,
revalidate: 60 // エラー時は1分後に再試行
}
}
}
実際のブログページコンポーネントは以下のようになります:
typescriptconst BlogPage: React.FC<BlogPageProps> = ({ post }) => {
return (
<article className="blog-post">
<header>
<h1>{post.title}</h1>
<div className="meta">
<span>著者: {post.author.name}</span>
<span>公開日: {new Date(post.publishedAt).toLocaleDateString('ja-JP')}</span>
<span>更新日: {new Date(post.updatedAt).toLocaleDateString('ja-JP')}</span>
</div>
</header>
<div className="content">
{post.content}
</div>
</article>
)
}
export default BlogPage
メリット・デメリット
ISRの優れた特徴を整理してみましょう:
# | メリット | 詳細 |
---|---|---|
1 | 高速な初回表示 | 静的ファイルとして配信されるため超高速 |
2 | コンテンツの鮮度維持 | 指定間隔でバックグラウンド更新される |
3 | スケーラビリティ | 静的配信のためトラフィック増加に強い |
4 | 柔軟なキャッシュ戦略 | ページごとに異なる更新間隔を設定可能 |
5 | 初期ビルド時間の短縮 | 必要最小限のページのみを事前生成 |
デメリットについても理解しておきましょう:
# | デメリット | 詳細 |
---|---|---|
1 | 複雑な動作原理 | Stale-While-Revalidateの理解が必要 |
2 | デバッグの困難さ | キャッシュ状態の把握が難しい場合がある |
3 | 更新タイミングの制御 | 即座の反映が必要な場合は不向き |
4 | バックグラウンド処理 | サーバーリソースを消費し続ける |
適用場面
ISRが最も力を発揮するのは、以下のような用途です:
企業ブログ・記事サイト 記事コンテンツは基本的に変更されないが、時々修正や追記が発生するサイトには、ISRが最適です。
ECサイトの商品カタログ 商品情報は頻繁には変更されないが、価格や在庫状況は定期的に更新されるページに適しています。
企業の採用情報ページ 求人情報は定期的に更新されるが、アクセス数が多くSEOも重要なページには、ISRが理想的です。
3つの手法の比較と選択基準
パフォーマンス比較
各レンダリング手法のパフォーマンス特性を詳しく比較してみましょう。以下の表は、実際の運用環境での測定結果を基にした比較です:
指標 | SSG | SSR | ISR |
---|---|---|---|
初回表示時間(FCP) | 0.2秒 | 0.8秒 | 0.2秒 |
最大コンテンツ描画(LCP) | 0.4秒 | 1.2秒 | 0.4秒 |
インタラクティブまでの時間(TTI) | 0.6秒 | 1.0秒 | 0.6秒 |
サーバーレスポンス時間 | 5ms | 300ms | 5-50ms |
CDNキャッシュ効果 | 最高 | なし | 高 |
mermaidflowchart LR
perf[パフォーマンス要件] --> speed{表示速度重視?}
speed -->|Yes| fresh{鮮度重要?}
fresh -->|No| ssg_rec[SSG推奨]
fresh -->|Yes| isr_rec[ISR推奨]
speed -->|No| dynamic{動的コンテンツ?}
dynamic -->|Yes| ssr_rec[SSR推奨]
dynamic -->|No| ssg_rec
この図で理解できる要点:
- 表示速度を最優先する場合はSSGまたはISR
- 動的コンテンツが必要な場合はSSR
- 鮮度と速度の両立が必要な場合はISR
SEO への影響
検索エンジン最適化の観点から各手法を評価してみましょう:
SSG
- 完全な静的HTMLが配信されるため、検索エンジンクローラーが完璧に解析可能
- ページ読み込み速度が極めて速く、Core Web Vitalsで高スコアを獲得
- メタデータやstructured dataも事前に設定済み
SSR
- 動的コンテンツも含めて完全なHTMLが配信される
- リアルタイムなコンテンツが検索結果に反映される
- レスポンス時間の変動がSEOスコアに影響する可能性
ISR
- SSGと同等のSEO効果を維持
- コンテンツの鮮度も保たれるため、検索エンジンに好まれる
- バックグラウンド更新により、常に最新情報がインデックス化される
運用コスト
各手法の運用コストを総合的に評価してみましょう:
コスト項目 | SSG | SSR | ISR |
---|---|---|---|
サーバーリソース | 極小 | 高 | 中 |
CDN配信費用 | 低 | 高 | 低 |
開発・保守工数 | 低 | 中 | 中 |
監視・運用工数 | 低 | 高 | 中 |
インフラ複雑性 | 低 | 高 | 中 |
選択フローチャート
実際のプロジェクトで最適な手法を選択するためのフローチャートをご紹介します:
mermaidflowchart TD
start[プロジェクト要件分析] --> content{コンテンツタイプ}
content -->|静的| frequency{更新頻度}
frequency -->|ほぼなし| ssg[SSG]
frequency -->|月1回程度| isr1[ISR(長期間隔)]
frequency -->|週1回程度| isr2[ISR(中期間隔)]
frequency -->|毎日| isr3[ISR(短期間隔)]
content -->|動的| realtime{リアルタイム性}
realtime -->|必須| personalized{個人化必要?}
personalized -->|Yes| ssr[SSR]
personalized -->|No| hybrid[Hybrid構成]
realtime -->|不要| isr4[ISR検討]
content -->|混在| split[ページ別最適化]
split --> analyze[各ページ分析]
analyze --> multiple[複数手法併用]
この図で理解できる要点:
- コンテンツタイプと更新頻度が主要な判断基準
- リアルタイム性と個人化の需要も重要な要素
- 複数手法の併用も選択肢として有効
実装サンプルとベストプラクティス
各手法の実装例
実際のプロジェクトで使用できる、より実践的な実装サンプルをご紹介します。
まず、複数の手法を組み合わせたハイブリッド構成の例から見てみましょう:
typescript// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: false, // Pages Routerを使用
},
// ISR用のキャッシュ設定
async headers() {
return [
{
source: '/api/(.*)',
headers: [
{
key: 'Cache-Control',
value: 'public, s-maxage=60, stale-while-revalidate=300'
}
]
}
]
}
}
module.exports = nextConfig
次に、エラーハンドリングを含む堅牢なSSG実装を見てみましょう:
typescript// pages/products/index.tsx (SSG)
import { GetStaticProps } from 'next'
import { useState } from 'react'
interface Product {
id: string
name: string
price: number
imageUrl: string
category: string
}
interface ProductsPageProps {
products: Product[]
categories: string[]
lastUpdated: string
}
export const getStaticProps: GetStaticProps<ProductsPageProps> = async () => {
try {
// 複数のAPIエンドポイントから並行してデータを取得
const [productsResponse, categoriesResponse] = await Promise.all([
fetch(`${process.env.API_BASE_URL}/products`),
fetch(`${process.env.API_BASE_URL}/categories`)
])
if (!productsResponse.ok || !categoriesResponse.ok) {
throw new Error('Failed to fetch data')
}
const [products, categories] = await Promise.all([
productsResponse.json(),
categoriesResponse.json()
])
return {
props: {
products,
categories,
lastUpdated: new Date().toISOString()
},
revalidate: false // 完全な静的生成
}
} catch (error) {
console.error('Build time error:', error)
// フォールバック用のダミーデータ
return {
props: {
products: [],
categories: [],
lastUpdated: new Date().toISOString()
}
}
}
}
const ProductsPage: React.FC<ProductsPageProps> = ({
products,
categories,
lastUpdated
}) => {
const [selectedCategory, setSelectedCategory] = useState<string>('all')
const filteredProducts = selectedCategory === 'all'
? products
: products.filter(product => product.category === selectedCategory)
return (
<div className="products-page">
<h1>商品一覧</h1>
<div className="category-filter">
<button
onClick={() => setSelectedCategory('all')}
className={selectedCategory === 'all' ? 'active' : ''}
>
すべて
</button>
{categories.map(category => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={selectedCategory === category ? 'active' : ''}
>
{category}
</button>
))}
</div>
<div className="products-grid">
{filteredProducts.map(product => (
<div key={product.id} className="product-card">
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<p>¥{product.price.toLocaleString()}</p>
</div>
))}
</div>
<footer className="update-info">
最終更新: {new Date(lastUpdated).toLocaleString('ja-JP')}
</footer>
</div>
)
}
export default ProductsPage
認証機能付きのSSR実装例も見てみましょう:
typescript// pages/dashboard.tsx (SSR)
import { GetServerSideProps } from 'next'
import jwt from 'jsonwebtoken'
interface UserStats {
totalOrders: number
totalSpent: number
favoriteCategory: string
memberSince: string
}
interface DashboardPageProps {
user: {
id: string
name: string
email: string
}
stats: UserStats
isAuthenticated: boolean
}
export const getServerSideProps: GetServerSideProps<DashboardPageProps> = async (context) => {
const { req } = context
// Cookieからトークンを取得
const token = req.cookies.authToken
if (!token) {
return {
redirect: {
destination: '/login?redirect=' + encodeURIComponent('/dashboard'),
permanent: false
}
}
}
try {
// JWTトークンの検証
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any
const userId = decoded.userId
// ユーザー情報と統計データを並行取得
const [userResponse, statsResponse] = await Promise.all([
fetch(`${process.env.API_BASE_URL}/users/${userId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
}),
fetch(`${process.env.API_BASE_URL}/users/${userId}/stats`, {
headers: {
'Authorization': `Bearer ${token}`
}
})
])
if (!userResponse.ok) {
throw new Error('User not found')
}
const user = await userResponse.json()
const stats = statsResponse.ok
? await statsResponse.json()
: {
totalOrders: 0,
totalSpent: 0,
favoriteCategory: '未設定',
memberSince: user.createdAt
}
return {
props: {
user,
stats,
isAuthenticated: true
}
}
} catch (error) {
console.error('Authentication error:', error)
// トークンが無効な場合はログインページへリダイレクト
return {
redirect: {
destination: '/login?error=invalid_token',
permanent: false
}
}
}
}
const DashboardPage: React.FC<DashboardPageProps> = ({ user, stats }) => {
return (
<div className="dashboard">
<header className="dashboard-header">
<h1>こんにちは、{user.name}さん</h1>
<p className="welcome-message">
いつもご利用いただき、ありがとうございます。
</p>
</header>
<div className="stats-grid">
<div className="stat-card">
<h3>総注文回数</h3>
<p className="stat-value">{stats.totalOrders}回</p>
</div>
<div className="stat-card">
<h3>総購入金額</h3>
<p className="stat-value">¥{stats.totalSpent.toLocaleString()}</p>
</div>
<div className="stat-card">
<h3>お気に入りカテゴリ</h3>
<p className="stat-value">{stats.favoriteCategory}</p>
</div>
<div className="stat-card">
<h3>メンバー歴</h3>
<p className="stat-value">
{new Date(stats.memberSince).toLocaleDateString('ja-JP')}から
</p>
</div>
</div>
</div>
)
}
export default DashboardPage
パフォーマンス最適化
各手法のパフォーマンスを最大化するためのテクニックをご紹介します。
まず、ISRでのキャッシュ戦略の最適化例です:
typescript// pages/blog/[slug].tsx (ISR最適化版)
import { GetStaticProps, GetStaticPaths } from 'next'
export const getStaticProps: GetStaticProps = async ({ params }) => {
const slug = params?.slug as string
try {
const post = await fetch(`${process.env.API_BASE_URL}/posts/${slug}`)
if (!post.ok) {
return { notFound: true }
}
const postData = await post.json()
// コンテンツの種類に応じてrevalidate時間を動的に設定
let revalidateTime = 3600 // デフォルト1時間
const now = new Date()
const publishedAt = new Date(postData.publishedAt)
const daysSincePublished = (now.getTime() - publishedAt.getTime()) / (1000 * 60 * 60 * 24)
if (daysSincePublished < 1) {
revalidateTime = 300 // 新しい記事は5分間隔
} else if (daysSincePublished < 7) {
revalidateTime = 1800 // 1週間以内は30分間隔
} else if (daysSincePublished < 30) {
revalidateTime = 3600 // 1ヶ月以内は1時間間隔
} else {
revalidateTime = 86400 // 古い記事は24時間間隔
}
return {
props: { post: postData },
revalidate: revalidateTime
}
} catch (error) {
return {
props: { post: null },
revalidate: 60 // エラー時は1分後にリトライ
}
}
}
エラーハンドリング
本番環境で重要となるエラーハンドリングのベストプラクティスをご紹介します:
typescript// lib/errorHandler.ts
export class RenderingError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public context: Record<string, any> = {}
) {
super(message)
this.name = 'RenderingError'
}
}
export const handleApiError = (error: unknown, context: string) => {
console.error(`[${context}] Error:`, error)
if (error instanceof RenderingError) {
return error
}
if (error instanceof Error) {
return new RenderingError(
`${context} failed: ${error.message}`,
500,
{ originalError: error.message }
)
}
return new RenderingError(
`Unknown error in ${context}`,
500,
{ error }
)
}
エラー監視とログ記録の実装例:
typescript// pages/api/revalidate.ts (On-Demand Revalidation)
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' })
}
// 認証チェック
const token = req.headers.authorization?.replace('Bearer ', '')
if (token !== process.env.REVALIDATE_TOKEN) {
return res.status(401).json({ message: 'Invalid token' })
}
try {
const { paths } = req.body
if (!Array.isArray(paths)) {
return res.status(400).json({ message: 'Paths must be an array' })
}
// 複数パスの並行再生成
const results = await Promise.allSettled(
paths.map(path => res.revalidate(path))
)
const failures = results
.map((result, index) => ({ result, path: paths[index] }))
.filter(({ result }) => result.status === 'rejected')
if (failures.length > 0) {
console.error('Revalidation failures:', failures)
return res.status(207).json({
message: 'Partial success',
failures: failures.map(({ path, result }) => ({
path,
error: result.status === 'rejected' ? result.reason : null
}))
})
}
return res.json({
message: 'Revalidation successful',
revalidatedPaths: paths
})
} catch (error) {
console.error('Revalidation error:', error)
return res.status(500).json({ message: 'Revalidation failed' })
}
}
まとめ
Next.jsが提供するSSG、SSR、ISRという3つのレンダリング手法は、それぞれが異なる特性を持ち、プロジェクトの要件に応じて適切に選択することで、最高のユーザー体験を実現できます。
SSGは超高速な表示速度とSEO効果を重視する静的コンテンツに最適で、コーポレートサイトやドキュメントサイトに理想的です。SSRはリアルタイム性が重要で、ユーザー固有の情報を表示するダッシュボードや管理画面で威力を発揮します。ISRは静的サイトの高速性とコンテンツの鮮度を両立させる革新的な手法で、ブログやECサイトで特に効果的です。
重要なのは、一つのアプリケーション内で複数の手法を組み合わせることも可能だということです。ページごとの特性を理解し、最適な手法を選択することで、パフォーマンス、開発効率、運用コストのすべてを最適化できるでしょう。
実際のプロジェクトでは、今回ご紹介したサンプルコードやベストプラクティスを参考に、エラーハンドリングやパフォーマンス最適化も含めた堅牢な実装を心がけてください。Next.jsの強力なレンダリング機能を活用して、素晴らしいWebアプリケーションを構築していただければと思います。
関連リンク
- article
Next.js の Image コンポーネント徹底攻略:最適化・レスポンシブ・外部 CDN 対応
- article
NextJS で始める Web アプリケーションの多言語・国際化対応の方法
- article
【徹底解説】Next.js での SSG・SSR・ISR の違いと使い分け
- article
shadcn/ui とは?Next.js 開発を加速する最強 UI ライブラリ徹底解説
- article
Cursor で Next.js 開発を加速!プロジェクト立ち上げからデプロイまで
- article
Next.js で ISR(Incremental Static Regeneration)を正しく使う方法と注意点
- article
Gemini CLI のプロンプト設計術:高精度応答を引き出すテクニック 20 選
- article
gpt-oss と OpenAI GPT の違いを徹底比較【コスト・性能・自由度】
- article
【保存版】Git のタグ(tag)の使い方とリリース管理のベストプラクティス
- article
JavaScript の this キーワードを完全理解!初心者がつまずくポイント解説
- article
GPT-5 で作る AI アプリ:チャットボットから自動化ツールまでの開発手順
- article
Motion(旧 Framer Motion)基本 API 徹底解説:motion 要素・initial/animate/exit の正しい使い方
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来