WebP: định dạng nén cho trang web
(purplesyringa.moe)- Do GitHub Pages không hỗ trợ Brotli, đây là một thử nghiệm mã hóa HTML thành ảnh WebP không mất dữ liệu rồi khôi phục bằng bộ giải mã ảnh của trình duyệt và JavaScript để giảm lượng dữ liệu truyền tải
- Bộ giải mã Brotli bằng WASM tạo thêm chi phí
71 KiB~200 KB, còn Compression Streams API chỉ hỗ trợgzip,deflate,deflate-raw, nên khó trở thành đường vòng để giải mã Brotli - WebP không mất dữ liệu VP8L có thể cho kết quả nhỏ hơn gzip ngay cả với dữ liệu dạng văn bản nhờ biến đổi dự đoán, tái sử dụng cây Huffman theo từng khối 16x16 và color cache
- Tệp HTML thử nghiệm
439,478byte đã được giảm xuống còn gzip94,683byte, WebP43,182byte; vẫn lớn hơn Brotli37 KiBnhưng nhỏ hơn khoảng 2.2 lần so với gzip - Do nhiễu anti-fingerprinting của Canvas 2D, thay đổi với
readPixelscủa WebGL, màn hình trắng lúc đầu và vấn đề khôi phục cuộn trang, cách này gần giống một mẹo hack phơi bày giới hạn của trình duyệt hơn là một giải pháp thực tế
Vấn đề không thể dùng Brotli trên GitHub Pages
- Khi muốn giảm thời gian tải trang, nén HTTP đem lại hiệu quả lớn hơn việc minify HTML
- HTTP hỗ trợ gzip và Brotli qua header
Content-Encoding- gzip rẻ nên thường được bật mặc định
- Brotli thường có tỷ lệ nén tốt hơn gzip nhưng chậm hơn nhiều
- GitHub Pages không hỗ trợ Brotli, nên bài dài nhất của trang là
Recovering garbled Bitcoin addressesnếu dùng Brotli có thể chỉ còn37 KiB, nhưng với gzip lại thành92 KiB - Chênh lệch này khiến thời gian tải tăng 2.5 lần một cách không cần thiết
Những phương án thay thế đầu tiên và các điểm bế tắc
- Nếu GitHub hỗ trợ tải lên và phục vụ các tệp Brotli đã nén sẵn thì có thể tránh được vấn đề, nhưng không có tính năng đó
- Phương án giải nén trực tiếp bằng JavaScript ở phía client bị giảm lợi ích vì kích thước bộ giải mã WASM
- brotli-dec-wasm khoảng
200 KB - tiny-brotli-dec-wasm là
71 KiB - Khi đó phép so sánh trở thành gzip
92 KiBvới phần thân Brotli37 KiB + 71 KiB, nên lợi thế biến mất
- brotli-dec-wasm khoảng
- Stack HTTP của trình duyệt có bộ giải mã Brotli, nhưng DecompressionStream của Compression Streams API chỉ nhận
gzip,deflate,deflate-raw - Ngay cả khi nén sẵn
gzipbằng Zopfli thì vẫn là86 KiB, nên vẫn lớn hơn Brotli
Dùng định dạng ảnh như một container nén
- Trình duyệt vốn đã có thể giải mã ảnh, nên nếu nhét dữ liệu vào pixel ảnh rồi đọc lại bằng Canvas API thì không cần tự thêm logic giải nén mới
- GIF trải dữ liệu theo thứ tự row-major rồi áp dụng LZW, nhưng DEFLATE của gzip được thiết kế để thay thế LZW nên khó kỳ vọng có lợi thế
- PNG dùng DEFLATE, nhưng trước đó còn áp dụng biến đổi dự đoán để nén chênh lệch với các pixel lân cận thay vì nén trực tiếp pixel gốc
- Ví dụ: nén
[a, b, c, d]thay vì[a, b-a, c-b, d-c] - Chênh lệch giữa giá trị dự đoán và giá trị thực càng nhỏ thì càng có lợi cho nén Huffman
- Ví dụ: nén
- Trọng tâm của thử nghiệm là tận dụng VP8L của WebP không mất dữ liệu để nén dữ liệu byte thông thường
VP8L khác gzip ở điểm nào
- WebP có cả biến thể mất dữ liệu và không mất dữ liệu; ở đây chỉ nói về định dạng không mất dữ liệu VP8L
- VP8L dùng biến đổi dự đoán giống PNG, nhưng thay DEFLATE bằng một cơ chế tương tự DEFLATE do Google tạo ra
- DEFLATE có thể chia tệp thành nhiều đoạn và dùng cây Huffman phù hợp cho từng đoạn
- Nếu một HTML trộn JavaScript, SVG và markup thì các cây khác nhau có thể sẽ có lợi hơn
- VP8L cho phép định nghĩa một bảng cây Huffman lớn tùy ý và dùng cây khác nhau cho từng khối pixel 16x16
- Khi có JavaScript rồi tới CSS rồi lại JavaScript, DEFLATE có thể phải mã hóa các cây tương tự nhiều lần
- VP8L có thể tái sử dụng cây, nên đổi cây thường xuyên hơn với chi phí thấp hơn
- Color cache của VP8L cho phép biểu diễn ngắn các giá trị kiểu như sao chép pixel gần đây có một số thuộc tính nhất định
Thử nghiệm nén WebP đầu tiên
- Tệp thử nghiệm là HTML của
Recovering garbled Bitcoin addresses- Kích thước gốc:
439,478byte gzip --best:94,683byte
- Kích thước gốc:
- Dùng crate webp của Rust để biến các byte thành ảnh RGB grayscale rồi nén thành WebP không mất dữ liệu
- Lý do dùng grayscale là vì biến đổi
subtract greencủa WebP- Với grayscale, nếu trừ kênh G khỏi R/B thì R/B gần như trở thành 0
- WebP mã hóa ba kênh bằng các cây Huffman riêng, nên các kênh có giá trị cố định sẽ gần như chỉ tốn không gian
O(1)
- Ban đầu định tạo ảnh
1xN, nhưng WebP chỉ hỗ trợ tối đa16383x16383, nên gặp lỗiVP8_ENC_ERROR_BAD_DIMENSION - Sau khi đổi sang dạng
16383xN, kết quả là45,604byte, nhỏ hơn gzip 2 lần và còn nhỏ hơn cả bzip249,764byte
Tinh chỉnh chuyên biệt cho WebP
- Nếu dùng thứ tự row-major với ảnh rộng, trong cùng một khối 16x16 sẽ trộn lẫn các byte ở rất xa nhau trong đầu vào
- Khi đổi hình dạng ảnh thành kiểu dọc
27x16383, kết quả nén giảm xuống43,232byte - Đã so sánh thiết lập hiệu năng nén method của
cwebptừ0~6- method 0:
48,902 - method 1:
43,546 - method 2:
43,442 - method 3:
43,292 - method 4:
43,232 - method 5:
43,182 - method 6:
43,182
- method 0:
- method
5được chọn vì cho cùng kích thước với method6nhưng nhanh hơn - Ở trạng thái này, WebP nhỏ hơn 2.2 lần so với gzip và lớn hơn 1.2 lần so với Brotli
Benchmark với nhiều tệp
- Đối tượng so sánh là snappy testdata, Canterbury Corpus và Large Corpus, cùng 2 tệp SVG
- Các định dạng đem so sánh là
gzip --best,brotli --best,bzip2 --bestvà script nén WebP - WebP gần như luôn tốt hơn gzip, trừ vài ngoại lệ như các tệp rất nhỏ
grammar.lsp,xargs.1và một số trường hợp khác - Các ngoại lệ là
kennedy.xlsvàpaper-100k.pdfpaper-100k.pdfcó19 KBXML trước dữ liệu nén, nên thực tế gần như đang đo trên một lượng dữ liệu nhỏkennedy.xlscũng cho tương quan hiệu năng Brotli/bzip2 bất thường, và có thể là tệp chứa nhiều dữ liệu khác loại nằm gần nhau khiến bộ nén khó xử lý
- WebP thường hơi kém hơn bzip2, nhưng trong một số trường hợp lại vượt lên
- WebP luôn kém hơn Brotli, trừ những ngoại lệ như
fireworks.jpegvốn gần như là một blob ngẫu nhiên khá đồng đều - Với dữ liệu plain-text lớn, nó vẫn cho mức cải thiện đo được so với gzip
- Cả tệp SVG cũng có cải thiện
- Với
html_x_4, WebP đạt tỷ lệ nén3.3%, kém hơn Brotli2.8%nhưng tốt hơn rất nhiều so với gzip13%
Khôi phục bằng JavaScript
- Việc giải mã WebP có thể triển khai bằng
fetch,createImageBitmap,OffscreenCanvas,getImageData,TextDecoder - Cấu trúc là lấy kênh R của pixel làm byte HTML gốc, giải mã UTF-8 rồi gán vào
document.documentElement.innerHTML - Canvas API thường bị dùng cho fingerprinting nên một số trình duyệt thêm nhiễu vào kết quả
getImageData- Trong strict tracking protection của Firefox, dưới 1% pixel có thể bị ảnh hưởng
- Với HTML, nhiễu đó sẽ hiện ra như lỗi gõ chữ
- Dùng
readPixelscủa WebGL thì vào thời điểm đó vẫn hoạt động mà không có nhiễu- WebGL chỉ hỗ trợ ổn định texture tối đa khoảng
2048x2048, nên lại phải điều chỉnh giới hạn kích thước - Mã khôi phục này sau khi minify có kích thước khoảng
550byte
- WebGL chỉ hỗ trợ ổn định texture tối đa khoảng
- Tính cả WebP và đoạn mã thì tổng là
44 KiB, để so với gzip92 KiBvà Brotli37 KiB - Sau ngày 15 tháng 4 năm 2026, Firefox đã thêm anti-fingerprinting cả cho
readPixels, nên cách này không còn hoạt động nguyên trạng nữa
Hiện tượng nhấp nháy màn hình và vấn đề cuộn trang
awaitxử lý theo promise, nên trước khi tải xong WebP, trình duyệt sẽ cho rằng việc thực thi script đã kết thúc- Vì DOM vẫn còn trống, người dùng sẽ thấy một màn hình trắng trống trong thời gian ngắn
- Để giảm bớt, có thể giữ lại CSS và khoảng
8 KiBphần đầu trang trong HTML gzip, rồi chỉ nén nội dung bên dưới viewport bằng WebP - Việc khôi phục vị trí cuộn khi refresh cũng gặp vấn đề
- Ví dụ đang refresh ở vị trí
Y = 5000pxnhưng chiều cao trang là0pxthì vị trí sẽ bị reset - Thêm một
divtạm rất lớn có thể giúp ích
- Ví dụ đang refresh ở vị trí
- Cần gán vào
document.documentElement.innerHTMLthay vìdocument.writeđể cập nhật tài liệu hiện tại mà không thay thế nó bằng một tài liệu mới
Nhúng WebP trực tiếp vào JavaScript
- Để giảm thêm một chút độ trễ, có thể nhúng trực tiếp WebP vào trong JavaScript
- Cách đơn giản nhất là base64 data URL
- Base64 làm kích thước gốc tăng
1.33 lần, nhưng gzip gần như bù lại phần tăng đó- Nếu biến
compressed.webpthành base64 thì được57,576byte - Nếu tiếp tục nén bằng gzip
--bestthì còn43,519byte
- Nếu biến
- Một blob nén như WebP gần như là dữ liệu ngẫu nhiên khá đồng đều, và phép biến đổi 8-bit sang 6-bit của base64 khiến cây Huffman của gzip gần như hoạt động như một phép đảo ngược
- Cũng có thể dùng Unicode và UTF-16, nhưng base64 vẫn là lời giải đầu tiên đủ tốt
Ứng dụng thực tế và tình trạng sau đó
- Vào thời điểm bài viết được viết, chính trang này, nếu không phải trên trình duyệt cũ hoặc môi trường tắt JavaScript, thì đã được nén bằng WebP начиная từ phần “Fool me twice”
- Ảnh WebP của trang trong mã thực tế có dạng cao và hẹp, nhưng bài cũng cung cấp ví dụ WebP hình vuông để dễ nhìn hơn
- Trong ảnh, phần sáng ở trên và dưới là văn bản cùng mã, vùng gạch chéo khoảng ở mốc 1/5 là sơ đồ, còn phần lớn vùng tối là văn bản bên trong sơ đồ
- Mức tiết kiệm thực tế là khá hạn chế
- Trang gzip ban đầu:
88 KiB - Trang gzip sau khi áp dụng WebP:
83 KiB - Ước tính Brotli:
69 KiB
- Trang gzip ban đầu:
- Sau ngày 15 tháng 4 năm 2026, để khách truy cập Firefox không thấy nội dung bị hỏng, hiện trang đã quay lại bản không nén kiểu này
- Mã Rust, corpus và các tệp khác được công khai trên GitHub
1 bình luận
Ý kiến trên Hacker News
Nếu bỏ qua độ trễ thì có lẽ đúng, nhưng trong thực tế có vẻ thời gian tải chỉ tăng khoảng 0,001%
Phần dung lượng tăng thêm không đáng kể so với độ trễ khứ hồi, và thời gian tiết kiệm được nhờ truyền ít hơn 55KiB thậm chí có thể nhỏ hơn thời gian dùng để giải nén
Đây là một thử nghiệm thú vị, nhưng trong trường hợp này trải nghiệm người dùng có khả năng còn tệ hơn, và có lẽ chỉ giảm tính tương thích trong khi tốc độ thì gần như giữ nguyên
Vấn đề là, trong khi có những người như tác giả TFA cố gắng giảm 100KB xuống 50KB, thì cũng có những nơi thản nhiên gửi cho tôi hàng chục MB hình ảnh chỉ để xem giờ mở cửa của một nhà hàng khi tôi đang dùng dữ liệu roaming
Ý thức về tài nguyên là có tồn tại, nhưng đáng tiếc là phân bố cực kỳ không đồng đều
Nếu kết nối bị ngắt thì mất sạch, và cũng không thể đọc được dù chỉ phần đã tải xuống
Trên kết nối thông thường thì khác biệt không đáng kể, còn trên kết nối rất chậm hoặc không ổn định đến mức 50KB là quan trọng thì cách này chắc chắn còn tệ hơn. Là một thử nghiệm vui, nhưng mong là đừng áp dụng lên website
Symbols-2048-em%20Nerd%20Font%20Complete.woff2dung lượng 850K, gần như xóa nhòa khác biệt đóCó thể không phải chênh 2,5 lần, nhưng cũng không phải 0,001%
Trên mạng có mất gói thì có thể sẽ khác, nhưng tôi không dám chắc
Tôi không hiểu vì sao
readPixelslại không thuộc diện chống fingerprinting. Vì nó không rải các lỗi gõ gần như không nhìn thấy lên toàn bộ trang nên với tôi thế là ổnĐoạn nói rằng HTML nén bằng gzip chỉ chứa style và khoảng 8KiB đầu trang, còn nội dung bên dưới viewport thì được nén bằng WebP đã giải thích vì sao bài viết lại đột ngột bị cắt giữa chừng sau một câu bất kỳ và sau đó là một trang trống
Tôi dùng LibreWolf nên WebGL bị tắt, còn các game web ngẫu nhiên cần WebGL thì tôi dùng Chromium. Khi bật WebGL thì bài viết hoạt động tốt, và nói thật là đây là một kỹ thuật khá gọn gàng
WWW nên có khả năng truy cập phổ quát, bắt đầu từ HTML thông thường rồi nâng cấp dần dần
Cũng có thể dùng trực tiếp Brotli trong trình duyệt web, nhưng hiển nhiên là có giới hạn
Tôi nghĩ bài dự thi JS1024 năm 2022 [1] là màn trình diễn đầu tiên của ý tưởng này, và cũng có mã proof-of-concept cho nén tùy ý. Đáng tiếc là nó không phù hợp với mục tiêu ban đầu là mã hóa độ dài
Giới hạn cốt lõi là trên thực tế nó gần như bị giới hạn ở ký tự ASCII, và vì những lý do khá hiển nhiên, nó cực kỳ nhạy với rendering stack. Hiện tại có vẻ nó không còn hoạt động trên Firefox
[1] https://js1024.fun/demos/2022/18/readme
[2] https://gist.github.com/lifthrasiir/1c7f9c5a421ad39c1af19a9c...
Chỉ có thể dùng định dạng tệp phông chữ WOFF2 mà Brotli vốn được thiết kế cho, nhưng để tận dụng điều đó thì phải tạo ra cả một tệp font hoàn chỉnh
Các trình duyệt gần đây thường dọn dẹp font bằng OpenType Sanitizer (OTS), vì đưa trực tiếp các tệp font không đáng tin vào hệ thống là cực kỳ nguy hiểm, nên phải tạo được một tệp WOFF2 vừa đủ hợp lệ để OTS chấp nhận, vừa có thể chứa chuỗi byte mong muốn bên trong rồi trích xuất ra
Ý tưởng chốt lại ở độ rộng glyph, tức advance, được mã hóa thành dãy số nguyên có dấu 2 byte với hầu như không có ràng buộc sau rất nhiều lần thất bại, thật sự quá xuất sắc
Phía Chromium đã chặn khá lâu, nhưng zstd giờ cũng đang vào web. Cuối cùng nó đã vào Chrome, giờ chỉ còn chờ Safari theo kịp
Tôi đang làm Batch Compress(https://batchcompress.com/en) và không lâu sau khi thêm hỗ trợ WebP thì đã chuyển nó thành mặc định
Theo tôi biết thì công cụ này vốn đã tạo ra JPEG nhỏ nhất trong số các công cụ nén web, nhưng WebP lại chỉ có kích thước bằng khoảng 50% JPEG. Vì vậy, việc đổi nó thành mặc định ngay sau khi thêm hỗ trợ là một quyết định dễ dàng
Trang có khá nhiều người dùng, nên tôi cũng nghĩ sẽ có chút phàn nàn sau khi đổi mặc định sang WebP, nhưng đến nay sau khoảng một tháng thì chỉ có đúng một câu hỏi hay phàn nàn liên quan tới WebP
Giờ có vẻ gần như mọi công cụ và trình duyệt đều hỗ trợ WebP. Gần đây tôi chỉ thấy đúng một website không xử lý chuẩn việc tải ảnh WebP lên nên bị chặn ở bước tiếp theo, còn dạo này thì hầu như đâu cũng hỗ trợ tốt
Nếu JPEG được nén và tối ưu tốt thì không nên thua WebP quá nhiều
Lúc nào cũng có thể tạo ra WebP trông gần như y hệt JPEG để giảm kích thước tệp, nhưng nếu nén lại thành JPEG trông gần như y hệt thì cũng vậy thôi
Đây là đặc tính của mọi codec nén mất dữ liệu; khi chất lượng tăng lên thì kích thước tệp tăng theo hàm mũ, nên mọi người luôn ngạc nhiên khi chỉ một mức giảm chất lượng rất nhỏ, gần như không nhìn ra, lại có thể làm dung lượng thay đổi đáng kể
WebP từng nổi tiếng với các giá trị mặc định tệ hại làm hỏng chi tiết ở vùng tối
Khi xem mã nguồn, tôi thấy khai báo doctype bị thiếu khoảng trắng. Dạng hiện tại là sai và cần có khoảng trắng
Bỏ khoảng trắng trong
DOCTYPEgiúp ngắn hơn một chútNói chính xác thì đó không phải HTML hợp lệ, nhưng vẫn kích hoạt được chế độ chuẩn
Tham khảo: https://GitHub.com/kangax/html-minifier/pull/970 / https://HTML.spec.WHATWG.org/multipage/parsing.html#parse-er...
Tôi cũng dùng mẹo đó trên https://FreeSolitaire.win
Có vẻ đây là kết quả từ một minifier cố gắng lược bỏ tối đa [0]
0: https://github.com/KTibow/KTibow/issues/3#issuecomment-23367...
Tôi từng dùng mẹo này trước đây. Lạ là tôi không nhớ mình đã dùng cho việc gì, nhưng có lẽ chỉ để xem liệu nó có khả thi không, và tôi cũng từng để lại bình luận ở đây: https://gist.github.com/gasman/2560551?permalink_comment_id=...
Tôi còn tìm lại được một nguyên mẫu cũ, có lẽ đúng là chỉ để thử nghiệm: https://retr0.id/stuff/bee_movie.webp.html
Giờ thì mấy thứ kiểu chèn script như vậy được gọi là extension chứ không còn là addon nữa, nhưng dù sao thì cách tiếp cận ban đầu — truyền vào “rác” rồi nối JS ở phía sau để quay lại thành trang — vẫn khá thú vị
Phần con người mê bảo mật trong tôi tự hỏi liệu điều này có thể mở ra khả năng tấn công khi có dữ liệu do người dùng cung cấp, như biểu mẫu bình luận, hay không
Tôi tự hỏi liệu ai đó có thể tìm ra một chuỗi byte để nhét vào bình luận, rồi sau khi nén nó biến thành thẻ
scriptnằm trước script của tôi và được thực thi hay khôngPhần lớn lợi thế của WebP không mất dữ liệu so với PNG nằm ở mô hình hóa chứ không phải mã hóa, trong khi kiểu nén văn bản này chỉ tận dụng phần mã hóa của WebP
Nếu bỏ Google Fonts thì thời gian tải trang cũng sẽ cải thiện đôi chút, vì nó được tải từ máy chủ từ xa và cần thêm một lần bắt tay kết nối
Trang này bị lỗi, ít nhất là trên trình duyệt Sailfish OS. Có một khoảng trắng dài sau đoạn sau
“Alright, so we’re dealing with 92 KiB for gzip vs 37 + 71 KiB for Brotli. Umm…”
Dù vậy, phần overhead của nén HTML bằng gzip và Brotli chẳng đáng là bao so với lượng JS, hình ảnh và video mà website ngày nay sử dụng
Cá nhân tôi không thích định dạng này lắm. Khi lưu ảnh mà nó thành WebP, ngoài trình duyệt web ra thì gần như chẳng có chỗ nào hỗ trợ, nên trước khi chỉnh sửa hay dùng cho ra hồn tôi lại phải chuyển đổi nó
Cảm giác như bị ép thêm một bước không cần thiết
Nhưng nếu mức hỗ trợ tăng thêm thì chắc cũng ổn. Cứ 20 năm mới có một định dạng mới thì vẫn chịu được
.webmthì có biến mất cũng được