Files
thrilltrack-explorer/migration/PHASE_13_NEXTJS_OPTIMIZATION.md

12 KiB

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:

/** @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

// 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

// 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

// 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

// 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

// 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

// 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

# 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

// 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:

# 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

// 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

// 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

// 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



Document Version: 1.0
Last Updated: November 9, 2025