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
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
7.1. Image Gallery với Loading States
// 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.jscho remote images- Dùng
next/fontđể optimize font loading- Tổ chức assets với cấu trúc rõ ràng trong
public/