Tối ưu hình ảnh cho Web Performance 2026 — AVIF, WebP, Sharp và CDN Edge

Posted on: 4/26/2026 3:16:21 AM

Hình ảnh chiếm trung bình 50-70% tổng dung lượng của một trang web. Một tấm ảnh hero chưa tối ưu có thể nặng 2-5 MB, đẩy Largest Contentful Paint (LCP) lên trên 4 giây và khiến Core Web Vitals rơi vào vùng đỏ. Trong năm 2026, với sự trưởng thành của AVIF, sự phổ biến toàn cầu của WebP, và các công cụ xử lý ảnh tại edge, việc tối ưu hình ảnh không còn là "nice-to-have" — nó là yếu tố sống còn cho SEO, UX và conversion rate.

Bài viết này đi sâu vào toàn bộ pipeline tối ưu hình ảnh: từ chọn format, responsive images, xử lý server-side với Sharp, placeholder strategies cho perceived performance, tích hợp CDN edge, đến đo lường tác động lên Core Web Vitals.

50-70%Dung lượng trang đến từ hình ảnh
50%AVIF nhỏ hơn JPEG cùng chất lượng
25-35%WebP nhỏ hơn JPEG tương đương
4-5xSharp nhanh hơn ImageMagick

1. Bản đồ Format hình ảnh 2026

Trước khi bàn về kỹ thuật tối ưu, cần hiểu rõ landscape các format ảnh hiện tại. Năm 2026, cuộc đua format ảnh đã có kết quả tương đối rõ ràng.

FormatNénChất lượngBrowser SupportUse Case
JPEGLossyTốt100%Fallback, ảnh cũ
PNGLosslessHoàn hảo100%Icon, logo, transparency
WebPLossy + LosslessRất tốt97%+Default cho mọi ảnh
AVIFLossy + LosslessXuất sắc93%+Hero images, LCP critical
JPEG XLLossy + LosslessXuất sắc~45%Chờ adoption rộng hơn
SVGVectorN/A100%Icon, illustration

Chiến lược format 2026

Format mặc định nên là WebP vì balance tốt nhất giữa file size, chất lượng và browser support. Dùng AVIF cho ảnh hero, banner, ảnh LCP — những nơi mỗi KB đều quan trọng. JPEG chỉ dùng làm fallback cuối cùng. JPEG XL có tiềm năng nhưng browser support chưa đủ để production.

1.1. AVIF — Vũ khí bí mật cho LCP

AVIF (AV1 Image File Format) sử dụng codec AV1 — vốn được thiết kế cho video — để nén ảnh. Kết quả là file nhỏ hơn JPEG 50% ở cùng chất lượng cảm nhận, và nhỏ hơn WebP khoảng 20-30%.

<!-- Fallback stack chuẩn: AVIF → WebP → JPEG -->
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero banner" width="1200" height="630">
</picture>

Lưu ý về AVIF

AVIF encode chậm hơn WebP 5-10x. Không nên generate AVIF on-the-fly cho user upload. Hãy pre-generate trong build pipeline hoặc dùng CDN image service có cache. Quality setting tối ưu cho AVIF là 60-70 (tương đương JPEG 85).

1.2. WebP — Format mặc định của 2026

WebP đã đạt mức support >97% trên toàn bộ browser hiện đại. Với khả năng nén lossy tốt hơn JPEG 25-35% và hỗ trợ cả animation (thay thế GIF), WebP là lựa chọn an toàn nhất cho production.

// Quality settings tối ưu cho từng format
const qualityMap = {
  avif: 65,    // Tương đương JPEG 85
  webp: 80,    // Balance size/quality tốt nhất
  jpeg: 85,    // Fallback quality
  png: null    // Lossless — dùng cho icons/logos
};

2. Responsive Images — Phục vụ đúng ảnh cho đúng thiết bị

Một trong những sai lầm phổ biến nhất là serve ảnh 1920px cho màn hình mobile 375px. Responsive images giải quyết vấn đề này bằng cách cung cấp nhiều kích thước và để browser tự chọn phiên bản phù hợp.

graph TD
    A["Browser phân tích viewport + DPR"] --> B{"srcset có sẵn?"}
    B -->|Có| C["Chọn ảnh phù hợp từ srcset"]
    B -->|Không| D["Tải ảnh từ src fallback"]
    C --> E{"sizes attribute?"}
    E -->|Có| F["Tính toán kích thước hiển thị"]
    E -->|Không| G["Giả định 100vw"]
    F --> H["Tải ảnh gần nhất ≥ kích thước cần"]
    G --> H
    style A fill:#e94560,stroke:#fff,color:#fff
    style C fill:#4CAF50,stroke:#fff,color:#fff
    style H fill:#2c3e50,stroke:#fff,color:#fff

Hình 1: Quy trình browser chọn ảnh từ srcset

2.1. srcset và sizes — Bộ đôi không thể thiếu

<img
  srcset="
    product-400w.webp   400w,
    product-800w.webp   800w,
    product-1200w.webp 1200w,
    product-1600w.webp 1600w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) 50vw,
    33vw
  "
  src="product-800w.webp"
  alt="Sản phẩm ABC"
  width="800"
  height="600"
  loading="lazy"
  decoding="async"
>

Giải thích chi tiết

srcset khai báo các phiên bản ảnh với width descriptor (400w, 800w...). sizes cho browser biết ảnh sẽ chiếm bao nhiêu viewport: trên mobile (<640px) chiếm 100vw, tablet chiếm 50vw, desktop chiếm 33vw. Browser sẽ nhân sizes × DPR để chọn ảnh phù hợp nhất. Ví dụ: iPhone 15 (390px viewport, 3x DPR) → cần ảnh 390×3 = 1170px → chọn product-1200w.webp.

2.2. Art Direction với <picture>

Khi cần thay đổi composition ảnh theo breakpoint (crop khác nhau cho mobile vs desktop), dùng <picture> element với media queries:

<picture>
  <!-- Mobile: crop vuông, focus vào sản phẩm -->
  <source
    media="(max-width: 640px)"
    srcset="product-mobile.avif 400w, product-mobile.webp 400w"
    sizes="100vw"
    type="image/avif"
  >
  <!-- Desktop: ảnh rộng, có context -->
  <source
    srcset="product-desktop.avif 1200w, product-desktop.webp 1200w"
    sizes="50vw"
    type="image/avif"
  >
  <img src="product-desktop.jpg" alt="Sản phẩm" width="1200" height="630">
</picture>

3. Server-Side Processing với Sharp

Sharp — thư viện xử lý ảnh Node.js dựa trên libvips — là tiêu chuẩn vàng cho server-side image processing. Nhanh hơn ImageMagick 4-5x, sử dụng bộ nhớ cực thấp nhờ streaming pixel data qua libvips thay vì decode toàn bộ ảnh vào heap.

graph LR
    A["Upload ảnh gốc"] --> B["Sharp Pipeline"]
    B --> C["Resize theo breakpoints"]
    C --> D["Generate AVIF"]
    C --> E["Generate WebP"]
    C --> F["Generate JPEG fallback"]
    D --> G["Lưu vào Storage/CDN"]
    E --> G
    F --> G
    G --> H["Serve qua CDN Edge"]
    style A fill:#e94560,stroke:#fff,color:#fff
    style B fill:#2c3e50,stroke:#fff,color:#fff
    style G fill:#4CAF50,stroke:#fff,color:#fff
    style H fill:#16213e,stroke:#fff,color:#fff

Hình 2: Pipeline xử lý ảnh server-side với Sharp

3.1. Pipeline xử lý ảnh hoàn chỉnh

import sharp from 'sharp';
import path from 'path';

const BREAKPOINTS = [400, 800, 1200, 1600];
const FORMATS = ['avif', 'webp', 'jpeg'];
const QUALITY = { avif: 65, webp: 80, jpeg: 85 };

async function processImage(inputPath, outputDir) {
  const metadata = await sharp(inputPath).metadata();
  const baseName = path.basename(inputPath, path.extname(inputPath));
  const results = [];

  for (const width of BREAKPOINTS) {
    if (width > metadata.width) continue;

    for (const format of FORMATS) {
      const outputPath = path.join(
        outputDir,
        `${baseName}-${width}w.${format}`
      );

      await sharp(inputPath)
        .resize(width, null, {
          withoutEnlargement: true,
          fit: 'inside',
        })
        .toFormat(format, {
          quality: QUALITY[format],
          effort: format === 'avif' ? 4 : 6,
        })
        .toFile(outputPath);

      results.push({ width, format, path: outputPath });
    }
  }

  return results;
}

// Sử dụng
const images = await processImage('hero.jpg', './optimized');
console.log(`Generated ${images.length} variants`);

3.2. Stream Processing cho upload real-time

import express from 'express';
import sharp from 'sharp';
import { PassThrough } from 'stream';

const app = express();

app.post('/upload', async (req, res) => {
  const transform = sharp()
    .resize(1200, null, { withoutEnlargement: true })
    .webp({ quality: 80 });

  // Stream trực tiếp từ request → sharp → storage
  // Không buffer toàn bộ ảnh vào memory
  req.pipe(transform).pipe(createWriteStream('./output.webp'));

  transform.on('info', (info) => {
    res.json({
      width: info.width,
      height: info.height,
      size: info.size,
      format: 'webp'
    });
  });
});

Tip: Parallel processing với Sharp

Sharp tự động tận dụng multi-core CPU thông qua libvips. Tuy nhiên, khi xử lý nhiều ảnh đồng thời, hãy giới hạn concurrency bằng sharp.concurrency(os.cpus().length) để tránh OOM. Trong production, khuyến nghị dùng job queue (BullMQ, Hangfire) để xử lý ảnh async.

4. Placeholder Strategies — Cải thiện Perceived Performance

Perceived performance (tốc độ cảm nhận) quan trọng không kém actual performance. Khi ảnh đang tải, hiển thị placeholder giúp giảm CLS (Cumulative Layout Shift) và tạo cảm giác trang tải nhanh hơn.

4.1. BlurHash vs ThumbHash

Tiêu chíBlurHashThumbHashLQIP (CSS blur)
Kích thước20-30 ký tự~28 bytes~200-500 bytes (ảnh tiny)
Chất lượngBlur tốtBlur + chi tiết hơnẢnh thật bị blur
TransparencyKhông
Aspect RatioKhông encodeCó encodeTừ ảnh gốc
DecodeJS cần thiếtJS cần thiếtNative browser
Inline đượcTrong JSON/DBTrong JSON/DBTrong HTML (base64)

4.2. Tạo ThumbHash server-side

import sharp from 'sharp';
import { rgbaToThumbHash } from 'thumbhash';

async function generateThumbHash(imagePath) {
  // Resize về kích thước rất nhỏ (max 100px)
  const { data, info } = await sharp(imagePath)
    .resize(100, 100, { fit: 'inside' })
    .ensureAlpha()
    .raw()
    .toBuffer({ resolveWithObject: true });

  const hash = rgbaToThumbHash(
    info.width, info.height, data
  );

  // Encode thành base64 để lưu DB
  return Buffer.from(hash).toString('base64');
}

// Lưu vào DB cùng với ảnh
const thumbhash = await generateThumbHash('product.jpg');
// "3OcRJYB4d3h/iIeHeEh3eIhw+j2w"

4.3. Hiển thị ThumbHash placeholder trong Vue.js

<script setup>
import { ref, onMounted, computed } from 'vue';
import { thumbHashToDataURL } from 'thumbhash';

const props = defineProps({
  src: String,
  thumbhash: String,
  alt: String,
  width: Number,
  height: Number
});

const loaded = ref(false);

const placeholderUrl = computed(() => {
  if (!props.thumbhash) return null;
  const hash = Uint8Array.from(
    atob(props.thumbhash),
    c => c.charCodeAt(0)
  );
  return thumbHashToDataURL(hash);
});
</script>

<template>
  <div class="image-wrapper" :style="{ aspectRatio: `${width}/${height}` }">
    <img
      v-if="placeholderUrl && !loaded"
      :src="placeholderUrl"
      :alt="alt"
      class="placeholder"
      aria-hidden="true"
    />
    <img
      :src="src"
      :alt="alt"
      :width="width"
      :height="height"
      loading="lazy"
      decoding="async"
      @load="loaded = true"
      :class="{ loaded }"
    />
  </div>
</template>

5. CDN Edge Optimization

Xử lý ảnh tại edge — gần người dùng nhất — giúp giảm latency và offload work khỏi origin server. Các CDN hiện đại cung cấp image transformation trực tiếp tại edge.

graph LR
    A["Client Request
Accept: image/avif,webp"] --> B["CDN Edge Node"] B --> C{"Ảnh đã cache?"} C -->|Có| D["Serve từ cache"] C -->|Không| E["Transform tại Edge"] E --> F["Resize + Format Convert"] F --> G["Cache + Serve"] D --> H["Response với format tối ưu"] G --> H style A fill:#e94560,stroke:#fff,color:#fff style B fill:#2c3e50,stroke:#fff,color:#fff style D fill:#4CAF50,stroke:#fff,color:#fff style G fill:#4CAF50,stroke:#fff,color:#fff

Hình 3: Image optimization tại CDN Edge

5.1. Cloudflare Polish — Tự động tối ưu không cần code

Cloudflare Polish tự động tối ưu mọi ảnh đi qua proxy mà không cần thay đổi markup. Polish có hai chế độ:

  • Lossless: Giảm size bằng cách loại bỏ metadata (EXIF, ICC profiles) và tối ưu compression. Không mất chất lượng.
  • Lossy: Nén thêm với chất lượng gần như không phân biệt bằng mắt thường. Giảm thêm 10-20% so với lossless.
  • WebP/AVIF auto-convert: Tự động chuyển đổi sang WebP hoặc AVIF khi browser hỗ trợ (detect qua Accept header).

5.2. Cloudflare Image Resizing — Transform on demand

<!-- URL-based image transformation -->
<img
  src="/cdn-cgi/image/width=800,quality=80,format=auto/images/hero.jpg"
  alt="Hero"
  width="800"
  height="420"
>

<!-- Responsive với Cloudflare Image Resizing -->
<img
  srcset="
    /cdn-cgi/image/width=400,format=auto/images/hero.jpg 400w,
    /cdn-cgi/image/width=800,format=auto/images/hero.jpg 800w,
    /cdn-cgi/image/width=1200,format=auto/images/hero.jpg 1200w
  "
  sizes="(max-width: 640px) 100vw, 50vw"
  src="/cdn-cgi/image/width=800,format=auto/images/hero.jpg"
  alt="Hero"
>

Tham số quan trọng của Image Resizing

format=auto — tự động chọn AVIF/WebP/JPEG dựa trên Accept header. fit=cover|contain|crop — kiểm soát cách resize. quality=1-100 — chất lượng nén. sharpen=1-10 — làm sắc nét sau resize. dpr=2 — tự động nhân width cho high-DPI display.

5.3. Self-hosted alternative: Imgproxy

Nếu không dùng Cloudflare paid plan, imgproxy là giải pháp self-hosted mạnh mẽ — viết bằng Go, xử lý ảnh cực nhanh, deploy dễ dàng qua Docker:

# docker-compose.yml
services:
  imgproxy:
    image: darthsim/imgproxy:latest
    environment:
      IMGPROXY_BIND: ":8080"
      IMGPROXY_MAX_SRC_RESOLUTION: 50    # megapixels
      IMGPROXY_PREFERRED_FORMATS: "avif,webp"
      IMGPROXY_AVIF_SPEED: 5
      IMGPROXY_ENABLE_WEBP_DETECTION: "true"
      IMGPROXY_ENABLE_AVIF_DETECTION: "true"
    ports:
      - "8080:8080"

# URL pattern: /resize:fit:width:height/plain/source_url
# Ví dụ: /resize:fill:400:300/plain/https://example.com/photo.jpg@webp

6. Lazy Loading và Preloading — Chiến lược tải thông minh

6.1. Native Lazy Loading

Attribute loading="lazy" đã được hỗ trợ trên toàn bộ browser hiện đại. Quy tắc vàng:

  • KHÔNG lazy load ảnh LCP — ảnh hero, ảnh đầu tiên trên viewport. Thêm fetchpriority="high" thay vì lazy.
  • Lazy load tất cả ảnh below-the-fold — ảnh trong content, gallery, sidebar.
  • Luôn set width/height hoặc aspect-ratio CSS để tránh CLS.
<!-- ✅ Ảnh LCP — load ngay, ưu tiên cao -->
<img
  src="hero.webp"
  alt="Hero banner"
  width="1200" height="630"
  fetchpriority="high"
  decoding="async"
>

<!-- ✅ Ảnh below-the-fold — lazy load -->
<img
  src="product.webp"
  alt="Sản phẩm"
  width="400" height="300"
  loading="lazy"
  decoding="async"
>

6.2. Preload ảnh LCP

Để browser bắt đầu tải ảnh LCP càng sớm càng tốt, dùng <link rel="preload"> trong <head>:

<head>
  <!-- Preload ảnh LCP với format tối ưu -->
  <link
    rel="preload"
    as="image"
    href="hero.avif"
    type="image/avif"
    fetchpriority="high"
    imagesrcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w"
    imagesizes="100vw"
  >
</head>

Đừng preload quá nhiều

Chỉ preload 1-2 ảnh LCP. Preload quá nhiều resource sẽ cạnh tranh bandwidth với CSS và JS critical, làm chậm tổng thể. Dùng Chrome DevTools → Performance panel để xác định ảnh nào thực sự là LCP element.

7. Đo lường tác động lên Core Web Vitals

Image optimization ảnh hưởng trực tiếp đến hai trong ba chỉ số Core Web Vitals:

MetricImage liên quan thế nàoTargetCách tối ưu
LCPẢnh hero thường là LCP element≤ 2.5sAVIF/WebP, preload, CDN, responsive sizes
CLSẢnh không có width/height gây layout shift≤ 0.1Luôn set dimensions, aspect-ratio, placeholder
INPDecode ảnh lớn có thể block main thread≤ 200msdecoding="async", kích thước phù hợp

7.1. Audit checklist cho production

# Lighthouse CLI — audit image optimization
npx lighthouse https://your-site.com \
  --only-categories=performance \
  --output=json \
  --output-path=./report.json

# Các audit liên quan đến ảnh:
# - "Serve images in next-gen formats" (WebP/AVIF)
# - "Properly size images" (responsive)
# - "Efficiently encode images" (compression)
# - "Image elements have explicit width and height"
# - "Preload Largest Contentful Paint image"
# - "Defer offscreen images" (lazy loading)

8. Tích hợp với Vue.js / Nuxt

8.1. Nuxt Image module

@nuxt/image cung cấp component <NuxtImg><NuxtPicture> với auto-optimization, lazy loading, và tích hợp sẵn với nhiều image provider:

<!-- nuxt.config.ts -->
export default defineNuxtConfig({
  modules: ['@nuxt/image'],
  image: {
    provider: 'cloudflare',
    cloudflare: {
      baseURL: 'https://your-site.com'
    },
    formats: ['avif', 'webp'],
    screens: {
      xs: 320,
      sm: 640,
      md: 768,
      lg: 1024,
      xl: 1280,
    }
  }
})

<!-- Component sử dụng -->
<template>
  <NuxtPicture
    src="/images/hero.jpg"
    width="1200"
    height="630"
    sizes="sm:100vw md:50vw lg:33vw"
    format="avif"
    quality="70"
    :placeholder="[20, 10, 75, 5]"
    loading="eager"
    fetchpriority="high"
  />
</template>

8.2. Vue Composable cho Intersection Observer

// useImageLazyLoad.ts
import { ref, onMounted, onUnmounted } from 'vue';

export function useImageLazyLoad(rootMargin = '200px') {
  const imageRef = ref<HTMLImageElement | null>(null);
  const isVisible = ref(false);
  let observer: IntersectionObserver | null = null;

  onMounted(() => {
    if (!imageRef.value) return;

    observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          isVisible.value = true;
          observer?.disconnect();
        }
      },
      { rootMargin }
    );

    observer.observe(imageRef.value);
  });

  onUnmounted(() => observer?.disconnect());

  return { imageRef, isVisible };
}

9. Pipeline tổng hợp cho Production

graph TD
    A["Ảnh gốc upload"] --> B["Build Pipeline"]
    B --> C["Sharp: resize breakpoints
400w, 800w, 1200w, 1600w"] C --> D["Generate AVIF + WebP + JPEG"] D --> E["Generate ThumbHash placeholder"] E --> F["Lưu metadata + ThumbHash vào DB"] D --> G["Upload variants lên Object Storage
R2 / S3"] G --> H["CDN Edge Cache"] H --> I["Client: picture + srcset + lazy load"] F --> I style A fill:#e94560,stroke:#fff,color:#fff style B fill:#2c3e50,stroke:#fff,color:#fff style H fill:#4CAF50,stroke:#fff,color:#fff style I fill:#16213e,stroke:#fff,color:#fff

Hình 4: Production image pipeline end-to-end

Checklist tối ưu ảnh cho production

  • ✅ Serve AVIF với WebP fallback qua <picture>
  • ✅ Responsive images với srcset + sizes cho mọi ảnh content
  • ✅ Preload ảnh LCP, lazy load phần còn lại
  • ✅ Luôn set width/height hoặc aspect-ratio
  • ✅ Dùng decoding="async" cho mọi ảnh
  • ✅ Generate placeholder (ThumbHash hoặc LQIP) cho perceived performance
  • ✅ CDN edge caching với format auto-negotiation
  • ✅ Build pipeline tự động: Sharp → multi-format → upload → CDN
  • ✅ Monitor LCP và CLS trong RUM (Real User Monitoring)
  • ✅ Audit định kỳ bằng Lighthouse CI

10. Kết luận

Image optimization là low-hanging fruit với ROI cực cao. Chỉ cần chuyển sang WebP/AVIF và implement responsive images đúng cách, bạn có thể giảm 50-70% dung lượng ảnh, cải thiện LCP 1-3 giây, và nâng điểm Core Web Vitals từ đỏ sang xanh. Kết hợp thêm placeholder strategies và CDN edge optimization, trải nghiệm người dùng sẽ nâng lên một tầm hoàn toàn mới.

Đầu tư vào image pipeline tự động hóa (Sharp + build tool + CDN) là khoản đầu tư một lần nhưng hưởng lợi vĩnh viễn cho mọi ảnh upload sau đó.

Tham khảo