next/image: tối ưu ảnh, đo trước và sau
Ảnh thường là thứ nặng nhất trên một trang. Trang này dựng một ứng dụng thật trên next@16.4.4, đặt cùng một tệp 3000×2000 (402 KB) theo bốn cách, rồi đo từng thứ bằng HTTP: số byte tải về, định dạng mà trình duyệt nhận được, header cache, và những gì HTML hứa với trình duyệt trước khi ảnh tới. Không có con số nào dưới đây viết từ ký ức.
Mã trên trang này là một ứng dụng chạy được: 22/22 kiểm tra đạt ngày 02/11/2026. Phiên bản đã chạy thử: next@16.4.4, react@19.3.0, react-dom@19.3.0, Node.js v26.0.0.
Bốn cách đặt một tấm ảnh
Ứng dụng mẫu có một tệp photo.jpg 3000×2000, 402 KB. Trang đặt nó bốn lần: một thẻ <img> thường trỏ vào tệp đã build, một next/image ở đầu trang với preload, một next/image nằm dưới màn hình đầu, và một ảnh lấy từ public/ với quality={65}.
Ảnh mẫu do chúng tôi sinh ra (đồi và mặt trời lúc chiều), nên không vướng bản quyền của ai. Nó khá mịn, vì vậy tỉ lệ nén cao hơn một bức ảnh chụp nhiều chi tiết — con số cho ảnh của bạn sẽ khác. Cách đo thì y như vậy, và đó mới là phần đáng mang về.
import Image from "next/image";
import photo from "./photo.jpg";
const ALT = "Ảnh mẫu: đồi và bầu trời lúc chiều";
export default function Home() {
return (
<main>
<h1>next/image: đo trước và sau</h1>
<p>Cùng một tệp JPEG 3000×2000 (1,2 MB), bốn cách đặt lên trang.</p>
<h2>1. Thẻ img thường</h2>
<img src={photo.src} alt={ALT} width={3000} height={2000} />
<h2>2. next/image, ảnh đầu trang</h2>
<Image src={photo} alt={ALT} sizes="(max-width: 640px) 100vw, 640px" preload />
<h2>3. next/image, ảnh dưới màn hình đầu</h2>
<Image src={photo} alt={ALT} width={480} height={320} sizes="480px" placeholder="blur" />
<h2>4. Ảnh trong public/ và một quality không có trong danh sách</h2>
<Image src="/banner.jpg" alt="Băng rôn" width={900} height={600} sizes="900px" quality={65} />
</main>
);
}import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
// AVIF first: a photo is smaller as AVIF than as WebP, at the cost of a slower first encode.
formats: ["image/avif", "image/webp"],
// Next.js 16 accepts only quality 75 unless you list others. 65 is deliberately missing:
// asking for it is coerced to 75 without a warning.
qualities: [50, 75, 90],
},
};
export default nextConfig;Trình tối ưu ảnh làm gì
Với ảnh import tĩnh, next/image không đưa trình duyệt tệp gốc. Nó viết một URL /_next/image?url=…&w=640&q=75; server đọc tham số, cắt ảnh theo w, nén theo q, và trả về định dạng mà request Accept cho phép. Ứng dụng mẫu khai formats: ['image/avif', 'image/webp'] (mặc định chỉ có image/webp).
Đo được trên cùng một tệp 402 KB: bề rộng 640 ra 11 KB AVIF hoặc 14 KB WebP; bề rộng 1920 ra 48 KB AVIF; bề rộng 384 ra 5 KB, 1080 ra 23 KB. Đó là chỗ phần lớn số byte tiết kiệm đến từ: không phải định dạng, mà là không gửi 3000 điểm ảnh cho một ô rộng 640.
Một chi tiết dễ bỏ qua: thẻ img vẫn mang src trỏ tới **bề rộng lớn nhất** (ở đây w=3840). Trình duyệt nào không hiểu srcset sẽ tải đúng ảnh đó; mọi trình duyệt hiện nay thì chọn theo srcset và sizes.
q=75 là mặc định. Hạ q xuống 50 trên cùng bề rộng 640 đo được 5 KB, nhưng số q của AVIF và WebP không cùng thang: ở q=75, AVIF ra 11 KB còn WebP ra 14 KB — đừng suy ra chất lượng lượng giác từ hai con số đó, hãy nhìn ảnh.
srcset, sizes: trình duyệt chọn độ phân giải nào
next/image sinh srcset gồm các bề rộng trong deviceSizes (mặc định 640, 750, 828, 1080, 1200, 1920, 2048, 3840). Đo được 8 bề rộng trong HTML của ảnh đầu trang. Trình duyệt lấy sizes để biết ảnh chiếm bao nhiêu điểm ảnh trên màn hình, nhân với tỉ lệ điểm ảnh, rồi chọn mức nhỏ nhất đủ dùng.
Vì vậy sizes sai là tiền mất thật: sizes="100vw" cho một ảnh chỉ rộng 640 điểm ảnh khiến điện thoại tải ảnh 1080 hoặc 1920 thay vì 640. Trong ứng dụng mẫu, ảnh đầu trang khai sizes="(max-width: 640px) 100vw, 640px", còn ảnh cố định 480×320 khai sizes="480px"; ở màn hình 2x, ô 480 điểm ảnh cần 960, nên trình duyệt lấy mức 1080.
Kinh nghiệm: sizes phải mô tả đúng bề rộng ảnh chiếm trên trang ở từng breakpoint — thường là cùng một biểu thức với CSS của bạn. Ảnh trong lưới 3 cột thì 33vw, không phải 100vw.
preload thay cho priority, còn fetchpriority thì tự đặt
Tài liệu Next.js 16 ghi: priority đã bị bỏ dần để nhường chỗ cho preload, cho rõ nghĩa. Ứng dụng mẫu dùng preload; kết quả là một thẻ <link rel="preload" as="image"> trong head, mang imageSrcSet đủ 8 bề rộng và imageSizes — trình duyệt bắt đầu tải ảnh trước khi đọc tới thẻ img.
Đo thêm một chỗ khác với kỳ vọng: ảnh đó **không** có thuộc tính fetchpriority trên thẻ img. preload chỉ đặt link và để ảnh tải ngay (không loading="lazy"). Muốn trình duyệt ưu tiên ở mức cao nhất, truyền thêm fetchPriority="high" — nó được chuyển thẳng xuống thẻ img.
Chỉ đặt preload cho một ảnh: ảnh LCP. Tài liệu cũng lưu ý ngược lại — khi có nhiều ảnh có thể là LCP tuỳ màn hình, dùng loading="eager" hoặc fetchPriority="high" thay vì preload cho tất cả, vì nhiều preload cùng lúc thì không ảnh nào còn được ưu tiên.
Không nhảy bố cục, có chỗ trống khi chờ
Ảnh dưới màn hình đầu trong ứng dụng mẫu ra HTML với loading="lazy", width="480", height="320" và một background-image là data URI. Chiều cao biết trước nên trang không nhảy khi ảnh tới; tải trễ nên ảnh không tranh băng thông với nội dung đầu trang.
Data URI đó là placeholder mờ. Với ảnh import tĩnh, Next.js tự sinh nó lúc build: một SVG nhỏ chứa một JPEG vài trăm byte phóng to và làm mờ. Không cần tự tạo blurDataURL; chỉ cần ảnh là import tĩnh, hoặc truyền blurDataURL khi ảnh là đường dẫn chuỗi.
Đừng đặt placeholder="blur" cho ảnh lớn đầu trang: ảnh đã tải ngay thì tấm mờ chỉ làm chậm thêm một khung hình.
Hai con số cache khác nhau, và vì sao
Đo được hai header khác hẳn nhau cho cùng một cơ chế. Ảnh import tĩnh (tên tệp có hash nội dung) trả cache-control: public, max-age=315360000, immutable — mười năm, vì nội dung đổi thì tên đổi. Ảnh trong public/ trả cache-control: public, max-age=14400, must-revalidate — bốn giờ, theo images.minimumCacheTTL mặc định, vì cùng một URL có thể là ảnh khác vào ngày mai.
Request thứ hai cho cùng URL trả x-nextjs-cache: HIT: Next.js đã lưu ảnh đã nén trên đĩa và trả lại, không nén lại.
Nếu có CDN trước Next.js, nó phải chuyển tiếp header Accept, nếu không mọi người dùng nhận cùng một định dạng — thường là WebP, mất phần lợi của AVIF. Tài liệu Next.js nói rõ điều này; đây là lỗi cấu hình hay gặp khi tự dựng VPS.
quality không có trong danh sách: component kéo về, URL thì 400
Khai qualities: [50, 75, 90] trong next.config là cách giới hạn những giá trị được phép nén. Đo được hai thái độ khác nhau với cùng một giá trị 65: <Image quality={65}> ra URL q=75 (component kéo về mức gần nhất trước khi dựng URL, và bản dev in cảnh báo), còn gọi thẳng /_next/image?…&q=65 trả 400 với câu "q" parameter (quality) of 65 is not allowed.
Nghĩa là: đừng viết tay URL tối ưu ảnh, và cũng đừng tin rằng quality của bạn được giữ nguyên. Danh sách qualities càng ngắn thì CDN càng ít biến thể phải giữ.
An toàn: trình tối ưu ảnh không phải proxy mở
Server tối ưu ảnh tải ảnh hộ người dùng, nên nó là một chỗ SSRF nếu mở. Đo được: một URL ngoài không khai trong images.remotePatterns trả 400 với "url" parameter is not allowed; một tệp .svg trả 400 kèm dòng log dangerouslyAllowSVG is disabled.
Đừng bật dangerouslyAllowSVG chỉ vì tiện. SVG là tài liệu có thể chứa script và tham chiếu ngoài; nếu buộc phải phục vụ SVG do người khác tải lên, hãy phục vụ nó như tệp tĩnh từ một host khác, kèm Content-Security-Policy và Content-Disposition.
Khai remotePatterns càng hẹp càng tốt: đúng host, đúng giao thức, đúng đường dẫn. Trình tối ưu cũng không chuyển tiếp header xác thực khi tải ảnh (tài liệu nói rõ), nên ảnh cần đăng nhập thì dùng unoptimized.
Danh sách kiểm tra cho một trang có ảnh
1. Mỗi ảnh có width/height (hoặc fill trong khung có kích thước) để không nhảy bố cục. 2. sizes khớp bề rộng thật trên trang ở từng breakpoint. 3. Ảnh cố định của giao diện thì import tĩnh, để được hash tên và cache mười năm. 4. preload cho đúng một ảnh LCP, và cân nhắc fetchPriority="high". 5. formats: ['image/avif', 'image/webp'], rồi kiểm tra CDN có chuyển tiếp Accept. 6. qualities ngắn, và không dùng quality ngoài danh sách. 7. remotePatterns hẹp, không bật dangerouslyAllowSVG. 8. Đo lại bằng byte thật: mở DevTools, hoặc curl -H 'Accept: image/avif' như script của trang này.
Đừng đo bằng cảm giác. Trong lần chạy này, cùng một ảnh cho ra 11 KB và 48 KB chỉ vì khác bề rộng, và 5 KB khi hạ quality — ba biến số, ba kết quả, tất cả đều đúng.
Đã kiểm tra
- the page prints a raw <img> and a next/image:
raw + optimized - a plain <img>: the original file, unchanged:
402 KB image/jpeg - next/image at 640w, browser accepts AVIF:
image/avif - next/image at 640w, browser accepts WebP only:
image/webp - AVIF and WebP at 640w, from 402 KB:
11 KB AVIF vs 14 KB WebP — AVIF smaller at the same quality number - quality numbers are not comparable between codecs: AVIF q=50 against AVIF q=75 and WebP q=75:
5 KB AVIF q50, 11 KB AVIF q75, 14 KB WebP q75 - w= in the URL controls the encoded width:
5 KB < 23 KB < 402 KB - cache-control for a static import (the file is hashed):
public, max-age=315360000, immutable - cache-control for a file in public/ (same URL, different bytes tomorrow):
200 public, max-age=14400, must-revalidate - the second request for the same URL:
HIT: public, max-age=14400, must-revalidate - q=65 in the URL when images.qualities is [50, 75, 90]:
400: "q" parameter (quality) of 65 is not allowed - quality={65} on a <Image> (the component clamps before it builds the URL):
q=75 - a remote host that images.remotePatterns does not allow:
400: "url" parameter is not allowed - an SVG through the optimizer, dangerouslyAllowSVG unset:
400: "url" parameter is valid but image type - preload (the 16 name for priority): a link in the head for the hero image:
present, names the photo - the hero img:
8 widths in srcSet, sizes, no loading (eager) - the src the hero img carries, for a browser without srcset:
w=3840 - priority/preload no longer sets fetchpriority on the img (measured, 16.4.4):
absent — pass fetchPriority="high" yourself - the image below the fold:
loading=lazy, dimensions, blur data URI - the hero image at 640 CSS px, 1x screen:
11 KB at 640w, 97% smaller than the file - the same slot on a 2x screen, where the browser takes 1920w:
48 KB at 1920w, 88% smaller - what served these images:
sharp 0.35.5; next 16.4.4
Chạy lại kiểm tra: bun scripts/guides/verify-next-image.ts
Liên quan
- Cài đặt Next.js 16 và tạo dự án đầu tiên
- Nâng cấp lên Next.js 16: những gì thật sự làm build dừng
- Lỗ hổng bảo mật Next.js
- Kiểm tra phiên bản Next.js của trang bạn
Nguồn
- Next.js docs — next/image (preload, sizes, quality, placeholder)
- Next.js docs — images config (formats, qualities, minimumCacheTTL, remotePatterns)
- sharp — the encoder Next.js uses for optimized images
- MDN — HTMLImageElement.fetchPriority
- npm registry — next dist-tags
Tóm tắt và diễn giải bằng lời của chúng tôi; mã viết lại từ ví dụ trong tài liệu Next.js. Khi có khác biệt, tài liệu Next.js là chuẩn.