Skip to main content

⚡ Next.js Commerce パフォーマンス最適化

Cloudflare Workers制約対応・実用的最適化戦略
3MB制限下でのBundle Size管理とEdge最適化

🎯 最適化戦略概要

制約条件・目標値

Cloudflare Workers制約:
Bundle Size: 3MB (無料) / 10MB (有料)
CPU Time: 50ms (CPU制限)
Memory: 128MB

パフォーマンス目標:
Lighthouse Score: 95+ (全項目)
Core Web Vitals:
- LCP: <2.5s
- FID: <100ms
- CLS: <0.1
Bundle Size: <2.5MB (余裕確保)

📦 Bundle Size最適化

Tree Shaking・Dynamic Import戦略

// next.config.js - 最適化設定
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
optimizePackageImports: [
'@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu',
'lucide-react'
]
},

// Bundle分析
webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
// Bundle サイズ分析
config.optimization.concatenateModules = true

// 不要コード削除
config.optimization.usedExports = true
config.optimization.sideEffects = false
}
return config
},

// Cloudflare対応
trailingSlash: true,
images: {
unoptimized: true // R2 + Image Resizing使用のため
}
}

コンポーネント遅延読み込み

// components/LazyComponents.ts - 戦略的遅延読み込み
import dynamic from 'next/dynamic'

// 高優先度: 初期表示で必要
export const ProductCard = dynamic(() => import('./ProductCard'), {
ssr: true // SSR対応
})

// 中優先度: スクロール後に表示
export const ProductGallery = dynamic(() => import('./ProductGallery'), {
ssr: false,
loading: () => <div className="animate-pulse bg-gray-200 aspect-square rounded-lg" />
})

// 低優先度: ユーザーアクション後
export const Cart = dynamic(() => import('./Cart'), {
ssr: false,
loading: () => <CartSkeleton />
})

export const CheckoutForm = dynamic(() => import('./CheckoutForm'), {
ssr: false
})

// モーダル系: 使用時のみ読み込み
export const AuthModal = dynamic(() => import('./AuthModal'), {
ssr: false
})

export const ImageZoom = dynamic(() => import('./ImageZoom'), {
ssr: false
})

Bundle分析・監視

// scripts/bundle-analysis.js - サイズ監視スクリプト
const fs = require('fs')
const { execSync } = require('child_process')

async function analyzeBundleSize() {
// Next.js build
execSync('npm run build')

// .next/static分析
const staticDir = '.next/static'
const chunks = fs.readdirSync(`${staticDir}/chunks`)

let totalSize = 0
const chunkSizes = []

chunks.forEach(chunk => {
const stats = fs.statSync(`${staticDir}/chunks/${chunk}`)
const sizeKB = Math.round(stats.size / 1024)
totalSize += sizeKB
chunkSizes.push({ file: chunk, size: sizeKB })
})

console.log(`\n📦 Bundle Size Analysis`)
console.log(`Total Size: ${totalSize}KB (${(totalSize/1024).toFixed(1)}MB)`)
console.log(`Cloudflare Limit: 3072KB (3MB)`)
console.log(`Remaining: ${3072 - totalSize}KB\n`)

// 大きいチャンク警告
const largeChunks = chunkSizes
.filter(chunk => chunk.size > 500)
.sort((a, b) => b.size - a.size)

if (largeChunks.length > 0) {
console.log(`🚨 Large Chunks (>500KB):`)
largeChunks.forEach(chunk => {
console.log(` ${chunk.file}: ${chunk.size}KB`)
})
}

// 制限超過チェック
if (totalSize > 3072) {
console.error(`❌ Bundle size exceeds Cloudflare Workers limit!`)
process.exit(1)
} else if (totalSize > 2500) {
console.warn(`⚠️ Bundle size approaching limit (${totalSize}/3072 KB)`)
} else {
console.log(`✅ Bundle size within limits`)
}
}

analyzeBundleSize()

🖼️ 画像最適化戦略

R2 + Cloudflare Image Resizing統合

// lib/image-optimization.ts - 画像最適化ユーティリティ
interface ImageOptions {
width?: number
height?: number
quality?: number
format?: 'webp' | 'avif' | 'jpeg'
fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad'
}

export function optimizeImageUrl(
originalUrl: string,
options: ImageOptions = {}
): string {
const {
width,
height,
quality = 85,
format = 'webp',
fit = 'scale-down'
} = options

// R2 URLかどうか判定
if (!originalUrl.includes('contents-print-images')) {
return originalUrl
}

// Cloudflare Image Resizing パラメータ構築
const params = new URLSearchParams()
if (width) params.set('width', width.toString())
if (height) params.set('height', height.toString())
params.set('quality', quality.toString())
params.set('format', format)
params.set('fit', fit)

// 変換URL生成
const baseUrl = originalUrl.replace(
'contents-print-images.r2.dev',
'cdn.contents-print.jp' // カスタムドメイン
)

return `${baseUrl}?${params.toString()}`
}

// React コンポーネント用フック
export function useOptimizedImage(src: string, options: ImageOptions) {
return useMemo(() => ({
src: optimizeImageUrl(src, options),
// WebP非対応ブラウザ用フォールバック
fallbackSrc: optimizeImageUrl(src, { ...options, format: 'jpeg' })
}), [src, options])
}

Next.js Image最適化統合

// components/OptimizedImage.tsx - 最適化画像コンポーネント
import Image from 'next/image'
import { useState } from 'react'
import { useOptimizedImage } from '@/lib/image-optimization'

interface OptimizedImageProps {
src: string
alt: string
width: number
height: number
priority?: boolean
quality?: number
className?: string
}

export function OptimizedImage({
src,
alt,
width,
height,
priority = false,
quality = 85,
className
}: OptimizedImageProps) {
const [imageError, setImageError] = useState(false)

const { src: optimizedSrc, fallbackSrc } = useOptimizedImage(src, {
width,
height,
quality,
format: 'webp'
})

return (
<Image
src={imageError ? fallbackSrc : optimizedSrc}
alt={alt}
width={width}
height={height}
priority={priority}
quality={quality}
className={className}
onError={() => setImageError(true)}
// Blur placeholder for R2 images
placeholder={src.includes('contents-print') ? 'blur' : 'empty'}
blurDataURL={`data:image/svg+xml;base64,${generateBlurDataURL()}`}
/>
)
}

// Blur placeholder生成
function generateBlurDataURL(): string {
const svg = `
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f3f4f6"/>
</svg>
`
return Buffer.from(svg).toString('base64')
}

⚡ レンダリング最適化

React Server Components活用

// app/products/[id]/page.tsx - RSC最適化
import { Suspense } from 'react'
import { ProductGallery } from '@/components/ProductGallery'
import { ProductInfo } from '@/components/ProductInfo'
import { RelatedProducts } from '@/components/RelatedProducts'

// RSC - サーバーサイドでデータ取得
async function getProduct(id: string) {
// BFF API呼び出し(サーバーサイド)
const response = await fetch(`${process.env.BFF_API_URL}/products/${id}`, {
cache: 'force-cache',
next: { revalidate: 3600 } // 1時間キャッシュ
})
return response.json()
}

// Static Generation対応
export async function generateStaticParams() {
// 人気商品のみ事前生成
const products = await fetch(`${process.env.BFF_API_URL}/products/popular`)
.then(res => res.json())

return products.map(product => ({
id: product.id
}))
}

export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id)

return (
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* 画像ギャラリー */}
<Suspense fallback={<GallerySkeleton />}>
<ProductGallery images={product.images} />
</Suspense>

{/* 商品情報 */}
<ProductInfo product={product} />
</div>

{/* 関連商品 - 遅延読み込み */}
<Suspense fallback={<RelatedProductsSkeleton />}>
<RelatedProducts productId={product.id} brand={product.productType} />
</Suspense>
</div>
)
}

データフェッチ最適化

// lib/data-fetching.ts - データ取得最適化
interface CacheOptions {
revalidate?: number
tags?: string[]
}

export class OptimizedDataFetcher {
private baseUrl: string

constructor(baseUrl: string) {
this.baseUrl = baseUrl
}

// 商品一覧 - ISR対応
async getProducts(params: {
brand?: 'neko' | 'tokinoe'
category?: string
limit?: number
}, cacheOptions: CacheOptions = {}) {
const searchParams = new URLSearchParams(params as any)
const url = `${this.baseUrl}/products?${searchParams}`

return fetch(url, {
next: {
revalidate: cacheOptions.revalidate || 1800, // 30分
tags: cacheOptions.tags || ['products']
}
}).then(res => res.json())
}

// 商品詳細 - 長期キャッシュ
async getProduct(id: string) {
return fetch(`${this.baseUrl}/products/${id}`, {
next: {
revalidate: 3600, // 1時間
tags: [`product-${id}`]
}
}).then(res => res.json())
}

// リアルタイムデータ(在庫など)
async getProductAvailability(id: string) {
return fetch(`${this.baseUrl}/products/${id}/availability`, {
cache: 'no-store' // 常に最新データ
}).then(res => res.json())
}
}

// SWR統合(クライアントサイド)
export function useProductSWR(id: string) {
return useSWR(
`/api/products/${id}`,
async (url) => {
const token = await getFirebaseToken()
return fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
}).then(res => res.json())
},
{
revalidateOnFocus: false,
dedupingInterval: 300000, // 5分間重複リクエスト防止
staleTime: 600000 // 10分間データ有効
}
)
}

🚀 Edge最適化・CDN活用

Cloudflare Edge最適化

// middleware.ts - Edge処理最適化
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
const response = NextResponse.next()

// Edge キャッシュ設定
if (request.nextUrl.pathname.startsWith('/products')) {
response.headers.set(
'Cache-Control',
'public, s-maxage=1800, stale-while-revalidate=3600'
)
}

// 画像最適化ヘッダー
if (request.nextUrl.pathname.includes('/images')) {
response.headers.set('Cache-Control', 'public, max-age=31536000') // 1年
}

// CDN最適化
response.headers.set('CF-Cache-Level', 'aggressive')
response.headers.set('CF-Edge-Cache-TTL', '1800')

return response
}

export const config = {
matcher: ['/products/:path*', '/images/:path*']
}

静的アセット最適化

// next.config.js - アセット最適化
const nextConfig = {
// 静的アセット最適化
assetPrefix: process.env.NODE_ENV === 'production'
? 'https://cdn.contents-print.jp'
: '',

// 圧縮設定
compress: true,

// 最適化設定
swcMinify: true,

// 実験的機能
experimental: {
// Edge Runtime使用
runtime: 'edge',

// 並列チャンク読み込み
workerThreads: false,
esmExternals: true
},

// Headers最適化
async headers() {
return [
{
source: '/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable'
}
]
},
{
source: '/api/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, s-maxage=60, stale-while-revalidate=300'
}
]
}
]
}
}

📊 パフォーマンス監視・測定

Core Web Vitals監視

// lib/performance-monitoring.ts - パフォーマンス測定
export function reportWebVitals(metric: any) {
// 基本メトリクス記録
const { id, name, value } = metric

console.log(`📊 ${name}: ${value}`)

// 閾値チェック・アラート
const thresholds = {
FCP: 2500, // First Contentful Paint
LCP: 2500, // Largest Contentful Paint
FID: 100, // First Input Delay
CLS: 0.1, // Cumulative Layout Shift
TTFB: 600 // Time to First Byte
}

if (value > thresholds[name]) {
console.warn(`⚠️ ${name} exceeds threshold: ${value} > ${thresholds[name]}`)

// 本番環境では外部監視サービスに送信
if (process.env.NODE_ENV === 'production') {
sendToAnalytics({
metric: name,
value,
threshold: thresholds[name],
url: window.location.href,
timestamp: Date.now()
})
}
}
}

// カスタムパフォーマンス測定
export class PerformanceTracker {
private marks: Map<string, number> = new Map()

mark(name: string) {
this.marks.set(name, performance.now())
}

measure(name: string, startMark: string) {
const startTime = this.marks.get(startMark)
if (!startTime) return

const duration = performance.now() - startTime
console.log(`⏱️ ${name}: ${duration.toFixed(2)}ms`)

return duration
}

// API レスポンス時間測定
async trackAPICall<T>(name: string, apiCall: () => Promise<T>): Promise<T> {
this.mark(`${name}_start`)
try {
const result = await apiCall()
this.measure(`${name}_success`, `${name}_start`)
return result
} catch (error) {
this.measure(`${name}_error`, `${name}_start`)
throw error
}
}
}

Lighthouse CI統合

# .github/workflows/lighthouse.yml - 自動パフォーマンステスト
name: Lighthouse CI
on: [push, pull_request]

jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'

- name: Install dependencies
run: npm ci

- name: Build project
run: npm run build

- name: Serve project
run: npm run start &

- name: Wait for server
run: sleep 10

- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@v9
with:
configPath: './lighthouserc.js'
uploadArtifacts: true
temporaryPublicStorage: true
// lighthouserc.js - Lighthouse設定
module.exports = {
ci: {
collect: {
url: [
'http://localhost:3000',
'http://localhost:3000/products/sample-neko',
'http://localhost:3000/products/sample-tokinoe'
],
startServerCommand: 'npm run start'
},
assert: {
assertions: {
'categories:performance': ['warn', { minScore: 0.9 }],
'categories:accessibility': ['error', { minScore: 0.9 }],
'categories:best-practices': ['warn', { minScore: 0.9 }],
'categories:seo': ['warn', { minScore: 0.9 }]
}
},
upload: {
target: 'temporary-public-storage'
}
}
}

⚡ 実装チェックリスト

Bundle Size最適化

✅ 実装項目:
- [ ] Dynamic Import戦略実装
- [ ] Tree Shaking設定
- [ ] Bundle分析スクリプト
- [ ] サイズ監視CI設定
- [ ] 不要依存関係削除

⚠️ 監視指標:
- Total Bundle: <2.5MB
- Largest Chunk: <800KB
- 初期読み込み: <1MB
- Gzip効果: 70%+

画像最適化

✅ 実装項目:
- [ ] R2 + Image Resizing統合
- [ ] WebP/AVIF対応
- [ ] Lazy Loading実装
- [ ] Blur Placeholder設定
- [ ] CDN最適化

⚠️ 監視指標:
- LCP (画像): <2.5s
- 画像圧縮率: 75%+
- CDN Hit率: 95%+
- 帯域節約: 50%+

レンダリング最適化

✅ 実装項目:
- [ ] RSC活用
- [ ] ISR設定
- [ ] Suspense境界
- [ ] 適切なキャッシュ戦略
- [ ] Edge処理最適化

⚠️ 監視指標:
- FCP: <2.5s
- TTI: <3.5s
- CLS: <0.1
- FID: <100ms

📍 最適化効果予測

期待される改善

  • Lighthouse Score: 85 → 95+ (全項目)
  • 読み込み速度: 30-40%向上
  • Bundle Size: 40-50%削減
  • CDN使用率: 95%以上

ROI分析

  • 初期投資: 最適化作業2-3週間
  • 継続効果: ユーザー体験向上 + 離脱率削減
  • コスト削減: CDN・帯域費用20-30%削減
  • 開発効率: ビルド時間短縮 + デバッグ容易化

📍 関連ドキュメント: 統合戦略 | コンポーネント設計 | 技術制約

文書作成日: 2025-08-23
最終更新: 2025-08-24
バージョン: 1.0 - 最適化戦略版