Hình dung tình huống: mở Lighthouse cho một project cần tối ưu performance, thấy dòng cảnh báo đỏ lòm: “Properly size images, potential savings 2.1 MB”. Trang web của team chỉ có 5 ảnh thôi, nhưng toàn ảnh gốc 4000×3000 từ máy ảnh, serve thẳng cho mọi thiết bị. Điện thoại 400px chiều ngang cũng phải tải ảnh 4MB. LCP rơi xuống 6 giây trên 3G, bounce rate tăng, SEO tụt hạng.
HTTP Archive 2025 ghi nhận trung bình khoảng 900 KB ảnh trên mỗi trang desktop top site, chiếm phần lớn trọng lượng trang web. Chọn format sai, serve kích thước sai, thiếu lazy load, bất kỳ sai lầm nào trong ba thứ đó cũng đủ phá hỏng trải nghiệm user. Bài này đi qua cách tiếp cận format ảnh cho web: không chỉ “chọn AVIF là xong” mà còn là pipeline encode, HTML khai báo đúng, và cách đo kết quả.
Phân loại ảnh trước, chọn format sau
Sai lầm phổ biến nhất là nhiều dev nhảy thẳng vào tranh luận “PNG hay AVIF” mà quên bước đầu tiên: phân loại nội dung ảnh. Mỗi loại ảnh có đặc tính khác nhau, format phù hợp cũng khác nhau.
Ảnh chụp, photo, illustration có tông màu liên tục, đây là nơi lossy compression toả sáng. JPEG, WebP lossy, hoặc AVIF lossy đều nén cực tốt vì thuật toán exploit được sự tương đồng màu sắc giữa các pixel lân cận. Chất lượng giảm một chút mà mắt thường gần như không phân biệt được, nhưng file size giảm 5-10 lần.
Ảnh đồ hoạ, UI, screenshot, diagram, loại này có viền sắc, text rõ ràng, vùng màu phẳng. Lossy compression hay tạo artifact quanh viền chữ và cạnh sắc, nhìn nhoè và unprofessional. PNG lossless hoặc WebP lossless giữ pixel chính xác, phù hợp hơn nhiều.
Vector, logo, icon, illustration phẳng, thuộc về SVG. File nhỏ, scale vô hạn không bao giờ bị vỡ pixel, và có thể style bằng CSS. Không có lý do gì để xuất logo thành PNG 5 kích thước khi SVG một file giải quyết hết.
Ảnh cần transparency, nếu ảnh có alpha channel (nền trong suốt), JPEG không hỗ trợ. Chọn PNG, WebP, hoặc AVIF, cả ba đều có alpha. Hay gặp team convert ảnh PNG có transparency sang JPEG rồi thắc mắc “sao nền đen thế”, JPEG fill alpha thành đen mặc định.
Nắm được bốn nhóm này rồi thì chọn format cho từng ảnh trở nên rất rõ ràng, không cần tranh luận.
PNG, lossless và pixel-perfect
PNG ra đời năm 1996 để thay thế GIF (vì bản quyền LZW lúc đó), và đến giờ vẫn là lựa chọn mặc định khi bạn cần ảnh không mất chất lượng. Mỗi pixel được lưu chính xác, không có artifact, không có nhoè. Có alpha channel cho transparency.
Nhược điểm duy nhất nhưng rất quan trọng: file PNG to. Một screenshot 1920×1080 có thể 1-3 MB tùy nội dung. Ảnh photo thì tệ hơn, PNG lossless cho photo có thể 5-10 MB, trong khi JPEG cùng chất lượng cảm quan chỉ vài trăm KB. Đó là lý do PNG không phù hợp cho photo trên web.
Nhưng với screenshot UI, diagram, logo có viền sắc, PNG là lựa chọn đúng. PNG phù hợp cho screenshot trong blog kỹ thuật vì text trong screenshot phải đọc được rõ ràng, lossy compression làm nhoè text trông rất tệ.
Tối ưu PNG không khó. pngquant giảm palette màu, với ảnh không cần full 16 triệu màu (hầu hết screenshot), giảm 50-70% file size mà mắt thường gần như không thấy khác biệt. oxipng tối ưu filter và compression trong PNG mà không thay đổi bất kỳ pixel nào, lossless 100%. zopflipng của Google nén mạnh hơn oxipng nhưng chậm hơn, phù hợp cho build-time pipeline.
Workflow hiệu quả: chạy pngquant trước (lossy nhưng visually lossless), rồi oxipng sau, combo này giảm 60-80% file size cho screenshot thông thường.
JPEG, tiêu chuẩn 30 năm, vẫn chạy tốt
JPEG tồn tại từ đầu những năm 90 và vẫn là format phổ biến nhất trên web cho photo. Mọi trình duyệt, mọi thiết bị, mọi công cụ đều hỗ trợ, không bao giờ phải lo compatibility.
Không có alpha channel, đó là hạn chế cố hữu của JPEG. Nếu ảnh cần transparency thì dùng format khác.
Về chất lượng, quality=75-82 thường là sweet spot cho ảnh photo trên web. Dưới 70 bắt đầu thấy artifact ở vùng gradient và cạnh sắc. Trên 85 thì file size tăng đáng kể mà cải thiện visual gần như không nhận ra trên màn hình thông thường. Tất nhiên con số này tuỳ nội dung ảnh, ảnh có nhiều text hay chi tiết nhỏ cần quality cao hơn ảnh landscape.
Chroma sub-sampling là khái niệm ít dev biết nhưng ảnh hưởng lớn. JPEG mặc định dùng 4:2:0, giảm resolution thông tin màu sắc xuống một nửa theo cả hai chiều, vì mắt người nhạy với độ sáng hơn màu sắc. Với photo thông thường, 4:2:0 hoàn toàn ổn. Nhưng nếu ảnh có chi tiết màu sắc quan trọng, text đỏ trên nền xanh, biểu đồ với nhiều màu rõ ràng, thì 4:4:4 (không sub-sample) giữ chi tiết tốt hơn.
Progressive JPEG là tính năng nên luôn bật. Thay vì render ảnh từ trên xuống dưới (baseline JPEG), progressive JPEG render toàn bộ ảnh ở chất lượng thấp trước, rồi tăng dần chất lượng. User thấy ảnh mờ ngay lập tức thay vì chờ nửa ảnh load xong, UX tốt hơn đáng kể trên mạng chậm.
Tool encode phù hợp là mozjpeg, fork của libjpeg tối ưu compression, nén chặt hơn 10-15% so với libjpeg-turbo ở cùng chất lượng nhìn. Nếu dùng Node thì sharp wrap mozjpeg sẵn.
WebP, bước tiến từ Google
WebP ra mắt năm 2010 từ Google, hỗ trợ cả lossy lẫn lossless, có alpha channel, và cả animation. Về cơ bản nó làm được mọi thứ mà PNG và JPEG làm, nhưng với file size nhỏ hơn.
Con số thường được trích dẫn: WebP lossy nhỏ hơn JPEG khoảng 25-35% ở cùng chất lượng cảm quan. WebP lossless nhỏ hơn PNG khoảng 20-30%. Những con số này phụ thuộc rất nhiều vào nội dung ảnh, có ảnh WebP chỉ nhỏ hơn 10%, có ảnh nhỏ hơn 50%. Nhưng nhìn chung, chuyển từ JPEG sang WebP gần như luôn có lợi.
Về browser support, tính đến 2025 thì khoảng 97% trình duyệt đang dùng hỗ trợ WebP, tất cả trình duyệt hiện đại, Safari từ 2020. Trừ khi bạn vẫn phải hỗ trợ IE11 (mong là không), WebP là lựa chọn an toàn. Nếu cần fallback cho trình duyệt cũ, dùng <picture> element với JPEG/PNG làm fallback, sẽ nói ở phần sau.
WebP animation cũng đáng chú ý, thay thế GIF animation. GIF animation file cực nặng vì nén tệ và giới hạn 256 màu. Cùng animation đó, WebP nhỏ hơn GIF 3-5 lần và chất lượng tốt hơn nhiều.
Tool encode: cwebp (CLI chính thức từ Google), sharp (Node), ImageMagick. sharp integrate vào build pipeline dễ dàng.
AVIF, thế hệ mới, nén cực mạnh
AVIF (AV1 Image File Format) kế thừa codec AV1 từ video, ra mắt khoảng 2019, và đang nhanh chóng trở thành format mặc định mới cho photo trên web.
Về compression, AVIF thường nhỏ hơn WebP 20-50% ở cùng chất lượng cảm quan, con số này ấn tượng. Một ảnh hero banner 200 KB dạng WebP có thể chỉ 100-120 KB dạng AVIF. Nhân lên cho một trang có 10-20 ảnh, tổng tiết kiệm rất đáng kể.
AVIF hỗ trợ HDR, 10/12-bit color depth, alpha channel, và cả animation. Về kỹ thuật, nó superior hơn WebP ở hầu hết mọi mặt. Browser support tính đến 2025 khoảng 90% globally, Chrome, Edge, Firefox, Safari 16+ đều hỗ trợ. Vẫn cần WebP hoặc JPEG fallback cho trình duyệt cũ, nhưng AVIF nên là format ưu tiên đầu tiên trong <picture>.
Tuy nhiên, AVIF có hai nhược điểm quan trọng mà bạn phải biết trước khi adopt.
Thứ nhất, encode chậm, chậm hơn nhiều so với WebP và JPEG. Tuỳ cài đặt, AVIF encode có thể chậm 10-50 lần so với JPEG. Điều này nghĩa là nếu bạn có pipeline encode ảnh real-time (user upload ảnh, server encode rồi serve ngay), AVIF encode synchronous sẽ là bottleneck nghiêm trọng. Giải pháp là encode AVIF async, serve JPEG/WebP ngay, push AVIF encode vào job queue, thay ảnh khi AVIF sẵn sàng. Hoặc dùng CDN ảnh để CDN lo encode.
Thứ hai, AVIF ở quality thấp có thể tạo artifact tệ hơn WebP/JPEG cho ảnh có text nhỏ hoặc chi tiết sắc nét. Ví dụ: convert screenshot code editor sang AVIF quality 30, text bị nhoè đến mức không đọc được, trong khi WebP lossless cùng ảnh đó trông perfect. Với text-heavy screenshot, PNG hoặc WebP lossless vẫn là lựa chọn tốt hơn. Luôn test bằng mắt trước khi apply AVIF cho toàn bộ ảnh, không phải format “thần kỳ” dùng cho mọi thứ.
Tool: avifenc (libavif CLI), sharp (Node, dùng libavif bên dưới), ImageMagick 7.
SVG, vector cho icon và logo
SVG không nằm trong cùng nhóm với PNG/WebP/AVIF vì nó là vector, không phải raster. Nhưng đáng đề cập vì nhiều team vẫn xuất logo và icon thành PNG rồi serve 5-6 kích thước, trong khi SVG một file giải quyết hết, scale vô hạn, và thường nhỏ hơn nhiều.
SVG là XML-based, có thể đọc và edit bằng text editor. Phù hợp tuyệt đối cho logo, icon, illustration phẳng (flat illustration). Với những icon nhỏ dùng nhiều lần trên trang, inline trực tiếp vào HTML để giảm HTTP request, không cần tải thêm file.
Nhớ minify SVG bằng SVGO trước khi deploy, SVG từ Figma hay Illustrator xuất ra thường chứa rất nhiều metadata, comment, và attribute thừa. SVGO giảm 30-60% file size mà không ảnh hưởng visual.
Một cảnh báo bảo mật quan trọng: nếu bạn cho phép user upload SVG, phải sanitize cực kỹ. SVG có thể chứa <script> tag, external references, và nhiều vector tấn công XSS khác. Strip tất cả script, event handler (onload, onclick), và external refs trước khi serve. Tốt nhất là convert SVG upload thành raster (PNG) hoặc re-render qua library an toàn, không bao giờ serve SVG user upload trực tiếp.
Quyết định format nhanh
Sau khi hiểu đặc tính từng format, tóm lại cách quyết định nhanh cho từng loại ảnh.
Ảnh banner, hero, photo, serve AVIF là ưu tiên đầu, WebP fallback thứ hai, JPEG fallback cuối cùng. Ba format trong <picture> element, trình duyệt tự chọn cái tốt nhất nó hỗ trợ.
Thumbnail photo, WebP với JPEG fallback thường đủ. Thumbnail nhỏ nên sự khác biệt giữa AVIF và WebP không đáng kể, trong khi AVIF encode tốn CPU hơn nhiều. Trừ khi bạn có hàng triệu thumbnail thì savings mới đáng kể.
Screenshot UI, diagram, PNG (qua pngquant + oxipng) hoặc WebP lossless. Text phải sắc nét, không artifact.
Logo, icon, SVG. Inline nếu icon nhỏ và dùng nhiều trên trang.
Animation, WebP animation hoặc AVIF animation cho clip ngắn. Với animation dài (hơn vài giây), dùng <video> tag với MP4/WebM, nhẹ hơn và chất lượng tốt hơn bất kỳ animated image format nào. Tuyệt đối không dùng GIF cho animation lớn, file nặng gấp 3-5 lần WebP animation và chất lượng tệ hơn nhiều với palette chỉ 256 màu.
Responsive images, <picture> và srcset
Chọn đúng format chỉ là một nửa bài toán. Nửa còn lại là serve đúng kích thước cho đúng thiết bị. Điện thoại 400px chiều ngang không cần ảnh 1600px, tải thừa 4 lần pixel, tốn bandwidth, chậm load.
Fallback format với <picture>
<picture> element cho phép bạn liệt kê nhiều source format, trình duyệt duyệt từ trên xuống và chọn <source> đầu tiên mà nó hỗ trợ:
<picture>
<source type="image/avif" srcset="/img/hero.avif" />
<source type="image/webp" srcset="/img/hero.webp" />
<img
src="/img/hero.jpg"
alt="Hero"
width="1600"
height="900"
loading="lazy"
/>
</picture>
Chrome hiểu AVIF thì lấy AVIF. Safari cũ chỉ hiểu WebP thì lấy WebP. IE11 (nếu còn ai dùng) fallback về <img src> JPEG. Trình duyệt tự chọn, không cần JavaScript, không cần server-side detection.
Luôn có <img> fallback, đây không phải optional. Và luôn đặt width/height thực tế trên <img> để trình duyệt reserve layout slot trước khi ảnh tải xong. Thiếu width/height là nguyên nhân phổ biến nhất gây CLS (Cumulative Layout Shift), trang nhảy loạn khi ảnh load xong và đẩy content xuống.
Chọn resolution với srcset và sizes
srcset cho phép bạn liệt kê nhiều phiên bản kích thước của cùng một ảnh, và sizes nói cho trình duyệt biết ảnh sẽ hiển thị ở kích thước bao nhiêu trên mỗi breakpoint:
<img
src="/img/hero-800.jpg"
srcset="
/img/hero-400.jpg 400w,
/img/hero-800.jpg 800w,
/img/hero-1600.jpg 1600w
"
sizes="(min-width: 1024px) 50vw, 100vw"
alt="Hero"
width="1600"
height="900"
loading="lazy"
/>
srcset liệt kê các phiên bản theo width descriptor (400w, 800w, 1600w). sizes nói với trình duyệt rằng trên màn hình từ 1024px trở lên, ảnh chiếm 50% viewport width, còn lại chiếm 100%. Trình duyệt dựa vào thông tin này cộng với device pixel ratio để chọn ảnh vừa đủ phân giải, điện thoại 400px lấy ảnh 400w, desktop retina lấy ảnh 1600w.
Mấu chốt ở đây: trình duyệt cần sizes để quyết định đúng. Nếu bạn chỉ có srcset mà không có sizes, trình duyệt mặc định assume ảnh chiếm 100vw, trên desktop mà ảnh thực tế chỉ chiếm 50% layout thì trình duyệt sẽ tải ảnh to gấp đôi cần thiết.
Kết hợp format fallback và responsive
Trong thực tế, bạn kết hợp cả <picture> (format fallback) và srcset/sizes (resolution selection):
<picture>
<source
type="image/avif"
srcset="
/img/hero-400.avif 400w,
/img/hero-800.avif 800w,
/img/hero-1600.avif 1600w
"
sizes="(min-width: 1024px) 50vw, 100vw"
/>
<source
type="image/webp"
srcset="
/img/hero-400.webp 400w,
/img/hero-800.webp 800w,
/img/hero-1600.webp 1600w
"
sizes="(min-width: 1024px) 50vw, 100vw"
/>
<img
src="/img/hero-800.jpg"
srcset="
/img/hero-400.jpg 400w,
/img/hero-800.jpg 800w,
/img/hero-1600.jpg 1600w
"
sizes="(min-width: 1024px) 50vw, 100vw"
alt="Hero"
width="1600"
height="900"
loading="lazy"
/>
</picture>
HTML nhiều hơn, nhưng kết quả là trình duyệt luôn chọn format tốt nhất và kích thước vừa đủ cho thiết bị hiện tại. Một ảnh hero 1600px AVIF có thể 150 KB trên desktop, nhưng cùng ảnh đó trên điện thoại chỉ cần phiên bản 400px, 20 KB. Tiết kiệm 7 lần bandwidth cho mobile user.
Lazy loading và fetch priority
Không phải ảnh nào cũng cần tải ngay khi trang load. Ảnh ngoài viewport, ảnh mà user phải scroll xuống mới thấy, có thể hoãn tải cho đến khi gần xuất hiện. Đây là lazy loading, và nó giảm đáng kể lượng data cần tải ban đầu.
loading="lazy", đơn giản nhất
<img src="/img/below-fold.jpg" loading="lazy" alt="..." />
Một attribute duy nhất, native browser support, không cần JavaScript library. Trình duyệt tự quyết định khi nào bắt đầu tải, thường là khi ảnh gần viewport (vài trăm pixel trước khi user scroll đến).
Nhưng có một sai lầm cực kỳ phổ biến hay gặp: đặt loading="lazy" cho ảnh above-the-fold, đặc biệt là ảnh LCP (Largest Contentful Paint). Ảnh LCP là ảnh lớn nhất visible khi trang vừa load, thường là hero banner. Đặt lazy cho ảnh này nghĩa là trình duyệt hoãn tải nó, gây regression LCP 500-1500ms trên mạng chậm. Ảnh LCP phải dùng loading="eager" (mặc định nếu không set attribute).
Quy tắc đơn giản: ảnh above-the-fold (nhìn thấy ngay khi trang load, không cần scroll), eager hoặc không set loading. Ảnh below-the-fold, loading="lazy".
fetchpriority, ưu tiên tải ảnh LCP
<img src="/img/hero.jpg" fetchpriority="high" alt="Hero" />
fetchpriority="high" nói với trình duyệt rằng ảnh này quan trọng, ưu tiên tải trước các resource khác. Thường dùng cho ảnh LCP, kết hợp với loading="eager" (hoặc không set loading) và preload để LCP nhanh nhất có thể.
Đừng đặt fetchpriority="high" cho nhiều ảnh, nếu tất cả đều “high priority” thì không ảnh nào thực sự được ưu tiên. Chỉ dùng cho 1-2 ảnh quan trọng nhất trên trang.
decoding="async", decode off main thread
<img decoding="async" src="/img/content.jpg" alt="..." />
Báo trình duyệt rằng việc decode ảnh (chuyển từ compressed format thành pixel) có thể chạy off-main-thread, không block rendering. Đặc biệt hữu ích với ảnh lớn, decode ảnh 4K trên main thread có thể gây long task 50-100ms, ảnh hưởng INP.
Nên đặt decoding="async" cho hầu hết ảnh. Nó an toàn và gần như không có downside.
Preload ảnh LCP
<link
rel="preload"
as="image"
imagesrcset="/img/hero-800.avif 800w, /img/hero-1600.avif 1600w"
imagesizes="(min-width: 1024px) 50vw, 100vw"
type="image/avif"
/>
Preload nói trình duyệt bắt đầu tải ảnh ngay từ khi parse HTML, không đợi đến khi gặp <img> tag trong DOM. Với ảnh LCP, preload có thể cải thiện LCP 200-500ms vì trình duyệt bắt đầu fetch sớm hơn.
Nhưng preload là cam kết, trình duyệt sẽ tải resource đó bất kể có dùng hay không. Preload bừa bãi nhiều ảnh nghĩa là tốn bandwidth cho ảnh có thể không cần. Chỉ preload ảnh mà bạn chắc chắn sẽ dùng, thường là ảnh LCP duy nhất.
Pipeline encode ảnh
CDN ảnh, giải pháp nếu budget cho phép
Cloudflare Images, imgix, Cloudinary, Akamai Image Manager, những service này nhận URL ảnh gốc, trả về ảnh đã tối ưu tự động: chọn format theo Accept header của trình duyệt, resize theo parameter, cache sẵn.
/cdn/hero.jpg?w=800&q=75
Trình duyệt gửi Accept: image/avif,image/webp,*/*, CDN trả AVIF. Trình duyệt cũ gửi Accept: */*, CDN trả JPEG. Không cần <picture> element phức tạp, không cần sinh nhiều file format.
Không phải project nào cũng cần CDN ảnh, blog cá nhân hay site nhỏ thì overkill. Nhưng site có nhiều ảnh user upload (marketplace, social, CMS) thì CDN ảnh tiết kiệm vô vàn effort encode, resize, và cache.
Self-host với sharp
Nếu tự host ảnh, sharp (Node.js library, wrapper libvips, rất nhanh) phù hợp trong build pipeline:
import sharp from "sharp";
async function build(input, outDir) {
const sizes = [400, 800, 1600];
for (const w of sizes) {
await sharp(input)
.resize({ width: w })
.jpeg({ quality: 78, mozjpeg: true })
.toFile(`${outDir}/hero-${w}.jpg`);
await sharp(input)
.resize({ width: w })
.webp({ quality: 78 })
.toFile(`${outDir}/hero-${w}.webp`);
await sharp(input)
.resize({ width: w })
.avif({ quality: 50 })
.toFile(`${outDir}/hero-${w}.avif`);
}
}
Chạy trong build-time, Next.js, Nuxt, Astro, Hugo pipes đều support. Ảnh encode một lần khi build, serve static file đã tối ưu, không cần encode khi request. Cache CDN hiệu quả vì file không thay đổi giữa các build (trừ khi ảnh gốc thay đổi).
Lưu ý quality setting khác nhau giữa format: quality: 78 cho JPEG không tương đương quality: 78 cho WebP hay quality: 50 cho AVIF. Mỗi format có compression algorithm khác nhau, quality scale khác nhau. Thường set JPEG 75-82, WebP 75-82, AVIF 45-55, ở các mức này, chất lượng cảm quan gần tương đương nhau.
Chi phí CPU encode
Đây là thứ ít người tính trước nhưng ảnh hưởng lớn đến architecture. AVIF encode chậm 10-50 lần so với JPEG trên cùng hardware. Encode 100 ảnh AVIF trong build pipeline có thể mất vài phút thay vì vài giây với JPEG.
Nếu ảnh encode build-time (static site, content pipeline) thì không sao, chờ thêm vài phút trong CI không phải vấn đề lớn. Nhưng nếu user upload ảnh và bạn cần serve ngay, encode AVIF synchronous sẽ block request hoặc tốn quá nhiều CPU.
Giải pháp thực tế: serve JPEG/WebP ngay (encode nhanh), push AVIF encode vào background job queue (Bull, Sidekiq, Celery), khi AVIF xong thì cập nhật URL. Lần request sau user nhận AVIF. Hoặc đơn giản hơn, dùng CDN ảnh và để CDN lo toàn bộ encode.
Ảnh hưởng đến Core Web Vitals
Ảnh ảnh hưởng trực tiếp đến ba chỉ số Core Web Vitals mà Google dùng để đánh giá trải nghiệm user.
LCP (Largest Contentful Paint), thường là ảnh hero hoặc banner. Đây là metric mà tối ưu ảnh ảnh hưởng nhiều nhất. Combo tối ưu LCP: format tốt nhất (AVIF > WebP > JPEG), preload với <link rel="preload">, fetchpriority="high", và tuyệt đối không loading="lazy". Ví dụ: chuyển ảnh hero từ PNG 2MB sang AVIF 150KB + preload có thể cải thiện LCP từ 4.2s xuống 1.8s.
CLS (Cumulative Layout Shift), ảnh gây CLS khi trình duyệt không biết kích thước ảnh trước khi load xong. Giải pháp đơn giản: luôn có width và height attribute trên <img>, hoặc dùng aspect-ratio trong CSS. Trình duyệt sẽ reserve đúng kích thước layout slot, không nhảy khi ảnh load xong. Đây là lỗi CLS phổ biến nhất và dễ fix nhất, nhưng nhiều dev vẫn quên.
INP (Interaction to Next Paint), decode ảnh lớn trên main thread có thể gây long task, ảnh hưởng responsiveness. Dùng decoding="async" để trình duyệt decode off-thread. Ngoài ra, ảnh quá lớn (cả dimension và file size) cũng tốn memory, gây GC pause trên thiết bị yếu.
Kiểm tra bằng Lighthouse (lab data), PageSpeed Insights (cả field data lẫn lab data), và WebPageTest filmstrip để xem chuỗi load thực tế. Đo trước khi tối ưu, đo sau khi tối ưu, để chắc chắn thay đổi thực sự có lợi chứ không phải cảm tính.
Cạm bẫy thường gặp
Dưới đây là những sai lầm hay gặp, để bạn tránh lặp lại.
Serve ảnh gốc cho mọi kích thước
Đây là sai lầm phổ biến nhất. Ảnh gốc từ máy ảnh 4000×3000, serve thẳng cho thumbnail 200×200 trên danh sách sản phẩm. Browser resize xuống visual đúng, nhưng user đã tải vài MB thay vì vài KB. Pipeline phải sinh nhiều kích thước, 400w, 800w, 1600w tối thiểu, và dùng srcset để trình duyệt chọn đúng size.
Ví dụ: một trang e-commerce có 40 product thumbnail trên trang danh sách, mỗi thumbnail serve ảnh gốc 3MB. Tổng cộng user phải tải 120MB ảnh chỉ cho một page. Resize thành 400×400 WebP, mỗi thumbnail 15KB, tổng trang giảm từ 120MB xuống 600KB. LCP cải thiện 5 lần.
Lazy load ảnh LCP
Đã nói ở trên nhưng nhắc lại vì quá phổ biến. Nhiều dev apply loading="lazy" cho tất cả ảnh vì “lazy load là tối ưu”. Không, lazy load cho ảnh LCP là regression. Gây chậm LCP 500-1500ms trên mạng thật. Ảnh LCP phải eager load, fetchpriority high, và preload nếu có thể.
Mất transparency khi convert JPEG
JPEG không có alpha channel. Khi convert PNG có transparency sang JPEG, tool sẽ fill nền bằng màu nào đó, thường là đen hoặc trắng tuỳ tool. Nếu bạn không kiểm tra output, ảnh product trên nền trong suốt bỗng có nền đen. WebP và AVIF đều hỗ trợ alpha, nếu ảnh cần transparency, dùng format có alpha.
AVIF cho text-heavy screenshot
Ở quality thấp, AVIF nhoè text tệ hơn WebP lossless hay PNG. Test thực tế, screenshot code editor, terminal output, diagram có nhiều text nhỏ, AVIF lossy quality 40 làm text gần như không đọc được ở zoom 100%. WebP lossless giữ text sắc nét hoàn hảo. Kết luận: đừng apply AVIF lossy cho mọi loại ảnh. Test bằng mắt với content thực tế trước khi quyết định.
Quên cache-control dài hạn
Ảnh đã được versioned trong URL (hash trong filename: hero-abc123.jpg) thì nên set Cache-Control: public, max-age=31536000, immutable, cache một năm, CDN và browser không cần re-validate. Nếu URL không có version hash, set hợp lý hơn, max-age=86400 một ngày chẳng hạn, và dùng ETag/Last-Modified để browser có thể conditional request.
Cache-control đúng giảm bandwidth đáng kể cho returning user, lần đầu tải 900KB ảnh, lần sau tải 0KB vì tất cả đã có trong cache.
Tối ưu ảnh cho social share
Open Graph (og:image) và Twitter Card cần ảnh ở format mà social crawler hiểu. Và đây là điểm nhiều dev bỏ qua: social crawler (Facebook, Twitter, LinkedIn, Slack) thường không hỗ trợ AVIF, một số không hỗ trợ WebP. JPEG hoặc PNG là lựa chọn an toàn nhất cho og:image.
Kích thước recommend cho OG image là 1200×630 pixel, format JPEG, chất lượng 80-85. Serve qua URL riêng biệt với URL ảnh trên trang web, ảnh trên trang dùng AVIF/WebP tối ưu, og:image dùng JPEG compatible.
Ví dụ: set og:image trỏ đến file AVIF, share trên Facebook thì hiện placeholder xám, không hiện ảnh. Đổi sang JPEG URL riêng là fix ngay. Đơn giản nhưng dễ quên.
Tóm tắt
Phân loại ảnh trước, chọn format sau, SVG cho vector, PNG cho UI screenshot text-heavy, WebP/AVIF lossy cho photo, JPEG làm fallback. Triển khai <picture> với srcset/sizes thay vì chỉ một <img src> duy nhất, để trình duyệt tự chọn format tốt nhất và kích thước vừa đủ cho thiết bị.
Width/height hoặc aspect-ratio là bắt buộc trên mọi ảnh để tránh CLS. Lazy load ảnh ngoài viewport, nhưng tuyệt đối không lazy load ảnh LCP, preload nó, set fetchpriority="high", và đảm bảo format đã tối ưu.
Build-time encode với sharp hoặc dùng CDN ảnh cho user upload. Đo Core Web Vitals trước và sau để xác nhận thay đổi thực sự có lợi, Lighthouse score tăng mà field data không cải thiện thì chưa xong.
Ảnh tốt trên web không phải là “chọn AVIF cho mọi thứ”. Đó là pipeline tự động sinh đúng size + đúng format cho đúng loại ảnh, HTML khai báo đầy đủ cho trình duyệt tự chọn, và ưu tiên hợp lý giữa ảnh critical (LCP) và ảnh phụ (lazy load). Sai bất kỳ mắt xích nào, toàn bộ công sức tối ưu cũng giảm đáng kể.