프롤로그 - 3MB 블로그 포스트의 비극
블로그 글 하나가 3MB였다. 글자는 겨우 몇 KB인데, 나머지는 전부 이미지였다. 스크린샷 5장, PNG로 저장했더니 하나당 500KB씩. 모바일에서 열면 로딩 바가 한참 돌아간다.
"이미지 용량 줄이면 되겠네?" 생각하고 포토샵으로 품질 낮췄더니 글씨가 뭉개졌다. JPEG로 바꾸니 투명 배경이 사라졌다. 결국 이거였다 - 포맷을 제대로 선택하고, 상황에 맞게 크기를 제공해야 한다는 것.
이미지 최적화는 레시피 같았다. 재료(포맷)를 고르고, 조리법(압축)을 정하고, 접시 크기(해상도)를 맞추는 것. 이 글은 내가 3MB 블로그를 300KB로 줄이면서 배운 실제 레시피다.
Aha Moment: JPEG가 답이 아니었다
이미지 포맷, 뭐가 다른가
처음엔 단순했다. "사진은 JPEG, 로고는 PNG" 이게 전부인 줄 알았다. 그런데 2024년에도 이렇게 쓰면 손해다. WebP와 AVIF라는 신세대 포맷이 있기 때문이다.
같은 이미지를 네 가지 포맷으로 저장하면 이런 차이가 났다:
- PNG (원본): 847KB - 투명 배경, 무손실, 그러나 거대함
- JPEG (품질 80): 312KB - 63% 감소, 하지만 투명 배경 사라짐
- WebP (품질 80): 156KB - JPEG보다 50% 더 작음, 투명 배경 유지
- AVIF (품질 65): 98KB - PNG 대비 88% 감소, 화질은 거의 동일
이건 마치 같은 물건을 배송하는데, 택배 박스 크기를 1/8로 줄인 것과 같았다. 내용물은 똑같은데 배송비(네트워크 비용)가 압도적으로 줄어든다.
실제 프로젝트에서 본 충격
블로그 커버 이미지 하나가 있었다. 1200×630 크기의 PNG로 1.2MB였다. 이걸 변환했더니:
# Sharp 라이브러리로 변환
npx sharp-cli -i cover.png -o cover.webp --webp '{"quality":85}'
npx sharp-cli -i cover.png -o cover.avif --avif '{"quality":70}'
# 결과
# cover.png: 1,234KB
# cover.webp: 287KB (77% 감소)
# cover.avif: 142KB (88% 감소)
88% 감소. 이건 그냥 숫자가 아니었다. 사용자가 3G 환경에서 글을 열면 이미지 로딩 시간이 12초에서 1.5초로 줄어든다는 뜻이었다.
포맷 선택 기준이 보였다
결국 이렇게 정리됐다:
- 사진, 복잡한 그래픽: AVIF 1순위, WebP 2순위
- 스크린샷, 텍스트 포함: WebP (AVIF는 텍스트에서 약간 뭉개짐)
- 투명 배경 필요: WebP 또는 PNG (JPEG 금지)
- 브라우저 지원 걱정:
<picture>태그로 폴백 제공
이건 도구가 아니라 전략이었다. 상황에 맞는 포맷을 고르는 것.
Deep Dive: 최적화 가이드
1. Next.js Image 컴포넌트: 자동 최적화의 힘
Next.js를 쓰면 이미지 최적화가 절반은 자동으로 된다. <img> 태그를 <Image>로 바꾸기만 하면 된다.
// Before: 수동 관리 지옥
<img
src="/hero.png"
alt="Hero image"
style={{width: '100%', height: 'auto'}}
/>
// After: Next.js가 알아서 처리
import Image from 'next/image'
<Image
src="/hero.png"
alt="Hero image"
width={1200}
height={630}
priority // LCP 최적화: 즉시 로딩
quality={85}
placeholder="blur" // 로딩 중 블러 처리
/>
Next.js Image는 이런 걸 자동으로 한다:
- 포맷 변환: 브라우저가 WebP/AVIF를 지원하면 자동 변환
- 반응형 크기: srcset 자동 생성 (320w, 640w, 1200w...)
- 지연 로딩: 뷰포트에 들어올 때만 로딩
- 캐싱: CDN에 최적화된 이미지 캐싱
처음엔 "이게 어떻게 자동으로 되지?" 싶었는데, 개발자 도구로 보니 이렇게 변환되어 있었다:
<img
srcset="
/_next/image?url=/hero.png&w=640&q=85 640w,
/_next/image?url=/hero.png&w=1200&q=85 1200w,
/_next/image?url=/hero.png&w=2400&q=85 2400w
"
sizes="(max-width: 640px) 100vw, 1200px"
src="/_next/image?url=/hero.png&w=2400&q=85"
type="image/webp"
/>
이건 마치 주문할 때 "사이즈 어떻게 해드릴까요?"라고 물어보고, 고객 화면 크기에 딱 맞춰서 서빙하는 것과 같았다.
2. srcset과 sizes: 반응형 이미지의 핵심
Next.js 없이 순수 HTML로 하려면 srcset과 sizes를 이해해야 한다.
<!-- 잘못된 방법: 모든 기기에 동일한 큰 이미지 -->
<img src="/product-2400w.jpg" alt="Product">
<!-- 올바른 방법: 기기별 최적 크기 제공 -->
<img
srcset="
/product-400w.webp 400w,
/product-800w.webp 800w,
/product-1200w.webp 1200w,
/product-2400w.webp 2400w
"
sizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
33vw
"
src="/product-1200w.webp"
alt="Product"
/>
srcset: 브라우저에게 선택지를 주는 메뉴판 sizes: "이 상황에선 이 크기가 필요해"라는 힌트 브라우저: 화면 크기와 DPI를 보고 최적 이미지 선택
모바일(375px 화면)에선 400w 이미지를, 데스크톱(1920px)에선 2400w 이미지를 가져간다. 같은 HTML인데 상황에 맞게 다르게 작동하는 거였다.
3. Picture 엘리먼트: 포맷 폴백의 예술
AVIF가 제일 작지만, 오래된 브라우저는 지원 안 한다. 이럴 때 <picture> 태그로 우아하게 대응한다.
<picture>
<!-- 1순위: AVIF (최신 브라우저) -->
<source
type="image/avif"
srcset="/hero-800.avif 800w, /hero-1200.avif 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
/>
<!-- 2순위: WebP (대부분의 브라우저) -->
<source
type="image/webp"
srcset="/hero-800.webp 800w, /hero-1200.webp 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
/>
<!-- 3순위: JPEG (모든 브라우저) -->
<img
src="/hero-1200.jpg"
srcset="/hero-800.jpg 800w, /hero-1200.jpg 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
alt="Hero image"
loading="lazy"
/>
</picture>
브라우저는 위에서부터 확인한다:
- AVIF 지원? → AVIF 로딩
- 안 되면 WebP 지원? → WebP 로딩
- 둘 다 안 되면? → JPEG 로딩
이건 식당 메뉴 같았다. "오늘의 특선(AVIF) 있으면 그걸로, 없으면 시그니처(WebP), 그것도 없으면 기본 메뉴(JPEG)."
4. Sharp: 서버에서 이미지 가공하기
블로그 포스트가 50개쯤 되니 수동으로 변환하기 힘들었다. Sharp 라이브러리로 배치 처리했다.
// scripts/optimize-images.ts
import sharp from 'sharp';
import fs from 'fs/promises';
import path from 'path';
async function optimizeImage(inputPath: string, outputDir: string) {
const filename = path.parse(inputPath).name;
// WebP 변환 (품질 85)
await sharp(inputPath)
.webp({ quality: 85 })
.toFile(path.join(outputDir, `${filename}.webp`));
// AVIF 변환 (품질 70, 더 좋은 압축률)
await sharp(inputPath)
.avif({ quality: 70, effort: 6 })
.toFile(path.join(outputDir, `${filename}.avif`));
// 반응형 크기 생성
const sizes = [400, 800, 1200, 2400];
for (const size of sizes) {
await sharp(inputPath)
.resize(size, null, { withoutEnlargement: true })
.webp({ quality: 85 })
.toFile(path.join(outputDir, `${filename}-${size}w.webp`));
}
console.log(`✓ Optimized: ${filename}`);
}
// 사용
const images = await fs.readdir('./public/images/blog');
for (const img of images) {
if (img.match(/\.(png|jpg|jpeg)$/i)) {
await optimizeImage(
`./public/images/blog/${img}`,
'./public/images/blog/optimized'
);
}
}
이 스크립트 하나로 50개 이미지가 10분 만에 변환됐다. 원본 PNG 보관하고, WebP/AVIF + 4가지 사이즈를 자동 생성.
5. Lazy Loading과 LCP 최적화
모든 이미지를 한 번에 로딩하면 초기 로딩이 느려진다. 화면에 보이는 것만 먼저 로딩하는 게 핵심이다.
// Hero 이미지: 즉시 로딩 (LCP 최적화)
<Image
src="/hero.webp"
alt="Hero"
width={1200}
height={630}
priority // fetchpriority="high"로 변환됨
loading="eager"
/>
// 스크롤해야 보이는 이미지: 지연 로딩
<Image
src="/content-image.webp"
alt="Content"
width={800}
height={600}
loading="lazy" // 뷰포트 근처 올 때만 로딩
/>
// 더 세밀한 제어
<Image
src="/below-fold.webp"
alt="Below fold"
width={600}
height={400}
loading="lazy"
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KG..." // 작은 블러 이미지
/>
LCP (Largest Contentful Paint): 가장 큰 콘텐츠가 로딩되는 시간. 보통 히어로 이미지가 LCP다. 이걸 priority로 설정하면 다른 리소스보다 먼저 로딩된다.
실제로 측정해보니:
- 모든 이미지 즉시 로딩: LCP 3.2초
- Hero만 priority, 나머지 lazy: LCP 1.1초
3배 차이가 났다.
6. CDN 기반 자동 최적화
Cloudflare나 imgix 같은 CDN을 쓰면 아예 URL 파라미터로 최적화할 수 있다.
// Cloudflare Polish (자동)
// https://example.com/image.jpg
// → 자동으로 WebP/AVIF 변환, 압축
// imgix (URL 파라미터)
const imgixUrl = (src: string, options: {
w?: number;
h?: number;
fit?: 'crop' | 'scale';
fm?: 'webp' | 'avif' | 'jpg';
q?: number;
}) => {
const params = new URLSearchParams(options as any);
return `https://yourproject.imgix.net${src}?${params}`;
};
// 사용
<img
src={imgixUrl('/hero.jpg', { w: 1200, fm: 'webp', q: 85 })}
srcset={`
${imgixUrl('/hero.jpg', { w: 640, fm: 'webp', q: 85 })} 640w,
${imgixUrl('/hero.jpg', { w: 1200, fm: 'webp', q: 85 })} 1200w
`}
/>
이건 마법 같았다. 원본 하나만 업로드하면 CDN이 알아서 포맷 변환, 리사이즈, 압축을 해준다. 서버에서 미리 변환할 필요 없이 실시간으로.
7. Art Direction: 화면마다 다른 이미지
때로는 크기만 바꾸는 게 아니라 완전히 다른 이미지를 보여줘야 한다. 가로 이미지는 모바일에서 너무 작아지니까.
<picture>
<!-- 모바일: 세로 크롭 버전 -->
<source
media="(max-width: 640px)"
srcset="/hero-mobile-portrait.webp"
/>
<!-- 태블릿: 정사각형 크롭 -->
<source
media="(max-width: 1024px)"
srcset="/hero-tablet-square.webp"
/>
<!-- 데스크톱: 가로 원본 -->
<img
src="/hero-desktop-landscape.webp"
alt="Hero image"
/>
</picture>
예를 들어 제품 사진이 있다면:
- 데스크톱: 제품 + 배경 전체 (16:9)
- 태블릿: 제품 중심으로 크롭 (4:3)
- 모바일: 제품만 클로즈업 (3:4)
같은 이미지인데 화면마다 다른 버전을 제공하는 거였다.
정리: 이미지 최적화 체크리스트
6개월간 블로그, 랜딩 페이지, 대시보드를 만들면서 매번 쓰는 체크리스트다:
포맷 선택
- 사진/복잡한 그래픽 → AVIF 우선, WebP 폴백
- 스크린샷/텍스트 → WebP
- 투명 배경 필요 → WebP 또는 PNG
-
<picture>로 폴백 체인 구성
크기 최적화
- srcset으로 최소 3가지 크기 제공 (400w, 800w, 1200w)
- sizes 속성으로 브라우저에 힌트 제공
- 레티나 디스플레이 고려 (2x, 3x)
- 불필요하게 큰 원본 사용 금지 (2400px 이상은 특수 목적만)
로딩 전략
- Hero/LCP 이미지 →
priority또는loading="eager" - Below-the-fold 이미지 →
loading="lazy" - placeholder 추가 (blur 또는 색상)
- 중요 이미지는 preload 고려
도구 활용
- Next.js 쓴다면 Image 컴포넌트 사용
- 배치 변환은 Sharp 스크립트
- CDN 있으면 자동 최적화 활성화
- 빌드 시 이미지 최적화 자동화
측정
- Lighthouse로 LCP 측정 (2.5초 이하 목표)
- Network 탭에서 실제 전송 크기 확인
- PageSpeed Insights로 필드 데이터 검증
- 3G 환경 시뮬레이션 테스트
팁
- 원본은 항상 보관 (Git LFS나 별도 스토리지)
- 자동화 스크립트로 휴먼 에러 방지
- 배포 전 이미지 최적화 CI 체크
- 모니터링으로 최적화 안 된 이미지 감지
에필로그: 숫자로 본 변화
이 모든 걸 적용하고 6개월 후:
Before
- 평균 페이지 크기: 2.8MB
- 이미지 비중: 82%
- LCP: 3.4초
- 모바일 Lighthouse: 62점
After
- 평균 페이지 크기: 420KB (85% 감소)
- 이미지 비중: 48%
- LCP: 1.2초 (64% 개선)
- 모바일 Lighthouse: 94점
사용자 반응도 달라졌다. "페이지 로딩이 빨라졌어요"라는 피드백이 왔다. 아무도 불평 안 했는데 개선하니 오히려 칭찬이 들어왔다.
이미지 최적화는 사용자를 존중하는 방식이었다. 3G 환경의 사용자, 데이터 제한 있는 사용자, 느린 기기 쓰는 사용자. 그들에게 불필요한 1MB를 보내지 않는 것. 결국 이거였다.
English Version
Prologue: The 3MB Blog Post Tragedy
One blog post weighed 3MB. The text was barely a few KB—everything else was images. Five screenshots saved as PNG, 500KB each. On mobile, the loading spinner would spin forever.
"Just reduce image quality?" I thought, and lowered quality in Photoshop. Text became blurry. Switched to JPEG, transparent backgrounds disappeared. The real answer was this: choose the right format and serve the right size for each situation.
Image optimization felt like cooking. Pick ingredients (formats), choose cooking methods (compression), match plate sizes (resolution). This post is the recipe I learned while shrinking a 3MB blog to 300KB.
Aha Moment: JPEG Wasn't the Answer
What Makes Image Formats Different
Initially, it was simple. "JPEG for photos, PNG for logos"—that was it. But using this in 2024 means leaving performance on the table. WebP and AVIF are the new generation formats.
Saving the same image in four formats showed dramatic differences:
- PNG (original): 847KB - transparent background, lossless, but huge
- JPEG (quality 80): 312KB - 63% reduction, but lost transparency
- WebP (quality 80): 156KB - 50% smaller than JPEG, kept transparency
- AVIF (quality 65): 98KB - 88% reduction from PNG, nearly identical quality
It's like shipping the same item but reducing the box size to 1/8th. Same contents, dramatically lower shipping costs (network transfer).
Real Project Impact
I had a blog cover image: 1200×630 PNG at 1.2MB. After conversion:
# Convert with Sharp library
npx sharp-cli -i cover.png -o cover.webp --webp '{"quality":85}'
npx sharp-cli -i cover.png -o cover.avif --avif '{"quality":70}'
# Results
# cover.png: 1,234KB
# cover.webp: 287KB (77% reduction)
# cover.avif: 142KB (88% reduction)
88% reduction. Not just a number—it meant image load time on 3G dropped from 12 seconds to 1.5 seconds.
Format Selection Strategy Emerged
It crystallized like this:
- Photos, complex graphics: AVIF first, WebP second
- Screenshots with text: WebP (AVIF can blur text slightly)
- Transparent backgrounds needed: WebP or PNG (never JPEG)
- Browser support concerns: Use
<picture>tag with fallbacks
This wasn't a tool—it was strategy. Choosing the right format for each situation.
Deep Dive: Practical Optimization Guide
1. Next.js Image Component: Automatic Optimization Power
With Next.js, half of image optimization happens automatically. Just replace <img> with <Image>.
// Before: manual management hell
<img
src="/hero.png"
alt="Hero image"
style={{width: '100%', height: 'auto'}}
/>
// After: Next.js handles it
import Image from 'next/image'
<Image
src="/hero.png"
alt="Hero image"
width={1200}
height={630}
priority // LCP optimization: load immediately
quality={85}
placeholder="blur" // blur while loading
/>
Next.js Image automatically:
- Format conversion: Auto-converts to WebP/AVIF if browser supports
- Responsive sizes: Auto-generates srcset (320w, 640w, 1200w...)
- Lazy loading: Loads only when entering viewport
- Caching: Optimized images cached on CDN
Initially I wondered "how does this work automatically?" DevTools revealed:
<img
srcset="
/_next/image?url=/hero.png&w=640&q=85 640w,
/_next/image?url=/hero.png&w=1200&q=85 1200w,
/_next/image?url=/hero.png&w=2400&q=85 2400w
"
sizes="(max-width: 640px) 100vw, 1200px"
src="/_next/image?url=/hero.png&w=2400&q=85"
type="image/webp"
/>
Like a restaurant asking "what size would you like?" and serving the perfect portion for each customer's screen.
2. srcset and sizes: Responsive Image Fundamentals
Without Next.js, you need to understand srcset and sizes for pure HTML.
<!-- Wrong: same large image for all devices -->
<img src="/product-2400w.jpg" alt="Product">
<!-- Right: optimal size per device -->
<img
srcset="
/product-400w.webp 400w,
/product-800w.webp 800w,
/product-1200w.webp 1200w,
/product-2400w.webp 2400w
"
sizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
33vw
"
src="/product-1200w.webp"
alt="Product"
/>
srcset: A menu of choices for the browser sizes: Hints like "in this situation, this size is needed" Browser: Picks optimal image based on screen size and DPI
Mobile (375px screen) fetches 400w, desktop (1920px) fetches 2400w. Same HTML, different behavior per context.
3. Picture Element: Format Fallback Artistry
AVIF is smallest, but older browsers don't support it. The <picture> tag handles this gracefully.
<picture>
<!-- Priority 1: AVIF (modern browsers) -->
<source
type="image/avif"
srcset="/hero-800.avif 800w, /hero-1200.avif 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
/>
<!-- Priority 2: WebP (most browsers) -->
<source
type="image/webp"
srcset="/hero-800.webp 800w, /hero-1200.webp 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
/>
<!-- Priority 3: JPEG (all browsers) -->
<img
src="/hero-1200.jpg"
srcset="/hero-800.jpg 800w, /hero-1200.jpg 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
alt="Hero image"
loading="lazy"
/>
</picture>
Browser checks from top:
- AVIF supported? → Load AVIF
- If not, WebP supported? → Load WebP
- Neither? → Load JPEG
Like a restaurant menu: "Chef's special (AVIF) if available, otherwise signature dish (WebP), or house classic (JPEG)."
4. Sharp: Server-Side Image Processing
With 50 blog posts, manual conversion became impossible. Batch processing with Sharp library:
// scripts/optimize-images.ts
import sharp from 'sharp';
import fs from 'fs/promises';
import path from 'path';
async function optimizeImage(inputPath: string, outputDir: string) {
const filename = path.parse(inputPath).name;
// Convert to WebP (quality 85)
await sharp(inputPath)
.webp({ quality: 85 })
.toFile(path.join(outputDir, `${filename}.webp`));
// Convert to AVIF (quality 70, better compression)
await sharp(inputPath)
.avif({ quality: 70, effort: 6 })
.toFile(path.join(outputDir, `${filename}.avif`));
// Generate responsive sizes
const sizes = [400, 800, 1200, 2400];
for (const size of sizes) {
await sharp(inputPath)
.resize(size, null, { withoutEnlargement: true })
.webp({ quality: 85 })
.toFile(path.join(outputDir, `${filename}-${size}w.webp`));
}
console.log(`✓ Optimized: ${filename}`);
}
// Usage
const images = await fs.readdir('./public/images/blog');
for (const img of images) {
if (img.match(/\.(png|jpg|jpeg)$/i)) {
await optimizeImage(
`./public/images/blog/${img}`,
'./public/images/blog/optimized'
);
}
}
This single script converted 50 images in 10 minutes. Keep original PNGs, auto-generate WebP/AVIF in 4 sizes.
5. Lazy Loading and LCP Optimization
Loading all images at once slows initial load. Load only what's visible first is key.
// Hero image: immediate load (LCP optimization)
<Image
src="/hero.webp"
alt="Hero"
width={1200}
height={630}
priority // converts to fetchpriority="high"
loading="eager"
/>
// Below-fold images: lazy load
<Image
src="/content-image.webp"
alt="Content"
width={800}
height={600}
loading="lazy" // loads when near viewport
/>
// More granular control
<Image
src="/below-fold.webp"
alt="Below fold"
width={600}
height={400}
loading="lazy"
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KG..." // tiny blur image
/>
LCP (Largest Contentful Paint): Time until largest content loads. Usually the hero image. Setting priority loads it before other resources.
Actual measurements:
- All images eager load: LCP 3.2s
- Hero priority, rest lazy: LCP 1.1s
3x difference.
6. CDN-Based Auto-Optimization
CDNs like Cloudflare or imgix enable optimization via URL parameters.
// Cloudflare Polish (automatic)
// https://example.com/image.jpg
// → Auto converts to WebP/AVIF, compresses
// imgix (URL parameters)
const imgixUrl = (src: string, options: {
w?: number;
h?: number;
fit?: 'crop' | 'scale';
fm?: 'webp' | 'avif' | 'jpg';
q?: number;
}) => {
const params = new URLSearchParams(options as any);
return `https://yourproject.imgix.net${src}?${params}`;
};
// Usage
<img
src={imgixUrl('/hero.jpg', { w: 1200, fm: 'webp', q: 85 })}
srcset={`
${imgixUrl('/hero.jpg', { w: 640, fm: 'webp', q: 85 })} 640w,
${imgixUrl('/hero.jpg', { w: 1200, fm: 'webp', q: 85 })} 1200w
`}
/>
Almost magical. Upload one original, CDN handles format conversion, resize, compression. No pre-processing needed—all real-time.
7. Art Direction: Different Images Per Screen
Sometimes you don't just resize—you need completely different images. Landscape images become too small on mobile.
<picture>
<!-- Mobile: portrait crop version -->
<source
media="(max-width: 640px)"
srcset="/hero-mobile-portrait.webp"
/>
<!-- Tablet: square crop -->
<source
media="(max-width: 1024px)"
srcset="/hero-tablet-square.webp"
/>
<!-- Desktop: landscape original -->
<img
src="/hero-desktop-landscape.webp"
alt="Hero image"
/>
</picture>
For product photos:
- Desktop: Product + full background (16:9)
- Tablet: Product-centered crop (4:3)
- Mobile: Product closeup only (3:4)
Same image concept, different versions per screen.
Summary: Image Optimization Checklist
From 6 months building blogs, landing pages, dashboards, here's my every-time checklist:
Format Selection
- Photos/complex graphics → AVIF first, WebP fallback
- Screenshots/text → WebP
- Transparent backgrounds → WebP or PNG
- Build fallback chain with
<picture>
Size Optimization
- Provide at least 3 sizes via srcset (400w, 800w, 1200w)
- Add sizes attribute to hint browser
- Consider retina displays (2x, 3x)
- Avoid unnecessarily large originals (2400px+ only for special cases)
Loading Strategy
- Hero/LCP images →
priorityorloading="eager" - Below-fold images →
loading="lazy" - Add placeholders (blur or color)
- Consider preload for critical images
Tooling
- Use Next.js Image component if available
- Batch conversion with Sharp scripts
- Enable CDN auto-optimization if available
- Automate image optimization in build
Measurement
- Measure LCP with Lighthouse (target
<2.5s) - Check actual transfer size in Network tab
- Validate with PageSpeed Insights field data
- Test with 3G throttling simulation
Practical Tips
- Always keep originals (Git LFS or separate storage)
- Prevent human error with automation scripts
- CI check for image optimization before deploy
- Monitor to detect unoptimized images