404 Not Found

404 Not Found


nginx

Image Optimization and Font Loading: Improving Core Web Vitals

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


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, and next/font to eliminate font blocking.

TSX
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
100%
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

TS
// 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
⚠️ Note: 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

TSX
// 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"
    />
  )
}
💡 Tip: 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

TSX
// ✅ 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

TEXT
# Usage: plaiceholder library
npm install plaiceholder
TS
// 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

TSX
// 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>
  )
}

5. next/font Font Optimization

(1) The Impact of Font Loading on Performance

100%
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

TSX
// 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)

TSX
// 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

TSX
// 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>
  )
}
CSS
/* 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

100%
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

TS
// 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

TSX
// 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>
  )
}
💡 Tip: The difference between 75% and 100% is almost imperceptible to the naked eye, but the volume differs by a factor of 3 to 4. For production environments, 75–85% is recommended.


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

TSX
// ✅ 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

TSX
// 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']
})
🔥 Common Mistake: Do not load more than 3 font weights on the same page. Each variant adds approximately 50 KB to the WOFF2 file.

▶ Example: CWV Monitoring Component

TSX
// 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
}
TSX
// 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

TSX
// 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>
  )
}
TSX
// 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

Q What is the difference between next/image and fill in width/height?
A 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.
Q How do I generate a blurred image for placeholder="blur"?
A Next.js automatically generates it during static import. For remote images, you need to manually provide 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).
Q Are display=swap and next/font in Google Fonts the same as display:swap?
A They have the same effect but are implemented differently. The traditional method involves adding &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.
Q Why isn’t my LCP image being preloaded automatically?
A Check whether the 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.
Q Which should I choose, AVIF or WebP?
A Configure both and let the browser automatically choose using the <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


📝 Exercises

  1. Basic Exercise (⭐): Configure next/image in the project, use remotePatterns to enable the Unsplash image source, and implement a responsive image component (including sizes and placeholder="blur").

  2. 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.

  3. Challenge (⭐⭐⭐): Build a complete CWV optimization solution: next/font configure two variable fonts (body text font + code font), configure LCP images priority + blur placeholders, useReportWebVitals report metrics to a custom API, and ultimately achieve a Lighthouse Performance score of ≥ 90.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏