Files
thrilltrack-explorer/migration/PHASE_13_NEXTJS_OPTIMIZATION.md

548 lines
12 KiB
Markdown

# PHASE 13: Next.js 15 Optimization
**Status:** ⬜ Not Started
**Estimated Time:** 15-20 hours
**Priority:** HIGH
**Depends On:** Phase 12 (Next.js Pages Migration)
**Blocks:** Phase 14 (Cleanup & Testing)
---
## 🎯 Phase Goal
Optimize the Next.js 15 application for production with Turbopack, implement advanced features, and ensure peak performance.
---
## 📋 Prerequisites
- [ ] Phase 12 complete (all pages migrated to Next.js App Router)
- [ ] All services converted from Supabase to Django API
- [ ] Sacred Pipeline working in Next.js
- [ ] Bun is configured as package manager
- [ ] Environment variables properly configured
---
## ✅ Task 13.1: Turbopack Configuration (3 hours)
### Configure next.config.js for Turbopack
Create/update `next.config.js`:
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
// Enable Turbopack for development
experimental: {
turbo: {
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
resolveAlias: {
'@': './src',
},
},
},
// Image optimization
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'imagedelivery.net', // CloudFlare Images
},
{
protocol: 'https',
hostname: process.env.NEXT_PUBLIC_DJANGO_API_URL?.replace('https://', '') || 'api.thrillwiki.com',
},
],
formats: ['image/avif', 'image/webp'],
},
// Environment variables
env: {
NEXT_PUBLIC_DJANGO_API_URL: process.env.NEXT_PUBLIC_DJANGO_API_URL,
},
// Production optimizations
compress: true,
poweredByHeader: false,
reactStrictMode: true,
// Output configuration
output: 'standalone', // For Docker deployment
};
module.exports = nextConfig;
```
### Checklist
- [ ] `next.config.js` created/updated
- [ ] Turbopack rules configured
- [ ] Image optimization configured for CloudFlare
- [ ] Environment variables properly referenced
- [ ] Production flags enabled
- [ ] Development server uses Turbopack
- [ ] Hot reload works correctly
---
## ✅ Task 13.2: Server vs Client Component Optimization (4 hours)
### Identify Component Types
**Server Components (Default):**
- Data fetching pages
- Static content
- SEO-critical pages
- No user interaction
**Client Components (Need 'use client'):**
- Forms with state
- Interactive UI elements
- Hooks (useState, useEffect, etc.)
- Event handlers
- Context providers
### Optimization Checklist
#### Server Components
- [ ] All data-fetching pages are Server Components
- [ ] Park listing page is Server Component
- [ ] Ride listing page is Server Component
- [ ] Detail pages use Server Components for data
- [ ] SEO metadata generated server-side
#### Client Components
- [ ] Form components marked with 'use client'
- [ ] Interactive UI (modals, dropdowns) marked
- [ ] Authentication components marked
- [ ] Submission forms marked
- [ ] No unnecessary 'use client' directives
#### Component Structure
```typescript
// Good: Server Component with Client Components inside
// app/parks/page.tsx
export default async function ParksPage() {
const parks = await fetch(API_URL).then(r => r.json());
return (
<div>
<h1>Parks</h1>
<ParkFilters /> {/* Client Component */}
<ParksList parks={parks} /> {/* Can be Server Component */}
</div>
);
}
// Client Component for interactivity
// components/ParkFilters.tsx
'use client';
export function ParkFilters() {
const [filters, setFilters] = useState({});
// ...
}
```
---
## ✅ Task 13.3: Data Fetching Optimization (3 hours)
### Implement Caching Strategies
```typescript
// app/parks/[parkSlug]/page.tsx
export async function generateStaticParams() {
// Pre-render top 100 parks at build time
const parks = await fetch(`${API_URL}/parks/?page_size=100`);
return parks.results.map((park) => ({
parkSlug: park.slug,
}));
}
export default async function ParkDetailPage({
params
}: {
params: { parkSlug: string }
}) {
// Cache for 1 hour, revalidate in background
const park = await fetch(`${API_URL}/parks/${params.parkSlug}/`, {
next: { revalidate: 3600 }
}).then(r => r.json());
return <ParkDetail park={park} />;
}
```
### Caching Strategy Checklist
- [ ] Static pages use ISR (Incremental Static Regeneration)
- [ ] Dynamic pages have appropriate revalidation times
- [ ] Listing pages cache for 5-10 minutes
- [ ] Detail pages cache for 1 hour
- [ ] User-specific pages are not cached
- [ ] Search results are not cached
### Parallel Data Fetching
```typescript
// Fetch multiple resources in parallel
export default async function RideDetailPage({ params }) {
const [ride, reviews, photos] = await Promise.all([
fetch(`${API_URL}/rides/${params.rideSlug}/`),
fetch(`${API_URL}/rides/${params.rideSlug}/reviews/`),
fetch(`${API_URL}/rides/${params.rideSlug}/photos/`),
]);
return <RideDetail ride={ride} reviews={reviews} photos={photos} />;
}
```
- [ ] Parallel fetching implemented where possible
- [ ] No waterfall requests
- [ ] Loading states for slow requests
---
## ✅ Task 13.4: Loading States & Suspense (2 hours)
### Create Loading Components
```typescript
// app/parks/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
<div className="h-8 bg-gray-200 rounded animate-pulse" />
<div className="grid grid-cols-3 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-48 bg-gray-200 rounded animate-pulse" />
))}
</div>
</div>
);
}
```
### Implement Streaming
```typescript
// app/parks/[parkSlug]/page.tsx
import { Suspense } from 'react';
export default async function ParkPage({ params }) {
return (
<div>
<Suspense fallback={<ParkHeaderSkeleton />}>
<ParkHeader slug={params.parkSlug} />
</Suspense>
<Suspense fallback={<RidesListSkeleton />}>
<RidesList parkSlug={params.parkSlug} />
</Suspense>
</div>
);
}
```
### Checklist
- [ ] Loading.tsx for each major route
- [ ] Suspense boundaries for slow components
- [ ] Skeleton screens match final UI
- [ ] Streaming SSR enabled
- [ ] No flash of loading state
---
## ✅ Task 13.5: Metadata API for SEO (2 hours)
### Implement Dynamic Metadata
```typescript
// app/parks/[parkSlug]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({
params
}: {
params: { parkSlug: string }
}): Promise<Metadata> {
const park = await fetch(`${API_URL}/parks/${params.parkSlug}/`)
.then(r => r.json());
return {
title: `${park.name} - ThrillWiki`,
description: park.description || `Discover ${park.name}, a ${park.park_type} featuring exciting rides and attractions.`,
openGraph: {
title: park.name,
description: park.description,
images: [
{
url: park.banner_image_url || '/og-image.png',
width: 1200,
height: 630,
alt: park.name,
},
],
type: 'website',
siteName: 'ThrillWiki',
},
twitter: {
card: 'summary_large_image',
title: park.name,
description: park.description,
images: [park.banner_image_url || '/og-image.png'],
},
alternates: {
canonical: `https://thrillwiki.com/parks/${park.slug}`,
},
};
}
```
### SEO Checklist
- [ ] All pages have unique titles
- [ ] All pages have meta descriptions
- [ ] OpenGraph tags for social sharing
- [ ] Twitter Card tags
- [ ] Canonical URLs
- [ ] Structured data (JSON-LD)
- [ ] Robots.txt configured
- [ ] Sitemap.xml generated
---
## ✅ Task 13.6: Performance Optimization (3 hours)
### Bundle Analysis
```bash
# Install bundle analyzer
bun add @next/bundle-analyzer
# Update next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer(nextConfig);
# Run analysis
ANALYZE=true bun run build
```
### Code Splitting
```typescript
// Use dynamic imports for heavy components
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // Don't render on server
});
```
### Performance Checklist
- [ ] Bundle size < 500KB initial load
- [ ] Code splitting for large dependencies
- [ ] Images use next/image
- [ ] Fonts optimized with next/font
- [ ] CSS modules for component styles
- [ ] No unused dependencies
- [ ] Tree shaking working
### Core Web Vitals Targets
- [ ] LCP (Largest Contentful Paint) < 2.5s
- [ ] FID (First Input Delay) < 100ms
- [ ] CLS (Cumulative Layout Shift) < 0.1
- [ ] TTFB (Time to First Byte) < 600ms
---
## ✅ Task 13.7: Environment Variables & Security (1 hour)
### Environment Configuration
Create `.env.local`:
```bash
# Django API (required for build)
NEXT_PUBLIC_DJANGO_API_URL=https://api.thrillwiki.com
# CloudFlare (public)
NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID=xxx
# Server-only secrets (no NEXT_PUBLIC_ prefix)
CLOUDFLARE_API_TOKEN=xxx
DJANGO_SECRET_KEY=xxx
```
### Security Checklist
- [ ] No secrets in client-side code
- [ ] Server-only vars don't have NEXT_PUBLIC_ prefix
- [ ] .env.local in .gitignore
- [ ] .env.example documented
- [ ] Environment validation on startup
- [ ] Sensitive data not logged
### Runtime Validation
```typescript
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
NEXT_PUBLIC_DJANGO_API_URL: z.string().url(),
NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID: z.string().min(1),
});
export const env = envSchema.parse({
NEXT_PUBLIC_DJANGO_API_URL: process.env.NEXT_PUBLIC_DJANGO_API_URL,
NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID: process.env.NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID,
});
```
---
## ✅ Task 13.8: Error Boundaries & Monitoring (2 hours)
### Global Error Handling
```typescript
// app/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/global-error.tsx (catches errors in root layout)
'use client';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</body>
</html>
);
}
```
### Not Found Pages
```typescript
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - Page Not Found</h2>
<p>The page you're looking for doesn't exist.</p>
</div>
);
}
```
### Checklist
- [ ] Error boundaries for each route segment
- [ ] Global error handler
- [ ] 404 page styled
- [ ] Error logging implemented
- [ ] User-friendly error messages
- [ ] Errors don't expose sensitive data
---
## 🎯 Phase Completion Criteria
### Configuration
- [ ] Turbopack fully configured
- [ ] Image optimization working
- [ ] Environment variables validated
- [ ] next.config.js optimized
### Performance
- [ ] Bundle size optimized
- [ ] Code splitting implemented
- [ ] Images optimized
- [ ] Fonts optimized
- [ ] Core Web Vitals meet targets
### SEO
- [ ] All pages have metadata
- [ ] OpenGraph tags present
- [ ] Sitemap generated
- [ ] Robots.txt configured
### User Experience
- [ ] Loading states implemented
- [ ] Error boundaries working
- [ ] 404 pages styled
- [ ] No jarring layout shifts
### Code Quality
- [ ] Server/Client components optimized
- [ ] No unnecessary 'use client' directives
- [ ] Parallel data fetching where possible
- [ ] Proper caching strategies
---
## 📊 Progress Tracking
**Started:** [Date]
**Completed:** [Date]
**Time Spent:** [Hours]
### Tasks Completed
- [ ] Task 13.1: Turbopack Configuration
- [ ] Task 13.2: Server vs Client Components
- [ ] Task 13.3: Data Fetching Optimization
- [ ] Task 13.4: Loading States & Suspense
- [ ] Task 13.5: Metadata API for SEO
- [ ] Task 13.6: Performance Optimization
- [ ] Task 13.7: Environment Variables
- [ ] Task 13.8: Error Boundaries
---
## ⏭️ Next Phase
Once this phase is complete, proceed to [Phase 14: Cleanup & Testing](./PHASE_14_CLEANUP_TESTING.md)
---
## 🔗 Related Documentation
- [Next.js 15 Migration Guide](./NEXTJS_15_MIGRATION_GUIDE.md)
- [Environment Variables Guide](./ENVIRONMENT_VARIABLES.md)
- [Bun Setup Guide](./BUN_GUIDE.md)
- Next.js Documentation: https://nextjs.org/docs
---
**Document Version:** 1.0
**Last Updated:** November 9, 2025