Files
2025-11-30 08:48:52 +08:00

512 lines
11 KiB
Markdown

# 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):
```tsx
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
```tsx
<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
```tsx
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
```js
// 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:
```tsx
// 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:
```css
.code {
font-family: var(--font-roboto-mono);
}
```
### Local Fonts
```tsx
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
```tsx
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:
```tsx
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
```bash
# Install bundle analyzer
npm install @next/bundle-analyzer
# Create next.config.js wrapper
```
```js
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// Your Next.js config
})
```
```bash
# Run analysis
ANALYZE=true npm run build
```
### Dynamic Import (Code Splitting)
Split code and load on-demand:
```tsx
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:
```tsx
const DynamicComponent = dynamic(
() => import('@/components/hello').then(mod => mod.Hello)
)
```
Multiple components:
```tsx
const DynamicHeader = dynamic(() => import('@/components/header'))
const DynamicFooter = dynamic(() => import('@/components/footer'))
```
### Tree Shaking
Import only what you need:
```tsx
// ❌ 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.
```js
// next.config.js
module.exports = {
experimental: {
ppr: true,
}
}
```
```tsx
// 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
```tsx
// 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
```tsx
// 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:
```tsx
// 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)