⚡ 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 - 最適化戦略版