Files
gh-rafaelcalleja-claude-mar…/skills/web-frameworks/references/nextjs-optimization.md
2025-11-30 08:48:52 +08:00

11 KiB

Next.js Optimization

Performance optimization techniques for images, fonts, scripts, and bundles.

Image Optimization

Next.js Image Component

Automatic optimization with modern formats (WebP, AVIF):

import Image from 'next/image'

export default function Page() {
  return (
    <>
      {/* Local image */}
      <Image
        src="/hero.jpg"
        alt="Hero image"
        width={1200}
        height={600}
        priority // Load immediately, no lazy loading
      />

      {/* Remote image */}
      <Image
        src="https://example.com/photo.jpg"
        alt="Photo"
        width={800}
        height={600}
        quality={90} // 1-100, default 75
      />

      {/* Responsive fill */}
      <div style={{ position: 'relative', width: '100%', height: '400px' }}>
        <Image
          src="/background.jpg"
          alt="Background"
          fill
          style={{ objectFit: 'cover' }}
          sizes="100vw"
        />
      </div>

      {/* With blur placeholder */}
      <Image
        src="/profile.jpg"
        alt="Profile"
        width={200}
        height={200}
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,..." // Or use static import
      />
    </>
  )
}

Image Props Reference

Required:

  • src - Image path (string or static import)
  • alt - Alt text for accessibility
  • width, height - Dimensions (required unless using fill)

Optional:

  • fill - Fill parent container (makes width/height optional)
  • sizes - Responsive sizes hint for srcset
  • quality - 1-100 (default 75)
  • priority - Disable lazy loading, preload image
  • placeholder - 'blur' | 'empty' (default 'empty')
  • blurDataURL - Data URL for blur placeholder
  • loading - 'lazy' | 'eager' (default 'lazy')
  • style - CSS styles
  • className - CSS class
  • onLoad - Callback when loaded

Responsive Images with Sizes

<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

This tells browser:

  • Mobile (<768px): Use 100% viewport width
  • Tablet (768-1200px): Use 50% viewport width
  • Desktop (>1200px): Use 33% viewport width

Static Import for Local Images

import heroImage from '@/public/hero.jpg'

<Image
  src={heroImage}
  alt="Hero"
  placeholder="blur" // Automatically generated
  // No width/height needed - inferred from import
/>

Remote Image Configuration

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
      }
    ],
    // Device sizes for srcset
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    // Image sizes for srcset
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    // Supported formats
    formats: ['image/webp'],
    // Cache optimization images for 60 days
    minimumCacheTTL: 60 * 60 * 24 * 60,
  }
}

Font Optimization

Google Fonts

Automatic optimization with zero layout shift:

// app/layout.tsx
import { Inter, Roboto_Mono, Playfair_Display } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  weight: ['400', '700'],
  variable: '--font-roboto-mono',
})

const playfair = Playfair_Display({
  subsets: ['latin'],
  display: 'swap',
  weight: ['400', '700', '900'],
  style: ['normal', 'italic'],
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body className={inter.className}>{children}</body>
    </html>
  )
}

Use CSS variables:

.code {
  font-family: var(--font-roboto-mono);
}

Local Fonts

import localFont from 'next/font/local'

const myFont = localFont({
  src: [
    {
      path: './fonts/my-font-regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: './fonts/my-font-bold.woff2',
      weight: '700',
      style: 'normal',
    }
  ],
  variable: '--font-my-font',
  display: 'swap',
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={myFont.variable}>
      <body>{children}</body>
    </html>
  )
}

Font Display Strategies

const font = Inter({
  display: 'swap', // Show fallback immediately, swap when loaded (recommended)
  // display: 'optional', // Only use font if available immediately
  // display: 'block', // Hide text until font loads (max 3s)
  // display: 'fallback', // Show fallback briefly, swap if loaded quickly
  // display: 'auto', // Browser default
})

Script Optimization

Script Component

Control loading behavior:

import Script from 'next/script'

export default function Page() {
  return (
    <>
      {/* Load after page is interactive (recommended for analytics) */}
      <Script
        src="https://www.googletagmanager.com/gtag/js"
        strategy="afterInteractive"
      />

      {/* Load while page is idle (lowest priority) */}
      <Script
        src="https://connect.facebook.net/en_US/sdk.js"
        strategy="lazyOnload"
      />

      {/* Load before page is interactive (use sparingly) */}
      <Script
        src="https://maps.googleapis.com/maps/api/js"
        strategy="beforeInteractive"
      />

      {/* Inline script with strategy */}
      <Script id="analytics" strategy="afterInteractive">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
        `}
      </Script>

      {/* With onLoad callback */}
      <Script
        src="https://example.com/sdk.js"
        onLoad={() => console.log('Script loaded')}
        onError={(e) => console.error('Script failed', e)}
      />
    </>
  )
}

Strategy options:

  • beforeInteractive - Load before page interactive (blocking)
  • afterInteractive - Load after page interactive (default)
  • lazyOnload - Load during idle time
  • worker - Load in web worker (experimental)

Bundle Optimization

Analyzing Bundle Size

# Install bundle analyzer
npm install @next/bundle-analyzer

# Create next.config.js wrapper
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // Your Next.js config
})
# Run analysis
ANALYZE=true npm run build

Dynamic Import (Code Splitting)

Split code and load on-demand:

import dynamic from 'next/dynamic'

// Dynamic import with loading state
const DynamicChart = dynamic(() => import('@/components/chart'), {
  loading: () => <div>Loading chart...</div>,
  ssr: false, // Disable SSR for this component
})

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <DynamicChart />
    </div>
  )
}

Named exports:

const DynamicComponent = dynamic(
  () => import('@/components/hello').then(mod => mod.Hello)
)

Multiple components:

const DynamicHeader = dynamic(() => import('@/components/header'))
const DynamicFooter = dynamic(() => import('@/components/footer'))

Tree Shaking

Import only what you need:

// ❌ Bad - imports entire library
import _ from 'lodash'
const result = _.debounce(fn, 300)

// ✅ Good - imports only debounce
import debounce from 'lodash/debounce'
const result = debounce(fn, 300)

// ❌ Bad
import * as Icons from 'react-icons/fa'
<Icons.FaHome />

// ✅ Good
import { FaHome } from 'react-icons/fa'
<FaHome />

Partial Prerendering (PPR)

Experimental: Combine static and dynamic rendering in same route.

// next.config.js
module.exports = {
  experimental: {
    ppr: true,
  }
}
// app/page.tsx
import { Suspense } from 'react'

// Static shell
export default function Page() {
  return (
    <div>
      <header>Static Header</header>

      {/* Dynamic content with Suspense boundary */}
      <Suspense fallback={<div>Loading...</div>}>
        <DynamicContent />
      </Suspense>

      <footer>Static Footer</footer>
    </div>
  )
}

// Dynamic component
async function DynamicContent() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'no-store'
  }).then(r => r.json())

  return <div>{data.content}</div>
}

Static shell loads instantly, dynamic content streams in.

Metadata Optimization

Static Metadata

// app/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My Page',
  description: 'Page description',
  keywords: ['next.js', 'react', 'javascript'],
  openGraph: {
    title: 'My Page',
    description: 'Page description',
    images: ['/og-image.jpg'],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'My Page',
    description: 'Page description',
    images: ['/twitter-image.jpg'],
  },
  alternates: {
    canonical: 'https://example.com/page',
  },
  robots: {
    index: true,
    follow: true,
  },
}

Dynamic Metadata

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
      type: 'article',
      publishedTime: post.publishedAt,
      authors: [post.author.name],
    },
  }
}

Metadata Files

Create these files in app/ directory:

  • favicon.ico - Favicon
  • icon.png / icon.jpg - App icon
  • apple-icon.png - Apple touch icon
  • opengraph-image.png - Open Graph image
  • twitter-image.png - Twitter card image
  • robots.txt - Robots file
  • sitemap.xml - Sitemap

Or generate dynamically:

// app/sitemap.ts
export default async function sitemap() {
  const posts = await getPosts()

  return [
    {
      url: 'https://example.com',
      lastModified: new Date(),
    },
    ...posts.map(post => ({
      url: `https://example.com/blog/${post.slug}`,
      lastModified: post.updatedAt,
    }))
  ]
}

Performance Best Practices

  1. Use Image component - Automatic optimization, lazy loading, modern formats
  2. Optimize fonts - Use next/font to eliminate layout shift
  3. Dynamic imports - Code split large components and third-party libraries
  4. Analyze bundle - Identify and eliminate large dependencies
  5. Proper caching - Use ISR for semi-static content
  6. Streaming with Suspense - Load fast content first, stream slow content
  7. Minimize JavaScript - Default to Server Components
  8. Prefetch links - Next.js prefetches Link components in viewport
  9. Use Script component - Control third-party script loading
  10. Compress assets - Enable compression in hosting platform
  11. Use CDN - Deploy to edge network (Vercel, Cloudflare)
  12. Monitor metrics - Track Core Web Vitals (LCP, FID, CLS)