Tối ưu Hình ảnh Web 2026: AVIF, WebP, Responsive Images và Chiến lược CDN cho Core Web Vitals

Hướng dẫn toàn diện tối ưu hình ảnh web 2026: so sánh AVIF vs WebP, triển khai responsive images, fetchpriority, lazy loading, chiến lược Image CDN, và cải thiện Core Web Vitals (LCP, CLS).

Giới thiệu: Hình ảnh — Nút thắt cổ chai lớn nhất của hiệu suất web

Hình ảnh chiếm trung bình hơn 50% tổng dung lượng của một trang web. Nghe thì có vẻ ai cũng biết, nhưng con số thực tế vẫn khiến mình bất ngờ mỗi lần xem lại. Theo thống kê từ HTTP Archive đầu năm 2026, trang web trung bình tải khoảng 1.8MB hình ảnh, chiếm phần lớn trong tổng dung lượng 2.5-3MB của toàn trang. Và con số này vẫn đang tăng đều khi các thiết kế web ngày càng nặng về visual, hero image độ phân giải cao, gallery sản phẩm chi tiết.

Nhưng vấn đề không chỉ nằm ở dung lượng đâu. Hình ảnh tác động trực tiếp đến Largest Contentful Paint (LCP) — một trong ba chỉ số Core Web Vitals quan trọng nhất. Theo dữ liệu của Google, hơn 70% các trang có LCP kém đều do hình ảnh gây ra. Ngoài ra, hình ảnh không được tối ưu còn gây Cumulative Layout Shift (CLS) khi thiếu kích thước cố định, và tốn băng thông không cần thiết làm chậm toàn bộ quá trình tải trang.

Thực lòng mà nói, đây là lĩnh vực mà nỗ lực tối ưu sẽ mang lại kết quả rõ ràng nhất.

Trong bài viết này, mình sẽ đi sâu vào mọi khía cạnh của tối ưu hình ảnh web năm 2026 — từ việc chọn đúng định dạng (AVIF vs WebP vs JPEG), triển khai responsive images đúng cách, sử dụng fetchpriority và lazy loading thông minh, cho đến xây dựng chiến lược Image CDN chuyên nghiệp. Mục tiêu cuối cùng? Đạt điểm Core Web Vitals xanh lá và mang lại trải nghiệm tốt nhất cho người dùng.

1. Hiểu rõ tác động của hình ảnh lên Core Web Vitals

1.1 Hình ảnh và Largest Contentful Paint (LCP)

LCP đo lường thời gian cần để phần tử lớn nhất hiển thị trong viewport. Trong phần lớn trường hợp, phần tử LCP chính là một hình ảnh — có thể là hero image, banner quảng cáo, hoặc ảnh sản phẩm nổi bật. Google khuyến nghị LCP nên dưới 2.5 giây để đạt điểm "tốt".

Quá trình tải LCP image gồm nhiều giai đoạn, và mỗi giai đoạn đều có thể trở thành nút thắt:

  • Time to First Byte (TTFB): Thời gian server phản hồi request đầu tiên
  • Resource discovery: Thời gian browser phát hiện ra resource cần tải — nếu hình ảnh nằm trong CSS background hay được tạo bởi JavaScript, browser phải parse xong các file đó trước mới biết
  • Resource download: Thời gian tải xuống hình ảnh — phụ thuộc vào dung lượng file và tốc độ mạng
  • Rendering: Thời gian decode và vẽ hình ảnh lên màn hình

Mỗi giai đoạn trên đều có giải pháp tối ưu riêng, và chúng ta sẽ lần lượt đi qua từng cái.

1.2 Hình ảnh và Cumulative Layout Shift (CLS)

CLS xảy ra khi các phần tử trên trang thay đổi vị trí bất ngờ trong quá trình tải. Nếu bạn từng đọc báo online mà nội dung cứ nhảy loạn xạ khi ảnh load xong — đó chính là CLS. Hình ảnh là thủ phạm phổ biến nhất khi không có kích thước (width/height) được khai báo trước. Browser chưa biết kích thước ảnh nên dành không gian bằng 0, ảnh tải xong thì đẩy toàn bộ nội dung bên dưới xuống.

Giải pháp cơ bản nhất mà ai cũng nên áp dụng ngay:

<!-- ❌ Gây CLS: không có width/height -->
<img src="hero.jpg" alt="Hero image">

<!-- ✅ Không gây CLS: browser biết aspect ratio trước -->
<img src="hero.jpg" alt="Hero image" width="1200" height="630">

<!-- ✅ Kết hợp CSS responsive -->
<style>
  img {
    max-width: 100%;
    height: auto; /* Giữ aspect ratio */
  }
</style>

CSS cũng hỗ trợ thuộc tính aspect-ratio giúp việc này dễ dàng hơn nữa:

.hero-image {
  width: 100%;
  aspect-ratio: 16 / 9; /* Browser tự tính height */
}

2. Định dạng hình ảnh hiện đại: AVIF vs WebP vs JPEG

2.1 AVIF — Định dạng vượt trội nhất 2026

AVIF (AV1 Image File Format) là định dạng hình ảnh dựa trên codec video AV1, được phát triển bởi Alliance for Open Media. Tính đến đầu năm 2026, AVIF đã được hỗ trợ bởi hơn 93% trình duyệt trên toàn cầu, bao gồm Chrome, Firefox, Edge và Safari.

Ưu điểm vượt trội của AVIF:

  • Nén hiệu quả hơn 30-50% so với WebP và 60-70% so với JPEG ở cùng chất lượng
  • Hỗ trợ HDR và wide color gamut: Phù hợp cho ảnh chất lượng cao trên màn hình hiện đại
  • Hỗ trợ transparency: Thay thế được cả PNG trong nhiều trường hợp
  • Hỗ trợ animation: Có thể thay thế GIF với chất lượng cao hơn và dung lượng nhỏ hơn nhiều

Tuy nhiên, AVIF cũng không hoàn hảo:

  • Thời gian encode chậm hơn: Mất nhiều CPU hơn WebP hoặc JPEG, nên không phù hợp cho real-time processing nếu không có cache
  • Giới hạn kích thước: Một số triển khai AVIF ban đầu giới hạn ở 8K pixels, dù hạn chế này đang dần được khắc phục
  • Khoảng 7-8% thiết bị chưa hỗ trợ: Chủ yếu là các thiết bị Android cũ chạy trình duyệt phiên bản cũ

2.2 WebP — Lựa chọn an toàn và ổn định

WebP được Google phát triển từ 2010 và đến giờ đã được gần 98% trình duyệt hỗ trợ. Nếu bạn chỉ chọn được một định dạng mới để triển khai, WebP là lựa chọn "an toàn nhất":

  • Nhỏ hơn 25-34% so với JPEG ở chất lượng tương đương
  • Hỗ trợ transparency và animation: Thay thế được cả PNG và GIF
  • Encode nhanh: Phù hợp cho xử lý real-time và on-the-fly conversion
  • Browser support gần như tuyệt đối: Không cần lo lắng về fallback

2.3 So sánh thực tế với ví dụ cụ thể

Nói nhiều không bằng nhìn số. Đây là kết quả so sánh dung lượng thực tế cho một ảnh hero 1920x1080px, chất lượng hình ảnh tương đương nhau:

Định dạng     | Dung lượng  | So với JPEG
--------------+-------------+-----------
JPEG (q=80)   | 245 KB      | baseline
WebP (q=80)   | 178 KB      | -27%
AVIF (q=60)   | 112 KB      | -54%
PNG            | 1,850 KB    | +655% (lossless)

Nhìn vào bảng trên, AVIF tiết kiệm hơn một nửa dung lượng so với JPEG mà chất lượng vẫn tương đương. Với một trang có 10 ảnh sản phẩm, việc chuyển từ JPEG sang AVIF có thể giảm hơn 1MB dữ liệu — cải thiện đáng kể LCP trên mobile. Đó là con số khó mà bỏ qua.

2.4 Triển khai Progressive Enhancement với thẻ picture

Cách tiếp cận tốt nhất là sử dụng progressive enhancement — phục vụ định dạng tốt nhất mà trình duyệt hỗ trợ:

<picture>
  <!-- Ưu tiên AVIF nếu browser hỗ trợ -->
  <source srcset="hero.avif" type="image/avif">
  <!-- Fallback sang WebP -->
  <source srcset="hero.webp" type="image/webp">
  <!-- Fallback cuối cùng là JPEG -->
  <img src="hero.jpg" alt="Hero banner" width="1920" height="1080"
       loading="eager" fetchpriority="high" decoding="async">
</picture>

Thẻ <picture> hoạt động theo thứ tự từ trên xuống — browser sẽ chọn source đầu tiên mà nó hỗ trợ. Nếu không source nào phù hợp, nó fallback về thẻ <img> bên trong. Đơn giản và hiệu quả.

3. Responsive Images: Phục vụ đúng kích thước cho đúng thiết bị

3.1 Tại sao responsive images quan trọng?

Một trong những sai lầm phổ biến nhất (và thật sự rất phổ biến) là phục vụ cùng một ảnh 1920px cho cả desktop lẫn mobile. Trên màn hình mobile 375px, ảnh 1920px là lãng phí hoàn toàn — browser phải tải gấp 5 lần dung lượng cần thiết, rồi lại phải scale down. Tốn cả băng thông lẫn CPU để decode ảnh lớn không cần thiết.

3.2 Sử dụng srcset và sizes

Thuộc tính srcset cho phép khai báo nhiều phiên bản ảnh với kích thước khác nhau, còn sizes cho browser biết ảnh sẽ hiển thị ở kích thước nào trong từng breakpoint:

<img
  srcset="
    hero-480.webp   480w,
    hero-768.webp   768w,
    hero-1200.webp 1200w,
    hero-1920.webp 1920w
  "
  sizes="
    (max-width: 480px) 100vw,
    (max-width: 768px) 100vw,
    (max-width: 1200px) 80vw,
    1200px
  "
  src="hero-1200.webp"
  alt="Hero banner"
  width="1920"
  height="1080"
  loading="eager"
  fetchpriority="high"
>

Giải thích thuộc tính sizes cho dễ hiểu:

  • Trên mobile (<480px): ảnh chiếm 100% viewport width → browser chọn ảnh 480w
  • Trên tablet (<768px): vẫn 100vw → browser chọn ảnh 768w
  • Trên laptop (<1200px): ảnh chiếm 80% viewport → browser chọn ảnh phù hợp
  • Trên desktop lớn: ảnh tối đa 1200px → browser chọn ảnh 1200w

3.3 Kết hợp srcset với picture cho nhiều định dạng

Đây là "combo" hoàn chỉnh nhất — phục vụ đúng định dạng VÀ đúng kích thước. Code hơi dài một chút nhưng kết quả rất đáng:

<picture>
  <source
    type="image/avif"
    srcset="
      hero-480.avif   480w,
      hero-768.avif   768w,
      hero-1200.avif 1200w,
      hero-1920.avif 1920w
    "
    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
  >
  <source
    type="image/webp"
    srcset="
      hero-480.webp   480w,
      hero-768.webp   768w,
      hero-1200.webp 1200w,
      hero-1920.webp 1920w
    "
    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
  >
  <img
    srcset="
      hero-480.jpg   480w,
      hero-768.jpg   768w,
      hero-1200.jpg 1200w,
      hero-1920.jpg 1920w
    "
    sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
    src="hero-1200.jpg"
    alt="Hero banner trang chủ"
    width="1920" height="1080"
    loading="eager" fetchpriority="high" decoding="async"
  >
</picture>

Một user trên iPhone 13 sẽ nhận ảnh AVIF 480w (~30KB) thay vì JPEG 1920w (~245KB). Tiết kiệm 87% dung lượng! Mình đã từng áp dụng cách này cho một trang e-commerce và kết quả thật sự ấn tượng.

3.4 Tự động hóa với build tools

Tất nhiên, việc tạo thủ công hàng chục phiên bản ảnh là không thực tế chút nào. Dưới đây là cách tự động hóa với sharp trong Node.js:

// scripts/generate-responsive-images.js
const sharp = require('sharp');
const path = require('path');
const fs = require('fs');

const SIZES = [480, 768, 1200, 1920];
const FORMATS = ['avif', 'webp', 'jpeg'];

async function generateResponsiveImages(inputPath) {
  const filename = path.parse(inputPath).name;
  const outputDir = path.join('public', 'images', 'responsive');

  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  for (const size of SIZES) {
    for (const format of FORMATS) {
      const ext = format === 'jpeg' ? 'jpg' : format;
      const outputPath = path.join(outputDir, `${filename}-${size}.${ext}`);

      let pipeline = sharp(inputPath).resize(size, null, {
        withoutEnlargement: true
      });

      switch (format) {
        case 'avif':
          pipeline = pipeline.avif({ quality: 60, effort: 4 });
          break;
        case 'webp':
          pipeline = pipeline.webp({ quality: 80 });
          break;
        case 'jpeg':
          pipeline = pipeline.jpeg({ quality: 80, progressive: true });
          break;
      }

      await pipeline.toFile(outputPath);
      const stats = fs.statSync(outputPath);
      console.log(`  ${outputPath}: ${(stats.size / 1024).toFixed(1)} KB`);
    }
  }
}

// Sử dụng
generateResponsiveImages('src/images/hero.jpg');

Với Vite hoặc Webpack, bạn có thể tích hợp quá trình này vào build pipeline bằng plugin vite-plugin-image-optimizer hoặc image-minimizer-webpack-plugin. Rất tiện lợi.

4. Fetch Priority và Preload: Tối ưu thứ tự tải hình ảnh

4.1 Thuộc tính fetchpriority

Nếu mình chỉ được chọn một kỹ thuật duy nhất để cải thiện LCP, thì có lẽ đó sẽ là fetchpriority. Thuộc tính này cho phép bạn điều chỉnh mức ưu tiên tải resource, và cách dùng cực kỳ đơn giản. Theo thử nghiệm của Google, việc thêm fetchpriority="high" cho LCP image đã cải thiện LCP từ 2.6 giây xuống còn 1.9 giây — giảm gần 27%.

Sử dụng thế nào?

<!-- ✅ LCP image: ưu tiên cao -->
<img src="hero.avif" alt="Hero" fetchpriority="high" loading="eager">

<!-- ✅ Ảnh below-the-fold: ưu tiên thấp -->
<img src="thumbnail.webp" alt="Thumbnail" fetchpriority="low" loading="lazy">

<!-- ⚠️ SAI: không kết hợp lazy + high priority -->
<!-- Nếu ảnh quan trọng thì không nên lazy load -->
<img src="hero.avif" alt="Hero" fetchpriority="high" loading="lazy">

Ba giá trị của fetchpriority:

  • high: Tải ưu tiên cao — dùng cho LCP image
  • low: Tải ưu tiên thấp — dùng cho ảnh below-the-fold, thumbnails
  • auto (mặc định): Browser tự quyết định

4.2 Preload LCP Image

Nếu LCP image không nằm trực tiếp trong HTML (ví dụ: background-image trong CSS, hoặc được render bởi JavaScript), browser sẽ phải chờ tải và parse CSS/JS trước mới phát hiện ra ảnh. Độ trễ này đáng kể lắm. Giải pháp là dùng <link rel="preload"> trong <head>:

<head>
  <!-- Preload LCP image để browser tải sớm nhất có thể -->
  <link
    rel="preload"
    as="image"
    href="hero.avif"
    type="image/avif"
    fetchpriority="high"
  >

  <!-- Preload responsive image với imagesrcset -->
  <link
    rel="preload"
    as="image"
    imagesrcset="
      hero-480.avif   480w,
      hero-768.avif   768w,
      hero-1200.avif 1200w
    "
    imagesizes="(max-width: 768px) 100vw, 80vw"
    type="image/avif"
    fetchpriority="high"
  >
</head>

4.3 Early Hints (103 status code)

Đây là kỹ thuật nâng cao hơn — server gửi header 103 Early Hints trước cả khi response chính (200) sẵn sàng. Browser có thể bắt đầu preload resources trong khi server vẫn đang xử lý request. Nghe hơi "ảo" nhưng hiệu quả rất thực:

# Cấu hình Nginx cho Early Hints
server {
    location / {
        # Gửi Early Hints cho LCP image
        add_header Link "; rel=preload; as=image; type=image/avif" early;

        # Gửi Early Hints cho font quan trọng
        add_header Link "; rel=preload; as=font; crossorigin" early;

        proxy_pass http://backend;
    }
}

# Cloudflare hỗ trợ Early Hints tự động từ các link preload tags trong HTML

Theo dữ liệu từ Cloudflare, Early Hints có thể cải thiện LCP thêm 300-500ms trên các kết nối chậm. Đối với người dùng mobile ở vùng mạng yếu, đó là sự khác biệt rất lớn.

5. Lazy Loading: Tải hình ảnh thông minh

5.1 Native Lazy Loading

Thuộc tính loading="lazy" đã được tất cả trình duyệt hiện đại hỗ trợ. Nó nói với browser rằng: "Đừng tải ảnh này ngay, chỉ tải khi nó sắp xuất hiện trong viewport." Đơn giản, hiệu quả, và không cần JavaScript:

<!-- ❌ SAI: lazy load LCP image → LCP sẽ rất chậm -->
<img src="hero.jpg" alt="Hero" loading="lazy">

<!-- ✅ ĐÚNG: LCP image tải ngay -->
<img src="hero.jpg" alt="Hero" loading="eager" fetchpriority="high">

<!-- ✅ ĐÚNG: ảnh below-the-fold tải lazy -->
<img src="product-1.webp" alt="Sản phẩm 1" loading="lazy" width="400" height="300">
<img src="product-2.webp" alt="Sản phẩm 2" loading="lazy" width="400" height="300">
<img src="product-3.webp" alt="Sản phẩm 3" loading="lazy" width="400" height="300">

Quy tắc vàng: Không bao giờ lazy load ảnh above-the-fold, đặc biệt là LCP image. Mình đã thấy không ít website mắc lỗi này và tự hỏi tại sao LCP lại chậm.

5.2 Lazy Loading cho CSS Background Images

Native lazy loading chỉ hoạt động với thẻ <img><iframe>. Cho background images, bạn cần dùng Intersection Observer API:

// Lazy load background images
document.addEventListener('DOMContentLoaded', () => {
  const lazyBackgrounds = document.querySelectorAll('.lazy-bg');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const element = entry.target;
        element.style.backgroundImage = `url(${element.dataset.bg})`;
        element.classList.add('loaded');
        observer.unobserve(element);
      }
    });
  }, {
    rootMargin: '200px 0px' // Bắt đầu tải trước khi vào viewport 200px
  });

  lazyBackgrounds.forEach(bg => observer.observe(bg));
});
<!-- HTML -->
<div class="hero lazy-bg" data-bg="/images/hero-bg.avif">
  <h1>Chào mừng đến website</h1>
</div>

<style>
  .hero {
    min-height: 500px;
    background-color: #f0f0f0; /* Placeholder color */
    background-size: cover;
    background-position: center;
    transition: opacity 0.3s;
  }
  .hero:not(.loaded) {
    opacity: 0.7;
  }
</style>

5.3 Placeholder và LQIP (Low Quality Image Placeholder)

Để tránh trải nghiệm "nhấp nháy" khi ảnh lazy-loaded xuất hiện đột ngột, bạn có thể dùng kỹ thuật LQIP — hiển thị ảnh chất lượng thấp, mờ trước, rồi thay thế bằng ảnh gốc khi tải xong:

<!-- Inline base64 placeholder cực nhỏ (~ 300 bytes) -->
<img
  src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
  data-src="product-full.avif"
  alt="Sản phẩm"
  class="lqip-image"
  width="800" height="600"
  loading="lazy"
  style="filter: blur(20px); transition: filter 0.5s;"
>

<script>
// Khi ảnh thật tải xong, xóa blur effect
document.querySelectorAll('.lqip-image').forEach(img => {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const fullImg = new Image();
        fullImg.onload = () => {
          img.src = img.dataset.src;
          img.style.filter = 'none';
        };
        fullImg.src = img.dataset.src;
        observer.unobserve(img);
      }
    });
  }, { rootMargin: '100px' });
  observer.observe(img);
});
</script>

Ngoài inline base64, bạn cũng có thể dùng BlurHash hoặc ThumbHash — hai thư viện tạo placeholder rất nhỏ (20-30 bytes) nhưng vẫn giữ được bố cục màu sắc của ảnh gốc. Cá nhân mình thích ThumbHash hơn vì nó cho kết quả tự nhiên hơn.

6. Chiến lược Image CDN chuyên nghiệp

6.1 Image CDN là gì và tại sao cần thiết?

Image CDN không phải CDN thông thường phục vụ file tĩnh. Đây là các dịch vụ chuyên biệt có khả năng xử lý, tối ưu và chuyển đổi hình ảnh on-the-fly dựa trên thiết bị, trình duyệt và kết nối mạng của người dùng. Theo web.dev, Image CDN có thể giảm 40-80% dung lượng hình ảnh mà không cần thay đổi code phía client.

Nghe có vẻ quá tốt? Thực tế thì đúng là vậy — đây là một trong những "quick win" lớn nhất mà bạn có thể áp dụng.

Các dịch vụ Image CDN phổ biến năm 2026:

  • Cloudinary: Tính năng toàn diện nhất, hỗ trợ AI-powered optimization
  • Imgix: Tập trung vào developer experience và tốc độ xử lý
  • Cloudflare Images: Giá phải chăng, tích hợp tốt với hệ sinh thái Cloudflare
  • ImageKit: Cân bằng tốt giữa tính năng và chi phí
  • Bunny Optimizer: Hiệu quả cao với mức giá thấp nhất

6.2 Tự động tối ưu với Cloudinary

Cloudinary là một trong những Image CDN mạnh mẽ nhất hiện tại. Chỉ cần thay đổi URL, bạn đã có thể tối ưu hoàn toàn hình ảnh:

<!-- URL gốc -->
<img src="https://res.cloudinary.com/demo/image/upload/sample.jpg">

<!-- Tự động chọn format tốt nhất (f_auto) và chất lượng tối ưu (q_auto) -->
<img src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto/sample.jpg">

<!-- Resize + auto format + auto quality -->
<img src="https://res.cloudinary.com/demo/image/upload/w_800,h_600,c_fill,f_auto,q_auto/sample.jpg">

<!-- Responsive breakpoints tự động -->
<img
  srcset="
    https://res.cloudinary.com/demo/image/upload/w_480,f_auto,q_auto/sample.jpg  480w,
    https://res.cloudinary.com/demo/image/upload/w_768,f_auto,q_auto/sample.jpg  768w,
    https://res.cloudinary.com/demo/image/upload/w_1200,f_auto,q_auto/sample.jpg 1200w
  "
  sizes="(max-width: 768px) 100vw, 80vw"
  src="https://res.cloudinary.com/demo/image/upload/w_800,f_auto,q_auto/sample.jpg"
  alt="Demo image"
  width="1200" height="800"
>

Tham số f_auto sẽ tự động phục vụ AVIF cho browser hỗ trợ, WebP cho browser không hỗ trợ AVIF, và JPEG cho browser cũ. Còn q_auto dùng thuật toán perceptual quality để tìm mức nén tối ưu mà mắt người không phân biệt được. Hai tham số này thôi đã giải quyết được phần lớn vấn đề.

6.3 Tự xây dựng Image CDN với Cloudflare Workers

Nếu bạn muốn kiểm soát hoàn toàn và tiết kiệm chi phí, bạn có thể tự xây dựng Image CDN bằng Cloudflare Workers kết hợp với Cloudflare Image Resizing:

// Cloudflare Worker: Image Optimization Proxy
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Chỉ xử lý các request đến /images/
    if (!url.pathname.startsWith('/images/')) {
      return fetch(request);
    }

    // Lấy thông tin từ request headers
    const accept = request.headers.get('Accept') || '';
    const userAgent = request.headers.get('User-Agent') || '';

    // Xác định format tốt nhất
    let format = 'jpeg';
    if (accept.includes('image/avif')) {
      format = 'avif';
    } else if (accept.includes('image/webp')) {
      format = 'webp';
    }

    // Xác định width từ query param
    const width = parseInt(url.searchParams.get('w')) || 1200;
    const quality = parseInt(url.searchParams.get('q')) || 80;

    // Lấy ảnh gốc từ origin
    const originUrl = `${env.ORIGIN_URL}${url.pathname}`;

    // Sử dụng Cloudflare Image Resizing
    const imageResponse = await fetch(originUrl, {
      cf: {
        image: {
          width: width,
          quality: quality,
          format: format,
          fit: 'cover',
          metadata: 'none' // Xóa EXIF để giảm dung lượng
        }
      }
    });

    // Thêm cache headers
    const response = new Response(imageResponse.body, imageResponse);
    response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
    response.headers.set('Vary', 'Accept');

    return response;
  }
};

6.4 Caching Strategy cho hình ảnh

Chiến lược caching đúng đắn có thể giảm đáng kể thời gian tải cho các lần truy cập tiếp theo. Đây là phần nhiều người hay bỏ qua:

# Nginx caching configuration cho images
location ~* \.(avif|webp|jpg|jpeg|png|gif|svg)$ {
    # Cache dài hạn cho ảnh có fingerprint trong tên file
    # Ví dụ: hero-abc123.avif
    if ($uri ~* "-[a-f0-9]{6,}\.(avif|webp|jpg)$") {
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # Cache trung bình cho ảnh không có fingerprint
    add_header Cache-Control "public, max-age=604800, stale-while-revalidate=86400";

    # Content negotiation cho format
    add_header Vary "Accept";

    # Nén không cần thiết cho ảnh (đã nén sẵn)
    gzip off;
    brotli off;
}
<!-- Service Worker caching cho ảnh -->
<script>
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}
</script>
// sw.js - Service Worker với cache-first strategy cho images
const IMAGE_CACHE = 'images-v1';
const MAX_CACHE_SIZE = 100;

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);

  if (event.request.destination === 'image') {
    event.respondWith(
      caches.open(IMAGE_CACHE).then(async (cache) => {
        const cached = await cache.match(event.request);
        if (cached) return cached;

        const response = await fetch(event.request);
        if (response.ok) {
          cache.put(event.request, response.clone());
          trimCache(cache, MAX_CACHE_SIZE);
        }
        return response;
      })
    );
  }
});

async function trimCache(cache, maxSize) {
  const keys = await cache.keys();
  if (keys.length > maxSize) {
    await cache.delete(keys[0]);
    trimCache(cache, maxSize);
  }
}

7. Tối ưu hình ảnh trong các Framework hiện đại

7.1 Next.js Image Component

Next.js cung cấp component next/image tích hợp sẵn rất nhiều tối ưu. Nói thật, đây là một trong những framework làm tốt nhất việc tối ưu hình ảnh tự động:

import Image from 'next/image';

// Next.js tự động:
// - Resize và serve responsive images
// - Convert sang WebP/AVIF
// - Lazy load mặc định
// - Ngăn CLS với placeholder
export default function HeroSection() {
  return (
    <section>
      {/* LCP image: priority={true} tắt lazy loading và thêm preload */}
      <Image
        src="/images/hero.jpg"
        alt="Hero banner"
        width={1920}
        height={1080}
        priority={true}
        sizes="100vw"
        quality={85}
      />

      {/* Ảnh thường: tự động lazy load */}
      <Image
        src="/images/product.jpg"
        alt="Sản phẩm"
        width={800}
        height={600}
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
      />
    </section>
  );
}

7.2 Nuxt Image Module

Nuxt cũng có module @nuxt/image với khả năng tương tự, tích hợp tốt với nhiều Image CDN providers:

<!-- nuxt.config.ts -->
<script>
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    provider: 'cloudinary',
    cloudinary: {
      baseURL: 'https://res.cloudinary.com/your-cloud/image/upload/'
    },
    screens: {
      xs: 320,
      sm: 640,
      md: 768,
      lg: 1024,
      xl: 1280,
      xxl: 1536
    }
  }
});
</script>

<!-- Sử dụng trong component -->
<template>
  <div>
    <NuxtImg
      src="/hero.jpg"
      sizes="sm:100vw md:80vw lg:1200px"
      format="avif,webp"
      quality="80"
      loading="eager"
      preload
    />
  </div>
</template>

7.3 Angular NgOptimizedImage

Angular cũng đã bắt kịp với directive NgOptimizedImage:

// app.component.ts
import { NgOptimizedImage } from '@angular/common';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [NgOptimizedImage],
  template: `
    <!-- LCP image: thêm priority directive -->
    <img ngSrc="hero.jpg" width="1920" height="1080" priority />

    <!-- Ảnh thường: tự động lazy load -->
    <img ngSrc="product.jpg" width="800" height="600"
         placeholder />
  `
})
export class AppComponent {}

Điểm hay của NgOptimizedImage là nó sẽ cảnh báo lỗi trong development mode nếu bạn mắc các sai lầm phổ biến như: lazy load LCP image, thiếu width/height, hoặc ảnh quá lớn cho container. Rất hữu ích cho team development.

8. Kiểm tra và đo lường hiệu quả tối ưu

8.1 Lighthouse Image Audit

Lighthouse cung cấp nhiều audit liên quan đến hình ảnh. Đây là các audit quan trọng nhất và cách khắc phục:

  • "Properly size images": Ảnh đang lớn hơn kích thước hiển thị → Dùng srcset hoặc Image CDN để resize
  • "Serve images in next-gen formats": Đang dùng JPEG/PNG → Chuyển sang picture element với AVIF + WebP
  • "Efficiently encode images": Ảnh chưa nén đủ → Giảm quality xuống 80% hoặc dùng q_auto
  • "Defer offscreen images": Ảnh below-the-fold tải ngay → Thêm loading="lazy"
  • "Image elements do not have explicit width and height": Thiếu kích thước → Thêm width + height

8.2 Script kiểm tra tự động

Mình hay dùng script này để nhanh chóng kiểm tra tất cả ảnh trên trang. Paste vào Chrome DevTools Console là chạy ngay:

// Paste vào Chrome DevTools Console
(function auditImages() {
  const images = document.querySelectorAll('img');
  const results = [];

  images.forEach((img, i) => {
    const rect = img.getBoundingClientRect();
    const isInViewport = rect.top < window.innerHeight;
    const naturalW = img.naturalWidth;
    const displayW = rect.width;
    const oversized = naturalW > displayW * 2;

    results.push({
      index: i,
      src: img.src.substring(0, 80) + '...',
      naturalSize: `${naturalW}x${img.naturalHeight}`,
      displaySize: `${Math.round(displayW)}x${Math.round(rect.height)}`,
      oversized: oversized ? `⚠️ ${Math.round(naturalW/displayW)}x quá lớn` : '✅',
      hasWidthHeight: (img.hasAttribute('width') && img.hasAttribute('height')) ? '✅' : '❌ Thiếu → CLS',
      loading: img.loading || 'auto',
      fetchpriority: img.fetchPriority || 'auto',
      inViewport: isInViewport ? 'above fold' : 'below fold',
      format: img.src.match(/\.(avif|webp|jpg|jpeg|png|gif|svg)/i)?.[1] || 'unknown',
      sizeKB: '(check Network tab)'
    });
  });

  console.table(results);

  const issues = results.filter(r =>
    r.oversized.includes('⚠️') ||
    r.hasWidthHeight.includes('❌') ||
    (r.inViewport === 'above fold' && r.loading === 'lazy') ||
    (r.inViewport === 'below fold' && r.loading !== 'lazy')
  );

  if (issues.length > 0) {
    console.warn(`⚠️ Tìm thấy ${issues.length} vấn đề cần sửa:`);
    console.table(issues);
  } else {
    console.log('✅ Tất cả ảnh đều đã được tối ưu!');
  }
})();

8.3 Performance Budget cho hình ảnh

Thiết lập performance budget giúp đảm bảo team không vô tình thêm ảnh quá nặng. Đây là thứ mà nhiều dự án thiếu:

// performance-budget.json — dùng với Lighthouse CI
{
  "budgets": [
    {
      "resourceSizes": [
        {
          "resourceType": "image",
          "budget": 500
        },
        {
          "resourceType": "total",
          "budget": 1500
        }
      ],
      "resourceCounts": [
        {
          "resourceType": "image",
          "budget": 25
        }
      ],
      "timings": [
        {
          "metric": "largest-contentful-paint",
          "budget": 2500
        },
        {
          "metric": "cumulative-layout-shift",
          "budget": 0.1
        }
      ]
    }
  ]
}

9. Checklist tối ưu hình ảnh hoàn chỉnh

Sau tất cả những gì đã trình bày, đây là checklist bạn có thể lưu lại và áp dụng cho mọi dự án web:

  1. Định dạng: Sử dụng AVIF với WebP fallback qua thẻ <picture>
  2. Kích thước: Phục vụ responsive images với srcset phù hợp các breakpoint chính (480, 768, 1200, 1920)
  3. Nén: AVIF quality 50-65, WebP quality 75-85, JPEG quality 75-85 (hoặc dùng q_auto)
  4. Kích thước cố định: Mọi thẻ img phải có width và height hoặc aspect-ratio CSS để tránh CLS
  5. LCP image: Thêm fetchpriority="high"loading="eager", tuyệt đối không lazy load
  6. Preload: Preload LCP image trong <head>, đặc biệt nếu ảnh nằm trong CSS background
  7. Lazy loading: Thêm loading="lazy" cho tất cả ảnh below-the-fold
  8. Decoding: Thêm decoding="async" cho ảnh non-critical
  9. Image CDN: Sử dụng Image CDN cho auto-format, auto-quality, và edge caching
  10. Caching: Set Cache-Control header hợp lý — immutable cho fingerprinted URLs
  11. Placeholder: Sử dụng LQIP, BlurHash, hoặc dominant color placeholder
  12. Metadata: Xóa EXIF metadata không cần thiết

10. Kết luận

Tối ưu hình ảnh là một trong những đòn bẩy hiệu quả nhất để cải thiện hiệu suất web. Với các kỹ thuật trong bài viết này, bạn hoàn toàn có thể:

  • Giảm 50-70% dung lượng hình ảnh bằng cách chuyển sang AVIF/WebP
  • Cải thiện LCP 20-30% bằng fetchpriority và preload
  • Loại bỏ hoàn toàn CLS do hình ảnh bằng width/height attributes
  • Giảm thêm 40-80% dung lượng với Image CDN tự động

Lời khuyên của mình: hãy bắt đầu từ những thay đổi đơn giản nhất. Thêm width/height cho ảnh, dùng lazy loading cho ảnh below-the-fold, và thêm fetchpriority="high" cho LCP image. Chỉ ba bước này thôi đã có thể cải thiện đáng kể điểm Core Web Vitals.

Sau đó, dần triển khai các giải pháp nâng cao hơn: chuyển đổi sang AVIF/WebP, thiết lập responsive images với srcset, và tích hợp Image CDN. Mỗi bước đều mang lại cải thiện rõ rệt, và tích lũy lại sẽ tạo ra sự khác biệt rất lớn cho trải nghiệm người dùng lẫn thứ hạng SEO của bạn.

Trong bài viết tiếp theo, chúng ta sẽ tiếp tục khám phá các kỹ thuật tối ưu hiệu suất web khác — bao gồm bundle optimization, critical CSS, và font loading strategies. Hãy theo dõi Web Perf Clinic để không bỏ lỡ nhé!

Về Tác Giả Editorial Team

Our team of expert writers and editors.