Skip to content

Next.js Static Assets

Static assets are an important part of web applications, including images, fonts, icons, and other files. Next.js provides powerful static asset handling capabilities. This chapter will detail how to optimize and manage these resources.

Static Assets Basics

The public Directory

Next.js uses the public directory to store static assets:

public/
├── favicon.ico
├── logo.png
├── images/
│   ├── hero.jpg
│   ├── avatar.png
│   └── gallery/
│       ├── photo1.jpg
│       └── photo2.jpg
├── icons/
│   ├── home.svg
│   └── user.svg
├── fonts/
│   ├── custom-font.woff2
│   └── custom-font.woff
└── documents/
    └── manual.pdf

Accessing Static Assets

typescript
// Access directly using path
export default function HomePage() {
  return (
    <div>
      <img src="/logo.png" alt="Logo" />
      <img src="/images/hero.jpg" alt="Hero" />
      <a href="/documents/manual.pdf" download>
        Download Manual
      </a>
    </div>
  )
}

Image Optimization

Next.js Image Component

typescript
// components/OptimizedImage.tsx
import Image from 'next/image'

export default function OptimizedImage() {
  return (
    <div>
      {/* Basic usage */}
      <Image
        src="/images/hero.jpg"
        alt="Hero Image"
        width={800}
        height={400}
      />

      {/* Responsive image */}
      <Image
        src="/images/hero.jpg"
        alt="Hero Image"
        fill
        style={{ objectFit: 'cover' }}
      />

      {/* Priority loading */}
      <Image
        src="/images/hero.jpg"
        alt="Hero Image"
        width={800}
        height={400}
        priority
      />
    </div>
  )
}

External Images

typescript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['example.com', 'cdn.example.com'],
    // Or use remotePatterns (recommended)
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
        port: '',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
      },
    ],
  },
}

module.exports = nextConfig
typescript
// Using external images
import Image from 'next/image'

export default function ExternalImage() {
  return (
    <Image
      src="https://example.com/images/photo.jpg"
      alt="External Photo"
      width={600}
      height={400}
    />
  )
}

Dynamic Images

typescript
// components/DynamicImage.tsx
import Image from 'next/image'
import { useState } from 'react'

interface DynamicImageProps {
  src: string
  alt: string
  width: number
  height: number
}

export default function DynamicImage({ src, alt, width, height }: DynamicImageProps) {
  const [isLoading, setIsLoading] = useState(true)
  const [hasError, setHasError] = useState(false)

  return (
    <div className="relative">
      {isLoading && (
        <div className="absolute inset-0 bg-gray-200 animate-pulse rounded" />
      )}
      
      <Image
        src={src}
        alt={alt}
        width={width}
        height={height}
        onLoad={() => setIsLoading(false)}
        onError={() => {
          setIsLoading(false)
          setHasError(true)
        }}
        className={`transition-opacity duration-300 ${
          isLoading ? 'opacity-0' : 'opacity-100'
        }`}
      />
      
      {hasError && (
        <div className="absolute inset-0 bg-gray-100 flex items-center justify-center">
          <span className="text-gray-500">Image failed to load</span>
        </div>
      )}
    </div>
  )
}
typescript
// components/ImageGallery.tsx
'use client'

import Image from 'next/image'
import { useState } from 'react'

interface ImageItem {
  id: string
  src: string
  alt: string
  width: number
  height: number
}

interface ImageGalleryProps {
  images: ImageItem[]
}

export default function ImageGallery({ images }: ImageGalleryProps) {
  const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null)

  return (
    <>
      <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
        {images.map((image) => (
          <div
            key={image.id}
            className="cursor-pointer hover:opacity-80 transition-opacity"
            onClick={() => setSelectedImage(image)}
          >
            <Image
              src={image.src}
              alt={image.alt}
              width={300}
              height={200}
              className="rounded-lg object-cover"
            />
          </div>
        ))}
      </div>

      {/* Modal */}
      {selectedImage && (
        <div
          className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
          onClick={() => setSelectedImage(null)}
        >
          <div className="relative max-w-4xl max-h-full p-4">
            <Image
              src={selectedImage.src}
              alt={selectedImage.alt}
              width={selectedImage.width}
              height={selectedImage.height}
              className="max-w-full max-h-full object-contain"
            />
            <button
              className="absolute top-4 right-4 text-white text-2xl"
              onClick={() => setSelectedImage(null)}
            >

            </button>
          </div>
        </div>
      )}
    </>
  )
}

Font Optimization

Google Fonts

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

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

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
})

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={inter.className}>
      <body>
        <div className={robotoMono.className}>
          Code font example
        </div>
        {children}
      </body>
    </html>
  )
}

Local Fonts

typescript
// app/layout.tsx
import localFont from 'next/font/local'

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={customFont.className}>
      <body>{children}</body>
    </html>
  )
}

Font Variables

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

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

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}
css
/* globals.css */
:root {
  --font-inter: 'Inter', sans-serif;
  --font-roboto-mono: 'Roboto Mono', monospace;
}

body {
  font-family: var(--font-inter);
}

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

Icon Management

SVG Icon Components

typescript
// components/icons/HomeIcon.tsx
export default function HomeIcon({ 
  size = 24, 
  color = 'currentColor' 
}: { 
  size?: number
  color?: string 
}) {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z"
        stroke={color}
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
      <path
        d="M9 22V12H15V22"
        stroke={color}
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  )
}

Icon Library

typescript
// components/icons/index.tsx
import HomeIcon from './HomeIcon'
import UserIcon from './UserIcon'
import SettingsIcon from './SettingsIcon'

export const icons = {
  home: HomeIcon,
  user: UserIcon,
  settings: SettingsIcon,
} as const

export type IconName = keyof typeof icons

interface IconProps {
  name: IconName
  size?: number
  color?: string
  className?: string
}

export default function Icon({ name, size = 24, color = 'currentColor', className }: IconProps) {
  const IconComponent = icons[name]
  
  return (
    <IconComponent 
      size={size} 
      color={color} 
      className={className}
    />
  )
}

Using Icons

typescript
// components/Navigation.tsx
import Icon from '@/components/icons'

export default function Navigation() {
  return (
    <nav className="flex space-x-4">
      <a href="/" className="flex items-center space-x-2">
        <Icon name="home" size={20} />
        <span>Home</span>
      </a>
      <a href="/profile" className="flex items-center space-x-2">
        <Icon name="user" size={20} />
        <span>Profile</span>
      </a>
      <a href="/settings" className="flex items-center space-x-2">
        <Icon name="settings" size={20} />
        <span>Settings</span>
      </a>
    </nav>
  )
}

File Downloads

Download Component

typescript
// components/DownloadButton.tsx
'use client'

import { useState } from 'react'

interface DownloadButtonProps {
  url: string
  filename: string
  children: React.ReactNode
}

export default function DownloadButton({ url, filename, children }: DownloadButtonProps) {
  const [downloading, setDownloading] = useState(false)

  const handleDownload = async () => {
    setDownloading(true)
    
    try {
      const response = await fetch(url)
      const blob = await response.blob()
      
      const downloadUrl = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = downloadUrl
      link.download = filename
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
      window.URL.revokeObjectURL(downloadUrl)
    } catch (error) {
      console.error('Download failed:', error)
    } finally {
      setDownloading(false)
    }
  }

  return (
    <button
      onClick={handleDownload}
      disabled={downloading}
      className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:opacity-50"
    >
      {downloading ? 'Downloading...' : children}
    </button>
  )
}

File Preview

typescript
// components/FilePreview.tsx
'use client'

import { useState } from 'react'

interface FilePreviewProps {
  url: string
  type: 'image' | 'pdf' | 'video'
  name: string
}

export default function FilePreview({ url, type, name }: FilePreviewProps) {
  const [isOpen, setIsOpen] = useState(false)

  const renderPreview = () => {
    switch (type) {
      case 'image':
        return (
          <img
            src={url}
            alt={name}
            className="max-w-full max-h-full object-contain"
          />
        )
      case 'pdf':
        return (
          <iframe
            src={url}
            className="w-full h-full"
            title={name}
          />
        )
      case 'video':
        return (
          <video
            src={url}
            controls
            className="max-w-full max-h-full"
          >
            Your browser does not support video playback
          </video>
        )
      default:
        return <div>Unsupported file type</div>
    }
  }

  return (
    <>
      <button
        onClick={() => setIsOpen(true)}
        className="text-blue-500 hover:underline"
      >
        Preview {name}
      </button>

      {isOpen && (
        <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
          <div className="relative w-full h-full max-w-4xl max-h-full p-4">
            <button
              onClick={() => setIsOpen(false)}
              className="absolute top-4 right-4 text-white text-2xl z-10"
            >

            </button>
            <div className="w-full h-full flex items-center justify-center">
              {renderPreview()}
            </div>
          </div>
        </div>
      )}
    </>
  )
}

Asset Optimization

Image Compression Configuration

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    formats: ['image/webp', 'image/avif'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60 * 60 * 24 * 365, // 1 year
  },
}

module.exports = nextConfig

Lazy Loading

typescript
// components/LazyImage.tsx
'use client'

import Image from 'next/image'
import { useState, useRef, useEffect } from 'react'

interface LazyImageProps {
  src: string
  alt: string
  width: number
  height: number
  placeholder?: string
}

export default function LazyImage({ 
  src, 
  alt, 
  width, 
  height, 
  placeholder = '/placeholder.jpg' 
}: LazyImageProps) {
  const [isInView, setIsInView] = useState(false)
  const imgRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsInView(true)
          observer.disconnect()
        }
      },
      { threshold: 0.1 }
    )

    if (imgRef.current) {
      observer.observe(imgRef.current)
    }

    return () => observer.disconnect()
  }, [])

  return (
    <div ref={imgRef} style={{ width, height }}>
      <Image
        src={isInView ? src : placeholder}
        alt={alt}
        width={width}
        height={height}
        className={`transition-opacity duration-300 ${
          isInView ? 'opacity-100' : 'opacity-50'
        }`}
      />
    </div>
  )
}

Resource Preloading

typescript
// components/ResourcePreloader.tsx
'use client'

import { useEffect } from 'react'

interface ResourcePreloaderProps {
  images?: string[]
  fonts?: string[]
}

export default function ResourcePreloader({ images = [], fonts = [] }: ResourcePreloaderProps) {
  useEffect(() => {
    // Preload images
    images.forEach(src => {
      const img = new Image()
      img.src = src
    })

    // Preload fonts
    fonts.forEach(src => {
      const link = document.createElement('link')
      link.rel = 'preload'
      link.as = 'font'
      link.type = 'font/woff2'
      link.crossOrigin = 'anonymous'
      link.href = src
      document.head.appendChild(link)
    })
  }, [images, fonts])

  return null
}

Static Asset CDN

CDN Configuration

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  assetPrefix: process.env.NODE_ENV === 'production' 
    ? 'https://cdn.example.com' 
    : '',
  
  images: {
    loader: 'custom',
    loaderFile: './lib/imageLoader.js',
  },
}

module.exports = nextConfig
javascript
// lib/imageLoader.js
export default function imageLoader({ src, width, quality }) {
  const params = new URLSearchParams()
  params.set('w', width.toString())
  if (quality) {
    params.set('q', quality.toString())
  }
  
  return `https://cdn.example.com${src}?${params}`
}

Environment Variable Configuration

bash
# .env.local
NEXT_PUBLIC_CDN_URL=https://cdn.example.com
NEXT_PUBLIC_ASSET_PREFIX=https://assets.example.com
typescript
// lib/assets.ts
export function getAssetUrl(path: string): string {
  const cdnUrl = process.env.NEXT_PUBLIC_CDN_URL
  return cdnUrl ? `${cdnUrl}${path}` : path
}

// Usage example
import { getAssetUrl } from '@/lib/assets'

export default function MyComponent() {
  return (
    <img 
      src={getAssetUrl('/images/logo.png')} 
      alt="Logo" 
    />
  )
}

Performance Monitoring

Resource Loading Monitoring

typescript
// hooks/useResourceMonitor.ts
'use client'

import { useEffect, useState } from 'react'

interface ResourceTiming {
  name: string
  duration: number
  size?: number
}

export function useResourceMonitor() {
  const [resources, setResources] = useState<ResourceTiming[]>([])

  useEffect(() => {
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      const resourceTimings = entries.map(entry => ({
        name: entry.name,
        duration: entry.duration,
        size: (entry as any).transferSize,
      }))
      
      setResources(prev => [...prev, ...resourceTimings])
    })

    observer.observe({ entryTypes: ['resource'] })

    return () => observer.disconnect()
  }, [])

  return resources
}

Best Practices

1. Image Optimization

typescript
// ✅ Good practice
<Image
  src="/hero.jpg"
  alt="Hero Image"
  width={800}
  height={400}
  priority // Above-the-fold image
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,..."
/>

// ❌ Avoid
<img src="/hero.jpg" alt="Hero Image" />

2. Font Optimization

typescript
// ✅ Good practice
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Avoid font flash
  preload: true,
})

// ❌ Avoid
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" />

3. Resource Organization

public/
├── images/
│   ├── common/          # Common images
│   ├── pages/           # Page-specific images
│   └── components/      # Component images
├── icons/
│   ├── ui/              # UI icons
│   └── social/          # Social icons
└── fonts/
    ├── display/         # Display fonts
    └── body/            # Body fonts

Summary

Key points for Next.js static asset management:

  • Image component - Automatic image performance optimization
  • Font optimization - Use next/font to avoid layout shift
  • Resource organization - Logical directory structure
  • Performance optimization - Lazy loading, preloading, CDN
  • Monitoring and analytics - Track resource loading performance

By using these features effectively, you can significantly improve your application's loading speed and user experience.

Content is for learning and research only.