Nội dung
- Một buổi chiều tối ưu để đổi lấy 17% dung lượng
- Phép tính điểm ảnh thật: 329px nhân 2,75 DPR
- Bẫy nấc thang srcset: không có ga ở giữa 768 và 1024
- Header Vary: Accept — thứ làm mọi thứ trông như đã chạy
- Năm file WebP trên 11 gigabyte ảnh thực tế
- Ba ngày sau: 88.598 tệp từng có thật, và biến mất lúc 15 giờ 18
- Bấm nút: 96.322 tệp trong ba tiếng mười lăm phút
- Ba bài học sau khi bấm nút convert hàng loạt
Hôm 01/08, tôi dành trọn một buổi chiều để sửa thuộc tính sizes trên toàn bộ ảnh bài viết của marketing365.vn. Mục tiêu rất rõ ràng: khung hiển thị nội dung trên màn hình điện thoại chỉ rộng 329 pixel, nhưng trình duyệt lại tải về bản ảnh 1170 pixel nặng hơn 106 kilobyte. Tôi muốn ép nó tải bản nhỏ hơn để giảm tải đường truyền.
Mười bốn ngày sau, khi dùng trình duyệt không đầu đo lại request thực tế đến từng byte, tôi nhận ra hai điều: một là phép tối ưu đó chỉ đem lại 17% mức giảm dung lượng vì vướng trần mật độ điểm ảnh; hai là ngay dưới chân mình, một đòn bẩy lớn hơn gấp ba lần đang được bật sẵn nhưng nằm chết cứng suốt hai tháng mà không ai hay biết.
Một buổi chiều tối ưu để đổi lấy 17% dung lượng
Vấn đề ban đầu trông rất thuyết phục: cột nội dung bài viết trên bản mobile có bề rộng bố cục đo được đúng 329 pixel. Thế nhưng mã nguồn HTML của theme lúc đó lại xuất thuộc tính sizes với thông số chung chung, khiến trình duyệt chọn nấc 1170w (nặng 106.747 byte). Nhìn vào đó, ai cũng sẽ nghĩ rằng tải một tấm ảnh 1170px cho cái khung 329px là sự lãng phí khủng khiếp.
Tôi đã ngồi viết lại bộ lọc wp_calculate_image_sizes trong theme child, phân định rõ ràng giữa ảnh đại diện bài viết (tính theo tỷ lệ viewport thực) và ảnh trong thân bài (dùng sizes="auto" kết hợp nạp lười). Tôi xóa sạch bộ nhớ đệm trang, chạy lại hàng đợi và kiểm tra: trình duyệt di động đã chịu đổi nấc tải từ 1170w xuống 1024w.
Dung lượng file giảm từ 106.747 byte xuống 88.213 byte — tức là tiết kiệm được khoảng 18,5 KB, tương đương 17,4%. Đó là một con số có thật. Nhưng để đổi lấy 17% đó, tôi đã mất hàng giờ chỉnh sửa 6 điểm ngắt CSS và xóa hàng nghìn trang cache.
Phép tính điểm ảnh thật: 329px nhân 2,75 DPR
Lý do bản vá sizes không thể kéo dung lượng xuống sâu hơn nằm ở khái niệm mật độ điểm ảnh phần cứng (Device Pixel Ratio – DPR). Màn hình điện thoại ngày nay không hiển thị 1 CSS pixel bằng 1 bóng đèn vật lý.
Khi tôi chạy Chrome Headless giả lập một chiếc Pixel 5 (màn hình 393px, DPR 2,75), phép tính mà trình duyệt thực hiện trong tích tắc là:
329px (khung bố cục) × 2,75 (DPR) = 904,75 điểm ảnh vật lý
Trình duyệt di động thông minh hơn chúng ta tưởng: nó biết rằng nếu tải bản ảnh 329px hay 768px về, ảnh sẽ bị vỡ nét trên màn hình retina. Để bức ảnh sắc nét, nó buộc phải tìm một bản ảnh có bề rộng tối thiểu 905 pixel.
Bẫy nấc thang srcset: không có ga ở giữa 768 và 1024
Thang kích thước ảnh mặc định của WordPress và theme nhảy cóc: 150w → 300w → 768w → 1024w → 1170w → 1200w. Khi trình duyệt đòi hỏi một file ảnh rộng ~905px, nó nhìn vào danh sách srcset và thấy 768w thì quá nhỏ (sẽ mờ), nên nó bắt buộc phải nhảy vọt lên nấc 1024w.
Dù tôi có khai báo thuộc tính sizes chính xác đến từng pixel lẻ, chừng nào hệ thống chưa sinh ra một bản crop riêng ở nấc ~900w, trình duyệt vẫn sẽ tải bản 1024w. Bản 1024w nặng 88 KB, và đó là giới hạn sàn của định dạng JPEG.
Chưa kể, một chi tiết bi hài mà tôi phát hiện khi đối chiếu bảng kích thước file: bản 1170w (106.747 B) thực tế còn nặng hơn cả bản gốc 1200w (105.849 B) do thuật toán re-encode của thư viện xử lý ảnh. Bước chặn kích thước tối đa 1170w hồi tháng 7 hóa ra đã khóa đúng vào file nặng nhất trong toàn bộ thang đo.

Header Vary: Accept — thứ làm mọi thứ trông như đã chạy
Trong khi đòn bẩy kích thước vật lý chạm trần 17%, thì đòn bẩy định dạng nén hiện đại (WebP) lại bị bỏ quên trong bóng tối. Plugin Converter for Media đã được cài đặt và kích hoạt từ giữa tháng 6. Khi tôi dùng curl gửi request kiểm tra một URL ảnh bất kỳ:
curl -sI -H "Accept: image/webp,*/*" https://marketing365.vn/wp-content/uploads/2026/08/sample-1024x571.jpg | grep -i vary
vary: Accept
Header vary: Accept xuất hiện đường hoàng. Mọi công cụ kiểm tra tự động bên ngoài nhìn vào header này đều sẽ đánh dấu tích xanh: “Trang web đã hỗ trợ nén WebP động”. Nhưng đó là một cái bẫy nhận thức tai hại.
Luật chuyển hướng trong tệp .htaccess có điều kiện kiểm tra tồn tại tệp: RewriteCond ... $1.jpg.webp -f. Nếu máy chủ không tìm thấy file .webp tương ứng trên đĩa cứng, luật rewrite sẽ tự động bỏ qua và máy chủ Web trả về file .jpg gốc mà không báo một lỗi nào. Header Vary: Accept chỉ có nghĩa là “phản hồi này có thể thay đổi tùy theo trình duyệt”, chứ không đảm bảo rằng “bạn đã nhận được file WebP”.
Năm file WebP trên 11 gigabyte ảnh thực tế
Ngày 15/08/2026, tôi vào tận ổ đĩa máy chủ để đếm số lượng file thực tế. Kết quả kiểm tra đối chiếu khiến tôi giật mình:
- Thư mục ảnh gốc
wp-content/uploads: nặng 11 Gigabyte với hơn 87.700 tệp ảnh JPG và PNG. - Thư mục ảnh nén
wp-content/uploads-webpc: chỉ nặng vỏn vẹn 148 Kilobyte, chứa đúng 5 file .webp cùng sinh ra vào ngày 18/06/2026 từ một tấm ảnh thử nghiệm ban đầu. - Tùy chọn trong cơ sở dữ liệu:
webpc_is_new_installation = 1, tức là plugin tự nhận mình vừa được cài đặt xong. Tôi đọc dòng này thành “chưa ai từng bấm nút nén” — và đó là chỗ tôi hiểu sai hoàn toàn, phải ba ngày sau mới vỡ lẽ.
Một trang bài viết trung bình hiện đang tải tới 538.054 byte ảnh JPEG cho một cột đọc nhỏ trên điện thoại. Khi tôi lấy 4 tấm ảnh đang phục vụ ra nén thử nghiệm bằng ffmpeg libwebp (q82), dung lượng lập tức giảm từ 34% đến 49,6% ở cùng kích thước hiển thị mà mắt thường không thể phân biệt được độ suy giảm chất lượng.

Ba ngày sau: 88.598 tệp từng có thật, và biến mất lúc 15 giờ 18
Ngày 18/08, tôi mở lại nhật ký vận hành của chính mình để chuẩn bị viết bài này thì gặp một dòng ghi ngày 08/08: lô nén đã chạy xong, 88.598 tệp WebP, thư mục nặng 4,81 GB, kiểm tra phục vụ ngoài thực tế đạt. Hai bản ghi chọi thẳng vào nhau: 08/08 có 88.598 tệp, 15/08 còn 5 tệp. Một trong hai phải sai, và tôi đã tin rằng bản ghi cũ mới là bản ghi bịa.
Thứ phân xử không phải trí nhớ mà là dấu thời gian sửa đổi của thư mục trên đĩa. Xoá tệp bên trong một thư mục thì thư mục đó đổi mtime:
wp-content/cache 2026-08-11 08:18 UTC
wp-content/uploads-webpc 2026-08-11 08:19 UTC (= 15:19 giờ Việt Nam)
└── uploads/ 2026-08-11 08:20 UTC
Lô nén không phải chưa từng chạy. Nó đã chạy, sống được ba ngày, rồi bị xoá sạch trong khoảng 15:18 đến 15:20 ngày 11/08. Ba chi tiết đi kèm khoanh vùng thủ phạm: thư mục tái tạo sau đó thuộc quyền root:root chứ không phải www-data, nghĩa là tiến trình xoá chạy bằng quyền quản trị máy chủ chứ không phải qua trình duyệt; cùng thời điểm đó tuỳ chọn webpc_settings bị gỡ khỏi cơ sở dữ liệu; và toàn bộ cấu hình nén trở về mặc định. Nhật ký truy cập của máy chủ ở khung giờ đó đã xoay vòng mất, bản sao lưu tự động thì chỉ giữ từ 12/08 — tức là chụp lại hiện trường sau khi mọi thứ biến mất. Tôi không truy được đích danh thao tác nào châm ngòi, và tôi ghi lại đúng như vậy thay vì chọn một thủ phạm nghe cho xuôi tai.
Bấm nút: 96.322 tệp trong ba tiếng mười lăm phút
Chiều 18/08 tôi dựng lại lớp nén từ đầu. Trình tự bốn bước, mỗi bước có lý do riêng: tắt plugin nén thứ hai được kích hoạt xen vào giữa hai mốc trên (hai plugin cùng ghi đè một lớp phục vụ ảnh là công thức gây hỏng); ghi lại 21 khoá cấu hình vào cơ sở dữ liệu để giao diện quản trị và cơ chế nén ảnh mới tải lên hoạt động trở lại; trả quyền sở hữu thư mục về www-data; rồi mới chạy lô.
Bước trả quyền sở hữu là bước dễ bỏ sót nhất và nó chặn đứng mọi thứ nếu thiếu. Thư mục do tiến trình quản trị tái tạo nên thuộc root, còn tiến trình nén chạy dưới quyền máy chủ web, kết quả là mọi tệp đều báo lỗi “không tạo được thư mục đích” mà không nói rõ vì sao:
chown -R www-data:www-data wp-content/uploads-webpc
Lô chính chạy từ 16:00 đến 19:15 giờ Việt Nam, xử lý theo mẻ 40 tệp, nhường CPU cho tiến trình web: 96.322/96.322 tệp, không một lỗi. Lượt quét vét thứ hai bắt thêm 146 tệp sót. Kết quả cuối cùng trên đĩa là 96.190 tệp WebP, tổng 5,5 GB. Con số 143 tệp không sinh ra bản WebP nhìn qua tưởng lỗi, thực chất là đúng thiết kế: tuỳ chọn chỉ giữ bản nhỏ hơn bỏ qua những ảnh mà bản WebP còn nặng hơn bản gốc — lô 08/08 cũng để lại 133 tệp như vậy.
Nghiệm thu không đọc số do plugin tự báo, vì con số đó vô nghĩa: bộ đếm nội bộ cộng dồn qua mọi lần chạy và in ra hơn 115 triệu. Thứ đáng tin là đếm tệp trên đĩa và hỏi thẳng máy chủ bằng đúng header mà trình duyệt gửi:
curl -sI -H "Accept: image/webp,*/*" <url ảnh> | grep -i content-type
content-type: image/webp
Bốn mẫu lấy ở bốn tháng khác nhau (03, 06, 07 và 08 năm 2026) đều trả về image/webp, nhẹ hơn bản JPEG gốc từ 33% đến 73%. Không cần xoá bộ nhớ đệm trang: lớp nén làm việc ở tầng máy chủ và địa chỉ ảnh giữ nguyên, nên trang đã lưu đệm vẫn tự nhận bản nhẹ.
Ba bài học sau khi bấm nút convert hàng loạt
Sự cố này dạy cho tôi ba bài học đắt giá về quy trình tối ưu hóa hiệu năng website:
- Đừng tin vào header trả về nếu chưa nhìn thấy file vật lý: Một phản hồi HTTP 200 kèm header
Vary: AccepthayContent-Typecó thể che giấu một cơ chế fallback âm thầm bên trong web server. Luôn kiểm tra kích thước gói tin (Content-Length) hoặc đếm file trên đĩa cứng. - Chọn đúng đòn bẩy lớn trước khi mài giũa chi tiết nhỏ: Cố gắng vắt kiệt từng pixel của thuộc tính
sizeschỉ mang lại 17% cải thiện, trong khi một lệnh chuyển đổi định dạng ảnh hiện đại mang lại 33–73% mức giảm tải trên toàn bộ kho ảnh. - Trạng thái quá khứ không phải bằng chứng cho hiện tại: Bản ghi “đã nén xong 88.598 tệp” hoàn toàn đúng vào ngày viết ra nó, và hoàn toàn vô giá trị ba ngày sau. Một lớp tối ưu có thể biến mất mà không phát ra tiếng động nào — không cảnh báo, không lỗi, không thay đổi mã trả về. Cách duy nhất trả lời câu hỏi “site có phục vụ WebP không” là hỏi máy chủ ngay lúc đó bằng đúng header trình duyệt gửi, chứ không phải mở lại nhật ký cũ.
Cũng tương tự như việc 11 ngày sửa nhầm chỗ vì tin vào nhãn LCP sai hay câu chuyện cắt 80% file CSS mà LCP không nhúc nhích: thứ chúng ta tưởng rằng đang hoạt động hoàn hảo trên lý thuyết thường lại là thứ đang đứng im lặng trong thực tế.
Xử lý gần một trăm nghìn bức ảnh trên một máy chủ có người truy cập là tác vụ ngốn CPU và ổ đĩa, nên lô nén chạy chia mẻ nhỏ, nhường tài nguyên cho trình duyệt của độc giả và có người ngồi canh từ đầu tới cuối. Ba tiếng mười lăm phút cho 96.322 tệp, không lỗi nào. Điều tôi mang theo sau vụ này không phải con số 5,5 GB, mà là thói quen mới: mỗi tuần một lần, gửi đúng một dòng curl kèm header Accept tới một tấm ảnh bất kỳ và nhìn dòng content-type trả về. Mất hai giây, và nó phát hiện được thứ mà ba ngày qua không hệ thống cảnh báo nào của tôi nhìn thấy.



