Image & Font Optimization
Images and fonts account for more than 70% of a page's size—optimizing them can reduce LCP from 4 seconds to 1 second.
1. What You'll Learn
- The core attributes of the component (
next/image/<Image>/remotePatterns/sizes/priority/placeholder) - Image Optimization Principles: Sharp server-side re-encoding, WebP / AVIF adaptive encoding
next/fontFont optimization (Google Web Fonts,display:swap,size-adjust, preloading)- Performance Optimization Strategies for Core Web Vitals (LCP / CLS / INP)
- Remote Image Security and
images.domainsDeprecation and Migration
2. A True Story of a Front-End Performance Engineer
(1) Pain Point: A Single Image Can Ruin the Performance of an Entire Page
Diana is a DevOps engineer on the TaskFlow team. She discovered that the LCP (Largest Contentful Paint) for the Dashboard page was as high as 4.8 seconds:
"The banner at the top of the page is a 5MB PNG image (original size 4000×3000px), which is loaded directly
<img src="/banner.png" />. Chrome DevTools shows: decoding took 800 ms, Cumulative Layout Shift (CLS) was 0.45, and font loading blocked rendering for 600 ms."
| Issue | Impact | CWV Metric |
|---|---|---|
| Original image 5MB uncompressed | Download 3.2s | ❌ LCP 4.8s |
| No dimension properties | Page layout keeps shifting | ❌ CLS 0.45 |
| Font rendering blockage | Extended white screen | ❌ FCP 2.1s |
| Non-responsive images | 4K images load on mobile devices too | ❌ Wastes data |
(2) The next/image + next/font Solution
Use the
<Image>component to automatically optimize images, andnext/fontto eliminate font blocking.
import Image from 'next/image'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], display: 'swap' })
export default function Hero() {
return (
<div className={inter.className}>
<Image
src="/banner.webp"
alt="TaskFlow Banner"
width={1200}
height={400}
priority
placeholder="blur"
blurDataURL="data:image/webp;base64,..."
/>
</div>
)
}
(3) Revenue
| Metric | Before Optimization | After Optimization | Improvement |
|---|---|---|---|
| LCP | 4.8s | 1.2s | 75% ↓ |
| CLS | 0.45 | 0.02 | 96% ↓ |
| Image Size | 5 MB | 120 KB | 97% ↓ |
| FCP | 2.1s | 0.8s | 62% ↓ |
3. Core Properties of the next/image Component
(1) Property Reference Table
| Property | Type | Required | Description |
|---|---|---|---|
src |
string / StaticImport | ✅ | Image path or static import |
width |
number | ✅ (static) | Image width (px) |
height |
number | ✅ (static) | Image height (px) |
alt |
string | ✅ | Alternative text (accessibility) |
priority |
boolean | ❌ | LCP Image Preloading |
placeholder |
'blur' / 'empty' |
❌ | Loading placeholder |
blurDataURL |
string | Requires blur |
base64 blur placeholder |
sizes |
string | ❌ | Responsive breakpoint |
fill |
boolean | ❌ | Fill parent container |
quality |
number | ❌ | Compression Quality (1–100) |
loading |
'lazy' / 'eager' |
❌ | Lazy Loading Strategy |
graph TB
A[<Image src="/photo.jpg"/>] --> B{Next.js Build}
B --> C[Sharp Server-Side Recoding]
C --> D[WebP Version<br/>1200w / 800w / 400w]
C --> E[AVIF Version<br/>1200w / 800w / 400w]
D --> F[Browser Selection<br/><picture> Auto-Adjust]
E --> F
style B fill:#cce5ff
style C fill:#d4edda
style F fill:#fff3cd
(2) Example of responsive configuration
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com' },
{ protocol: 'https', hostname: 'cdn.taskflow.io', port: '', pathname: '/assets/**' }
],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384]
}
}
export default nextConfig
images.domains has been deprecated in Next.js 15 and later; please use remotePatterns (which supports wildcard path matching).
▶ Example: Remote Image + Responsive + Blur Placeholder
Output:
TypeScript module executes successfully.
// components/TeamPhoto.tsx
import Image from 'next/image'
export default function TeamPhoto() {
return (
<Image
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200"
alt="TaskFlow Team Photo"
width={1200}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 1200px"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
priority
className="rounded-lg"
/>
)
}
Output:
Renders the TeamPhoto component UI.
sizes tells the browser the display width of an image under different viewports, helping the browser select the most appropriate image size. If this is not set, mobile devices may end up loading the 1920px image as well.
4. placeholder="blur" and blurDataURL
(1) Two Placeholder Schemes
| Solution | Generation Method | Volume | Applicable Scenarios |
|---|---|---|---|
| Static import of "blur" | Generated automatically by Next.js | ~2KB | Local image (import img from './photo.jpg') |
| Manual blurDataURL | Tool-generated base64 | ~200B | Remote images, animated images |
| Placeholder Library | Generated at runtime | ~500B | Requires a dynamic remote URL |
(2) Static import automatically blurs
// ✅ Static Import — Next.js Automatically Generated blurDataURL
import teamPhoto from '@/public/team.jpg'
export default function AboutPage() {
return (
<Image
src={teamPhoto}
alt="Team Photo"
placeholder="blur" // Automatically use the generated blur
className="rounded-xl"
/>
)
}
(3) Tool for Generating Blurred Previews of Remote Images
# Usage: plaiceholder library
npm install plaiceholder
// lib/getBlurData.ts — Remote Image blurURL Generate
import { getPlaiceholder } from 'plaiceholder'
export async function getBlurDataURL(src: string) {
try {
const response = await fetch(src)
const buffer = Buffer.from(await response.arrayBuffer())
const { base64 } = await getPlaiceholder(buffer)
return base64
} catch {
return undefined
}
}
▶ Example: Dynamic remote image + blur placeholder
Output:
TypeScript code executed successfully.
// app/team/page.tsx — List of Team Avatars + blur Placeholder
import Image from 'next/image'
import { getBlurDataURL } from '@/lib/getBlurData'
const members = [
{ name: 'Alice', avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200' },
{ name: 'Bob', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200' },
]
export default async function TeamPage() {
const membersWithBlur = await Promise.all(
members.map(async (m) => ({ ...m, blur: await getBlurDataURL(m.avatar) }))
)
return (
<div style={{ display: 'flex', gap: '1rem' }}>
{membersWithBlur.map((m) => (
<Image
key={m.name}
src={m.avatar}
alt={m.name}
width={100}
height={100}
placeholder="blur"
blurDataURL={m.blur}
className="rounded-full"
/>
))}
</div>
)
}
Output:
Renders a list by mapping over membersWithBlur, displaying each m.
5. next/font Font Optimization
(1) The Impact of Font Loading on Performance
graph TB
A[Traditional @font-face] --> B[Blocking Rendering<br/>FOIT]
A --> C[Layout Offset<br/>FOUT]
B --> D[FCP Delayed 300-600ms]
C --> E[CLS 0.1-0.3]
F[next/font] --> G[display:swap<br/>Use the fallback font immediately]
F --> H[size-adjust<br/>Eliminate Layout Offsets]
F --> I[preload Preload<br/>Critical Path Without Blocking]
F --> J[CSS size-adjust<br/>Typeface Metric Overlap]
style A fill:#f8d7da
style F fill:#d4edda
| Issue | Traditional Font Loading | next/font Solution |
|---|---|---|
| FOIT (font not visible) | Text is not displayed until the font is downloaded | display:swap Display fallback font immediately |
| CLS (Cumulative Layout Shift) | Size changes before and after font loading | size-adjust Overrides font metrics |
| Additional Requests | Serialized download of multiple font files | Inline CSS + Preloading |
| Google Fonts Delay | Slow cross-border CDN requests | Downloaded during build, zero runtime requests |
(2) Google Variable Fonts
// app/layout.tsx — Google Variable Fonts
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
preload: true,
variable: '--font-inter', // CSS Variable Patterns
weight: 'variable' // Variable Font Range
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono'
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh" className={`${inter.variable} ${robotoMono.variable}`}>
<body style={{ fontFamily: 'var(--font-inter)' }}>
<code style={{ fontFamily: 'var(--font-roboto-mono)' }}>{children}</code>
</body>
</html>
)
}
(3) Font Size Adjustment (size-adjust)
// Custom Local Fonts + size-adjust for CLS prevention
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/CustomFont.woff2',
display: 'swap',
adjustment: {
ascent: 90,
descent: 20,
lineGap: 10,
sizeAdjust: '105%'
}
})
▶ Example: Combining Multiple Fonts + Tailwind CSS
Output:
Renders the Component component UI.
// app/layout.tsx — Business Fonts + Code Font + Tailwind
import { Inter, JetBrains_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-sans',
display: 'swap'
})
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
display: 'swap'
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}
Output:
Renders the RootLayout component UI.
/* tailwind.config.ts */
import type { Config } from 'tailwindcss'
export default {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)'],
mono: ['var(--font-mono)']
}
}
}
} satisfies Config
6. Principles of Image Optimization: Sharp + WebP/AVIF
(1) Sharp Pipeline Optimization
graph LR
A[Source image<br/>PNG/JPEG 5MB] --> B[Sharp Server-side processing]
B --> C[Resize<br/>1200w / 800w / 400w]
B --> D[Quality Compression<br/>75% quality]
B --> E[Format Conversion]
E --> F[WebP<br/>~80% Volume↓]
E --> G[AVIF<br/>~90% Volume↓]
F --> H[<picture> Auto-negotiation<br/>Browser: Select the Best Format]
style B fill:#d4edda
style H fill:#cce5ff
| Format | Compression Ratio (vs. JPEG) | Browser Support | Decoding Speed |
|---|---|---|---|
| JPEG | Benchmark | 100% | Fastest |
| WebP | ~30% smaller file size | 96% | Fairly fast |
| AVIF | ~50% reduction in file size | 93% | Slower |
| HEIC | ~50% smaller file size | Safari only | Faster |
(2) Configure Global Image Quality
// next.config.ts — Global Image Optimization Settings
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 Sky Cache
dangerouslyAllowSVG: false,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
}
}
▶ Example: Image Quality and Size Test
Output:
TypeScript module executes successfully.
// components/ImageTest.tsx — Comparison of Different Quality Levels for the Same Image
import Image from 'next/image'
export default function ImageTest() {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
<div>
<h3>Quality=50</h3>
<Image src="/test.jpg" alt="q50" width={400} height={300} quality={50} />
</div>
<div>
<h3>Quality=75(Default)</h3>
<Image src="/test.jpg" alt="q75" width={400} height={300} quality={75} />
</div>
<div>
<h3>Quality=100</h3>
<Image src="/test.jpg" alt="q100" width={400} height={300} quality={100} />
</div>
</div>
)
}
Output:
Renders: Quality=50
Visible text: Quality=50 | Quality=75(Default) | Quality=100
7. Practical Optimization of Core Web Vitals
(1) The Three Key Metrics of CWV
| Metric | Maximum Score | Gap | Optimization Measures |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤2.5s | >4.0s | Preload LCP images + compression + CDN |
| CLS (Cumulative Layout Shift) | ≤0.1 | >0.25 | Fixed image dimensions + font size adjustment |
| INP (Interaction to Next Paint) | ≤200 ms | >500 ms | Reduce JS execution + code splitting |
(2) LCP Image Optimization Checklist
// ✅ Correct LCP Image Settings
export default function HeroBanner() {
return (
<Image
src="/hero-banner.webp"
alt="TaskFlow Hero"
width={1440}
height={600}
priority // Tell Next.js Preload this image
quality={85}
placeholder="blur"
sizes="100vw"
style={{ width: '100%', height: 'auto' }} // Responsive
/>
)
}
(3) Eliminating Font CLS
// layout.tsx — Font CLS Complete Solution for Elimination
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
adjustFontFallback: true, // Next.js Automatic Calculation size-adjust
fallback: ['system-ui', 'sans-serif']
})
▶ Example: CWV Monitoring Component
Output:
Renders the Component component UI.
// components/WebVitals.tsx — Submit CWV Go to the analytics platform
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export default function WebVitalsReporter() {
useReportWebVitals((metric) => {
console.log(metric) // View during development
// Reporting from the Production Environment to Analytics API
if (process.env.NODE_ENV === 'production') {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id
})
navigator.sendBeacon('/api/analytics', body)
}
})
return null
}
Output:
Renders the WebVitalsReporter component UI.
// app/layout.tsx — Mount Monitoring
import WebVitalsReporter from '@/components/WebVitals'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<WebVitalsReporter />
</body>
</html>
)
}
8. Complete Example: Comprehensive Optimization of Homepage Images, Fonts, and CWV
// app/layout.tsx — Root Layout,Global Fonts + Image + Vitals Surveillance
import { Inter, Noto_Sans_SC } from 'next/font/google'
import WebVitalsReporter from '@/components/WebVitals'
import type { Metadata } from 'next'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
adjustFontFallback: true
})
const notoSansSC = Noto_Sans_SC({
subsets: ['latin'],
weight: 'variable',
variable: '--font-noto',
display: 'swap',
adjustFontFallback: true
})
export const metadata: Metadata = {
title: 'TaskFlow - Team Collaboration Platform',
description: 'TaskFlow Help 10,000+ Highly Effective Team Collaboration'
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh" className={`${inter.variable} ${notoSansSC.variable}`}>
<body style={{ fontFamily: 'var(--font-inter), var(--font-noto), sans-serif' }}>
{children}
<WebVitalsReporter />
</body>
</html>
)
}
// app/page.tsx — Home Hero + LCP Optimization
import Image from 'next/image'
import Link from 'next/link'
export default function HomePage() {
return (
<div>
{/* Hero Region — LCP Image */}
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<Image
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1920&q=85"
alt="TaskFlow Teamwork"
fill
priority
sizes="100vw"
style={{ objectFit: 'cover' }}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ textAlign: 'center', color: '#fff' }}>
<h1 style={{ fontSize: 'clamp(2rem, 5vw, 4rem)' }}>TaskFlow</h1>
<p style={{ fontSize: '1.25rem' }}>10,000+ The collaboration platform the team is currently using</p>
<Link href="/signup" style={{ display: 'inline-block', padding: '0.75rem 2rem', background: '#4f46e5', color: '#fff', borderRadius: 8 }}>
Get Started for Free
</Link>
</div>
</div>
</div>
{/* Feature Region — Lazy Loading of Images */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '2rem', padding: '4rem 2rem' }}>
{features.map((feature) => (
<div key={feature.title}>
<Image
src={feature.icon}
alt={feature.title}
width={48}
height={48}
loading="lazy"
/>
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
))}
</div>
</div>
)
}
const features = [
{ title: 'Project Management', description: 'Bulletin Board + Gantt Chart', icon: '/icons/project.svg' },
{ title: 'Real-Time Collaboration', description: 'Simultaneous Editing by Multiple Users', icon: '/icons/team.svg' },
{ title: 'Intelligent Analysis', description: 'AI Insights Driven by', icon: '/icons/analytics.svg' },
]
❓ FAQ
next/image and fill in width/height?fill causes the image to fill its parent container (which must have position: relative) without specifying a fixed size; it is used in conjunction with objectFit. Static width and height require explicitly specifying the image dimensions to calculate the space it occupies and prevent CLS.placeholder="blur"?blurDataURL (a base64-encoded blurred thumbnail), which can be generated using the plaiceholder library or an online tool (such as https://png-pixel.com to generate a 4x4 blurred image).display=swap and next/font in Google Fonts the same as display:swap?&display=swap to the <link> tag and replacing it only after the font has finished downloading. next/font downloads the font file during build time and inlines it into the page via CSS, resulting in zero runtime requests, while also automatically generating size-adjust to eliminate CLS.priority attribute is set on the LCP image. Next.js only adds <link rel="preload"> to images that have priority. Additionally, incorrect remotePatterns configuration can also cause the optimization to fail.<picture> tag. AVIF offers higher compression but is slower to decode, making it suitable for non-LCP images; WebP has better compatibility (96%) and is suitable for LCP images. Next.js generates both formats by default.📖 Summary
next/imageOffers automatic optimization: Sharp re-encoding, WebP/AVIF adaptive encoding, responsive multi-size images, and lazy loadingremotePatternsReplaces the deprecatedimages.domainsand supports secure matching of path wildcardsplaceholder="blur"Combined withblurDataURLto eliminate blank flickering when images loadnext/fontDownload Google Fonts during build to eliminate FOIT and FOUT, and automatically adjust font metrics to prevent CLS- LCP images must be set to
priorityfor preloading; non-above-the-fold images should useloading="lazy"for lazy loading - Core Web Vitals Optimization: Image Compression (LCP), Fixed Dimensions (CLS), and JavaScript Code Splitting (INP)
📝 Exercises
-
Basic Exercise (⭐): Configure
next/imagein the project, useremotePatternsto enable the Unsplash image source, and implement a responsive image component (includingsizesandplaceholder="blur"). -
Advanced Exercise (⭐⭐): Compare the impact of different image qualities (50/75/100) and formats (JPEG/WebP/AVIF) on the page’s LCP, record the file size and load time for each combination, and determine the optimal configuration.
-
Challenge (⭐⭐⭐): Build a complete CWV optimization solution:
next/fontconfigure two variable fonts (body text font + code font), configure LCP imagespriority+blurplaceholders,useReportWebVitalsreport metrics to a custom API, and ultimately achieve a Lighthouse Performance score of ≥ 90.



