Skip to main content

Overview

Poke-Nex implements multiple performance optimizations to deliver an exceptional user experience. From parallel builds to smart component architecture, every aspect is tuned for speed.
Key Achievement: 1030+ static pages generated in < 12 seconds with optimized assets and zero runtime API calls.

Parallel Build with Worker Pools

Multi-threaded Page Generation

Next.js 16 uses worker pools to parallelize static page generation, dramatically reducing build times:
# Build output example
 Generating static pages (1030/1030)
 Finalizing page optimization

Route (app)                              Size     First Load JS
 /                                    1.2 kB         85.3 kB
 /pokemon/[slug]                      2.8 kB         88.1 kB
 /favorites                           1.5 kB         86.8 kB

Build time: 11.7s
How it works:
  1. Next.js spawns multiple worker threads (based on CPU cores)
  2. Each worker generates a subset of static pages in parallel
  3. Workers share the same Node.js runtime for efficiency
  4. Results are collected and finalized in the main thread
To maximize parallel build performance, ensure your build environment has sufficient CPU cores. Most CI/CD platforms provide 2-4 cores by default.

Next.js Image Optimization

Remote Image Patterns

Poke-Nex configures Next.js Image optimization for Pokémon sprites hosted on GitHub:
next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  reactCompiler: true,
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'raw.githubusercontent.com',
        pathname: '/PokeAPI/sprites/master/sprites/pokemon/**',
      },
    ],
  },
}

export default nextConfig

Benefits of Image Optimization

  • Automatic WebP/AVIF conversion: Smaller file sizes with better quality
  • Lazy loading: Images load only when visible in viewport
  • Responsive images: Automatically sized for device screen
  • Blur placeholder: Prevents layout shift during load
  • CDN optimization: Images served from Next.js CDN
Always configure remotePatterns for external images. Using domains (deprecated) or allowing all domains is a security risk.

React Compiler Optimization

Automatic Memoization

Poke-Nex enables the experimental React Compiler for automatic performance optimizations:
next.config.ts
const nextConfig: NextConfig = {
  reactCompiler: true,
  // ...
}
What the React Compiler does:
  • Automatically memoizes components (no need for React.memo)
  • Optimizes re-renders without manual useMemo or useCallback
  • Reduces bundle size by eliminating wrapper components
  • Improves runtime performance with smarter reconciliation
The React Compiler is experimental in Next.js 16. It provides significant performance improvements but may have edge cases. Monitor your application behavior after enabling.

Smart Component Architecture

Client-Side Components with Hydration Guards

Poke-Nex uses selective client-side rendering with hydration guards to prevent flickering:
src/components/pokemon/PokeGallery.tsx
'use client'

import { useHydrated } from '@/hooks/useHydrated'
import { PokeGallerySkeleton } from '../skeletons'

export const PokeGallery = ({ content }: Props) => {
  const isHydrated = useHydrated()
  const view = useTweaksStore((s) => s.view)
  
  if (!isHydrated) return <PokeGallerySkeleton />
  
  return (
    <section className="flex flex-col gap-8">
      {/* Gallery content */}
    </section>
  )
}
Architecture benefits:
  1. Server renders skeleton: Initial HTML shows loading state
  2. Hydration guard prevents flash: No mismatch between SSR and client state
  3. Smooth transition: From skeleton to real content
  4. Better perceived performance: User sees content immediately

Zustand for Minimal Re-renders

Poke-Nex uses Zustand with fine-grained selectors to minimize component re-renders:
src/components/pokemon/PokeGallery.tsx
// Only re-renders when 'view' changes
const view = useTweaksStore((s) => s.view)
const setView = useTweaksStore((s) => s.setView)

// Separate selector for different state
const page = useTweaksStore((s) => s.page)
Performance advantages:
  • Components only re-render when their selected state changes
  • No context provider re-render cascades
  • Smaller bundle size than Redux or Context API
  • Zero boilerplate for selectors

High-Performance Filtering

Search input is debounced to prevent excessive filtering operations:
src/hooks/usePokeFilters.ts
const {
  list: filteredList,
  state: filterState,
  setSearch,
  // ...
} = usePokeFilters(content, { debounce: 250 })
How debouncing improves performance:
  1. User types “charizard”
  2. Each keystroke is captured but not immediately processed
  3. After 250ms of no typing, the filter runs once
  4. Prevents 9 filter operations, only performs 1
  5. Reduces CPU usage and improves battery life on mobile
A 250ms debounce is the sweet spot for search inputs. It feels instant to users while significantly reducing computational overhead.

Pagination for Large Lists

Virtualized Rendering

Poke-Nex uses custom pagination to render only visible items:
src/components/pokemon/PokeGallery.tsx
const {
  paginated: paginatedList,
  current,
  pages,
  next,
  prev,
  setCurrent,
} = usePaginate(filteredList, 24)
Performance impact:
  • Without pagination: Rendering 1025+ cards = ~5-10 seconds initial render
  • With pagination: Rendering 24 cards = ~50-100ms initial render
  • Memory usage: 98% reduction in DOM nodes
  • Scroll performance: Smooth scrolling without layout thrashing

State Persistence Strategy

SessionStorage for View Preferences

Poke-Nex uses sessionStorage for temporary preferences and localStorage for persistent data:
src/stores/tweaks.store.ts
export const useTweaksStore = create<TweaksState>()()
  persist(
    (set, get) => ({
      // State and actions
    }),
    {
      name: 'pokenex-tweaks',
      storage: createJSONStorage(() => sessionStorage),
      partialize: (state) => ({
        view: state.view,
        region: state.region,
        types: state.types,
        // page is NOT persisted - resets to 1 on navigation
      }),
    }
  )
)
src/stores/favorite.store.ts
export const useFavoriteStore = create<Store>()()
  persist(
    (set, get) => ({
      // State and actions
    }),
    {
      name: 'POKENEX-FAVORITE-LIST',
      // Uses localStorage by default for permanent persistence
    }
  )
)
Storage strategy:
  • SessionStorage (tweaks): View mode, filters, region - cleared when tab closes
  • LocalStorage (favorites): User’s favorite Pokémon - persists across sessions
  • Partialize: Only persist necessary state, keep page/query transient
This dual-storage approach provides the best user experience: preferences persist within a session, but volatile data like pagination resets for a fresh start.

Build-Time Optimizations

GraphQL for Minimal Data Transfer

The home page uses GraphQL to fetch only required fields:
src/lib/api/pokemon.api.ts
const query = `
  query getPokemonList($limit: Int, $offset: Int) {
    pokemon(limit: $limit, offset: $offset) {
      id
      name
      pokemontypes {
        type {
          name
        }
      }
    }
  }
`
Data reduction:
  • REST endpoint: ~500KB for full Pokémon data
  • GraphQL query: ~50KB with only id, name, types
  • Build time savings: 90% less data to process
  • Bandwidth savings: 90% less data transferred during build

Selective Extended Fetching

Detail pages conditionally fetch extended data:
src/services/pokemon.service.ts
export const getPokemonDetail = async (
  slug: string,
  extended: boolean = true
): Promise<ServiceResponse<PokemonDetail>> => {
  // extended=true: Fetches varieties, evolution chains, flavor text
  // extended=false: Fetches only base stats for list views
}

Runtime Performance Metrics

Core Web Vitals

  • LCP (Largest Contentful Paint): < 0.5s (pre-rendered HTML)
  • FID (First Input Delay): < 50ms (minimal JavaScript)
  • CLS (Cumulative Layout Shift): < 0.1 (image optimization)
  • TTI (Time to Interactive): < 1s (efficient hydration)

JavaScript Bundle Size

First Load JS shared by all:     85.1 kB
├ chunks/framework-[hash].js     45.2 kB
├ chunks/main-[hash].js          32.8 kB
├ chunks/webpack-[hash].js       2.1 kB
└ css/[hash].css                 5.0 kB
Bundle optimizations:
  • Tree-shaking removes unused code
  • Dynamic imports for routes (code splitting)
  • Minimal dependencies (Zustand, React Icons)
  • No heavy charting libraries in main bundle
Keep your First Load JS under 100 kB for optimal performance on slower networks. Poke-Nex stays under 90 kB for all routes.

Monitoring and Profiling

Build Time Analysis

# Analyze build output
pnpm build

# Look for:
# - Long build times for individual pages
# - Large JavaScript bundles
# - Unused dependencies

Runtime Performance

// Use React DevTools Profiler
import { Profiler } from 'react'

<Profiler id="PokeGallery" onRender={callback}>
  <PokeGallery content={pokemonList} />
</Profiler>

Best Practices Summary

  1. Enable React Compiler for automatic optimizations
  2. Configure Image Optimization with specific remote patterns
  3. Use Zustand with fine-grained selectors to minimize re-renders
  4. Implement debouncing for search and filter inputs
  5. Paginate large lists to reduce initial render time
  6. Use GraphQL to fetch only necessary fields
  7. Separate persistence layers (session vs local storage)
  8. Monitor bundle size and keep it under 100 kB
  9. Profile components to identify performance bottlenecks
  10. Leverage SSG/ISR for zero runtime API calls

Build docs developers (and LLMs) love