Static Assets, Fonts, Images

Bài 6 – Cách quản lý tài nguyên tĩnh, font và tối ưu hình ảnh trong Next.js

18/11/2025 DaiPhan
Bài 6 / 18

Static Assets, Fonts và Images

Next.js cung cấp cơ chế quản lý tài nguyên tĩnh hiệu quả qua thư mục public/, hỗ trợ nhúng font an toàn, và cung cấp component <Image /> giúp tối ưu hình ảnh tự động. Đây là nền tảng để cải thiện tốc độ tải trang và trải nghiệm người dùng.


1. Static Assets - Tài nguyên tĩnh

1.1. Thư mục public/

public/
├── images/
│   ├── logo.png
│   ├── hero.jpg
│   └── icons/
│       ├── facebook.svg
│       └── twitter.svg
├── documents/
│   └── sample.pdf
└── videos/
    └── demo.mp4

1.2. Sử dụng assets trong code

// app/page.tsx
import Image from 'next/image'

export default function HomePage() {
  return (
    <div>
      {/* Ảnh từ public */}
      <img src="/images/logo.png" alt="Logo" />
      
      {/* Tài liệu PDF */}
      <a href="/documents/sample.pdf" target="_blank">
        Download Sample PDF
      </a>
      
      {/* Video */}
      <video controls>
        <source src="/videos/demo.mp4" type="video/mp4" />
      </video>
    </div>
  );
}

1.3. Best practices cho static assets

// ✅ Tốt: Tổ chức theo thư mục
public/
├── assets/
│   ├── css/
│   ├── js/
│   └── vendor/
├── uploads/
│   └── user-avatars/
└── media/
    ├── 2024/
    └── 2025/

// ❌ Tránh: Để file lung tung
public/
├── logo.png
├── banner.jpg
├── style.css
└── script.js

2. Next.js Image Component

2.1. Local Images với Image Component

// app/page.tsx
import Image from 'next/image'
import heroImage from '@/public/images/hero.jpg'

export default function HomePage() {
  return (
    <div className="hero-section">
      <Image
        src={heroImage}
        alt="Hero Image"
        priority // Load ngay lập tức
        placeholder="blur" // Hiện blur placeholder
      />
    </div>
  );
}

2.2. Remote Images

// app/page.tsx
import Image from 'next/image'

export default function ProductPage() {
  return (
    <div>
      <Image
        src="https://example.com/product.jpg"
        alt="Product"
        width={500}
        height={300}
        className="product-image"
      />
    </div>
  );
}

2.3. Cấu hình next.config.js

// next.config.js
module.exports = {
  images: {
    // Remote domains cho phép
    domains: ['example.com', 'cdn.example.com'],
    
    // Hoặc dùng remotePatterns (recommended)
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
        port: '',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: '*.mycdn.com',
        port: '',
        pathname: '/assets/**',
      },
    ],
    
    // Image formats
    formats: ['image/webp', 'image/avif'],
    
    // Device sizes
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    
    // Image sizes
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
}

3. Advanced Image Features

3.1. Responsive Images

// app/components/ProductGallery.tsx
import Image from 'next/image'

export default function ProductGallery({ images }) {
  return (
    <div className="gallery">
      {images.map((image, index) => (
        <div key={index} className="gallery-item">
          <Image
            src={image.src}
            alt={image.alt}
            fill // Fill container
            sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
            className="gallery-image"
          />
        </div>
      ))}
    </div>
  );
}

3.2. Image Optimization

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

export default function OptimizedImage({ src, alt }) {
  return (
    <div className="image-container">
      <Image
        src={src}
        alt={alt}
        width={800}
        height={600}
        quality={85} // Quality setting (1-100)
        priority={true} // High priority loading
        placeholder="blur" // Blur placeholder
        blurDataURL="data:image/jpeg;base64,..." // Optional blur data
        loading="eager" // or "lazy"
      />
    </div>
  );
}

3.3. Static Image Generation

// app/api/og/route.tsx - Dynamic OG Image
import { ImageResponse } from 'next/og'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const title = searchParams.get('title') || 'Default Title'
  
  return new ImageResponse(
    (
      <div style={{
        fontSize: 48,
        background: 'white',
        width: '100%',
        height: '100%',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
      }}>
        {title}
      </div>
    ),
    {
      width: 1200,
      height: 630,
    }
  )
}

4. Fonts với next/font

4.1. Google Fonts

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

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

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

const playfair = Playfair_Display({
  subsets: ['latin'],
  variable: '--font-playfair',
  weight: ['400', '700'],
})

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="vi" className={`${inter.variable} ${robotoMono.variable} ${playfair.variable}`}>
      <body className="font-inter">
        {children}
      </body>
    </html>
  );
}

4.2. Local Fonts

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

const myFont = localFont({
  src: [
    {
      path: './fonts/MyFont-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: './fonts/MyFont-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
    {
      path: './fonts/MyFont-Italic.woff2',
      weight: '400',
      style: 'italic',
    },
  ],
  variable: '--font-myfont',
})

export default function RootLayout({ children }) {
  return (
    <html lang="vi" className={myFont.variable}>
      <body className="font-sans">
        {children}
      </body>
    </html>
  );
}

4.3. Font Configuration

// app/layout.tsx
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap', // or 'block', 'fallback', 'optional'
  preload: true, // Preload font
  fallback: ['system-ui', 'arial'], // Fallback fonts
  adjustFontFallback: true, // Adjust fallback
})

5. CSS và Styling với Assets

5.1. Global CSS với Fonts

/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom font classes */
.font-heading {
  font-family: var(--font-playfair), serif;
}

.font-body {
  font-family: var(--font-inter), sans-serif;
}

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

/* Image utilities */
.image-responsive {
  max-width: 100%;
  height: auto;
}

.image-rounded {
  border-radius: 0.5rem;
}

5.2. Component với Fonts và Images

// app/components/Hero.tsx
import Image from 'next/image'

export default function Hero() {
  return (
    <section className="hero">
      <div className="hero-content">
        <h1 className="font-heading text-4xl font-bold">
          Welcome to Our Platform
        </h1>
        <p className="font-body text-lg">
          Discover amazing features and boost your productivity
        </p>
      </div>
      <div className="hero-image">
        <Image
          src="/images/hero-illustration.svg"
          alt="Hero Illustration"
          width={600}
          height={400}
          priority
          className="image-rounded"
        />
      </div>
    </section>
  );
}

6. Performance và Best Practices

6.1. Image Performance Checklist

// ✅ Tốt: Optimized image usage
<Image
  src="/images/hero.jpg"
  alt="Hero Image"
  width={1920}
  height={1080}
  quality={85}
  priority={true}
  placeholder="blur"
/>

// ❌ Tránh: Unoptimized images
<img src="/images/hero.jpg" alt="Hero" />

// ❌ Tránh: Missing dimensions
<Image src="/images/hero.jpg" alt="Hero" />

6.2. Font Performance

// ✅ Tốt: Optimized font loading
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
  preload: true,
})

// ❌ Tránh: Unoptimized fonts
const inter = Inter({
  // Missing subsets
  // Missing display strategy
  // No preloading
})

6.3. Asset Organization

// ✅ Tốt: Organized structure
public/
├── images/
│   ├── optimized/
│   ├── placeholders/
│   └── originals/
├── fonts/
│   ├── webfonts/
│   └── variable/
└── assets/
    ├── css/
    ├── js/
    └── vendor/

// ❌ Tránh: Disorganized
public/
├── logo.png
├── banner.jpg
├── font1.woff2
└── style.css

7. Common Patterns và Solutions

// app/components/ImageGallery.tsx
import Image from 'next/image'
import { useState } from 'react'

export default function ImageGallery({ images }) {
  const [loaded, setLoaded] = useState<{[key: string]: boolean}>({})
  
  return (
    <div className="gallery">
      {images.map((image) => (
        <div key={image.id} className="gallery-item">
          {!loaded[image.id] && (
            <div className="image-skeleton">Loading...</div>
          )}
          <Image
            src={image.src}
            alt={image.alt}
            width={400}
            height={300}
            className={`gallery-image ${loaded[image.id] ? 'loaded' : ''}`}
            onLoad={() => setLoaded(prev => ({ ...prev, [image.id]: true }))}
          />
        </div>
      ))}
    </div>
  );
}

7.2. Responsive Background Images

// app/components/HeroSection.tsx
import Image from 'next/image'

export default function HeroSection() {
  return (
    <div className="hero-container">
      <Image
        src="/images/hero-background.jpg"
        alt="Hero Background"
        fill
        className="hero-background"
        sizes="100vw"
        priority
      />
      <div className="hero-content">
        <h1>Welcome to Our Platform</h1>
        <p>Amazing experiences await you</p>
      </div>
    </div>
  );
}

8. Bài tập thực hành

Bài tập 1: Portfolio Website

Tạo portfolio với:

  • Hero section với background image
  • Gallery với optimized images
  • Custom fonts cho heading và body
  • Loading states cho images

Bài tập 2: E-commerce Product Page

Tạo product page với:

  • Product image gallery
  • Zoom functionality
  • Responsive image sizing
  • Optimized loading

Bài tập 3: Blog với Typography

Tạo blog layout với:

  • Custom fonts cho different heading levels
  • Optimized featured images
  • Responsive image sizing
  • Font loading optimization

9. Kết luận bài học

  • Static Assets: Tổ chức trong thư mục public/ với cấu trúc rõ ràng
  • Image Component: Tự động tối ưu với lazy loading, responsive sizing
  • Font Optimization: Giảm CLS và cải thiện performance với next/font
  • Performance: Luôn optimize images và fonts để có UX tốt nhất

🔑 GHI NHỚ QUAN TRỌNG:

  • Luôn sử dụng <Image> component cho tất cả images
  • Cấu hình domains trong next.config.js cho remote images
  • Dùng next/font để optimize font loading
  • Tổ chức assets với cấu trúc rõ ràng trong public/
18 bài học
Bài 6
Tiến độ hoàn thành 33%

Đã hoàn thành 6/18 bài học