大家好,很高興又見面了,我是"進階前端進階",由我帶着大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點贊、收藏、轉發,您的支援是我不斷創作的動力。
大家都知道,如果沒有 JavaScript 膠水代碼,目前無法在浏覽器中将 WebAssembly 用于 Web 應用程式。 但是,開發者已經可以使用 WebAssembly 在浏覽器中執行諸多操作,而這些操作在過去很長一段時間隻能使用 JavaScript 執行。
本文主要和大家讨論 Brotli,即 Google 推出的一種無損壓縮算法,通過變種的 LZ77 算法、Huffman 編碼等方式進行資料壓縮。在年初,我也确實使用 WebAssembly 将用戶端應用成功移植到了 Web,這也是為什麼我一直對 WebAssembly 充滿好奇的原因。我甚至在頭條上開了一個合集《WebAssembly 前沿技術》來專門探讨 WebAssembly ,并将持續關注 WebAssembly 的最新動态。
下面是已釋出部分文章傳送門:
- 《 進階語言全面擁抱WebAssembly ?JavaScript瑟瑟發抖?》
- 《 2023 年讓 WebAssembly 大火的 10+應用!》
- 《 萬字長文!2023 年 WebAssembly 各個運作時性能對比!》
- 《 讓 JavaScript 在 WebAssembly 上加速運作!》
- 《全網最火的5+優秀 WebAssembly 運作時!》
- 《 線上表格再添一員猛将excelize,支援 wasm! 》
正如大家所看見,當大多數開發者還在遲疑是否要在日常開發中引入 WebAssembly 的時候,很多優秀的應用、工具已經開始吃 WebAssembly 的紅利了,而且取得了不錯的成就,這可能也是為什麼各個浏覽器廠商、開發者如此熱衷 WebAssembly 的原因吧。
話不多說,直接進入正題!
1.Brotli 壓縮算法介紹
1.1 什麼是 Brotli
速度對于任何網站都至關重要,在 Web 追求快速加載時間的過程中,有許多不同的技術可以幫助開發者。 一種方法是最大限度地減少網站使用的底層代碼,而不影響其功能。 另一種方式是依賴不同的壓縮技術,比如常見的 GZIP,但 Brotli 壓縮是另一種值得關注的新興方法。
Brotli(發音 [b'rɒdi:]) 是 Google 推出的一種無損壓縮算法,通過變種的 LZ77 算法、Huffman 編碼等方式進行資料壓縮,同時壓縮資料格式規範在 RFC 7932 中定義。與其他壓縮算法相比(如 zip,gzip 等),無論是壓縮時間,還是壓縮體積上看,Brotli 都有着更高的效率。
總之,開啟 Brotli 壓縮功能後,CDN 節點會對資源進行智能壓縮後傳回,縮小傳輸檔案大小,提升檔案傳輸效率,減少帶寬消耗。
目前 Brotli 支援壓縮的檔案類型有: text/xml、text/plain、text/css、application/javascript、application/x-javascript、application/rss+xml、text/javascript、image/tiff、image/svg+xml、application/json、application/xml,幾乎涵蓋了 Web 開發資源的方方面面。
1.2 Brotli 與其他壓縮算法資料化比較
壓縮率表示壓縮算法可以減少多少檔案體積,計算公式如下:
壓縮比 =未壓縮大小/壓縮大小
是以數字越高,算法壓縮能力就越好。接下來,首先看一下 Web 開發中出現的典型檔案格式,即 CSS。 對于特定示例,下圖可以看到 Brotli 壓縮比在最佳上更好,在最快上大緻相同:
當然,還可以通過擴充該集合以包含一些 HTML 和 JavaScript 示例來看到這種模式并不是該示例所獨有的:
前三列顯示“最快”的尺寸減小,接下來的三列顯示“最佳”,最後一列顯示 Brotli 的中等品質水準。 正如上圖中看到的,即使在中等品質級别,Brotli 壓縮率也高于 gzip 和 Deflate 的最佳品質級别。
為了評估 Brotli 是否還可以在 Web 檔案領域之外提供較好的壓縮比,一起來看看 Canterbury Corpus,這是用于測試壓縮算法的流行檔案集。從資料來看,很明顯,Brotli 特别适合較大的檔案,但即使對于較小的檔案,Brotli 也與 Deflate 和 gzip 相當甚至稍好。
更多關于 Brotli 壓縮算法的對比可以參考文末的資料,本文不再過多展開。
2.什麼是 brotli-wasm
brotli-wasm 是 Brotli 的可靠壓縮和解壓縮器,通過 wasm 支援 Node.js 和浏覽器環境。Brotli 在 Node 12+以上版本上可用,但在較舊的 Node 或浏覽器中不可用。 有了 brotli-wasm,開發者就可以在任何地方使用brotli算法。
brotli-wasm 包含一個圍繞 Rust Brotli crate 的壓縮和解壓縮 API 的小包裝器,編譯為 wasm,隻需一些設定即可輕松地在 JavaScript 中使用它。
brotli-wasm 是經過實戰測試的,在 Node.js 和浏覽器中作為 HTTP 工具包的一部分在生産中使用,并包括通過 Node.js 和浏覽器測試進行自動建構以確定這一點。
3.如何使用 brotli-wasm
3.1 安裝 brotli-wasm
開發者能夠像使用其他包一樣将 brotli-wasm 直接導入到 Node 中,或者使用任何支援 ES 子產品和 WebAssembly 的打包器(例如: Webpack v4 或 v5、Vite、Rollup 和大多數其他)在浏覽器中導入。
npm install brotli-wasm
對于每個目标(node.js、commonjs 打包器和 ESM 打包器),該子產品導出不同的 WASM 檔案和設定,入口點略有不同。 除了一些可能有所不同的其他導出之外,這些入口點都公開了一緻的預設導出 API(例如,Node 同步公開 brotli 方法,而浏覽器由于 WASM 限制始終需要 await)。
在所有建構中(在浏覽器中 await 導出的 Promise 之後),該子產品公開了兩個核心方法:
- compress(Buffer, [options]) : 使用 Brotli 壓縮緩沖區,同時傳回壓縮後的緩沖區。 可以提供可選的選項對象。 目前唯一支援的選項是 quality,是 1 到 11 之間的數字。
- decompress(Buffer) : 使用 Brotli 解壓縮緩沖區,傳回原始資料。
對于進階使用資料流用例,開發者可以使用用于流壓縮的 CompressStream 和 DecompressStream 類。
3.2 使用 brotli-wasm
如果想用相同的代碼支援 Node.js 和浏覽器,可以在任何地方使用預設導出的 await 浏覽器相容形式。
下面是 Node.js 中的使用用例:
const brotli = require("brotli-wasm");
const compressedData = brotli.compress(Buffer.from("some input"));
const decompressedData = brotli.decompress(compressedData);
console.log(Buffer.from(decompressedData).toString("utf8")); // Prints 'some input'
下面是在浏覽器中使用的代碼示例:
import brotliPromise from "brotli-wasm";
// 導入預設導出
const brotli = await brotliPromise;
// 由于 wasm 要求,導入在浏覽器中是異步的!
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const input = "some input";
const uncompressedData = textEncoder.encode(input);
const compressedData = brotli.compress(uncompressedData);
const decompressedData = brotli.decompress(compressedData);
console.log(textDecoder.decode(decompressedData));
// 列印 'some input'
當然,在浏覽器中也可以使用 CDN 的方式進行導入:
let brotli = await import(
"https://unpkg.com/[email protected]/index.web.js?module"
).then((m) => m.default);
該包本身沒有運作時依賴性,如果更喜歡使用 Buffer 而不是 TextEncoder/TextDecoder,可能需要浏覽器 Buffer polyfill,即 browserify-zlib。
下面的代碼是在浏覽器環境中以流的方式使用:
import brotliPromise from "brotli-wasm";
const brotli = await brotliPromise;
const input = "some input";
// 擷取輸入流:
const inputStream = new ReadableStream({
start(controller) {
controller.enqueue(input);
controller.close();
},
});
// 如有必要,将流資料轉換為 Uint8Arrays:
const textEncoderStream = new TextEncoderStream();
// 可以在此處使用喜歡的任何流分塊大小,具體取決于用例:
const OUTPUT_SIZE = 100;
// 建立一個流以在資料流式傳輸時增量壓縮資料:
const compressStream = new brotli.CompressStream();
const compressionStream = new TransformStream({
transform(chunk, controller) {
let resultCode;
let inputOffset = 0;
// 壓縮該塊,一次最多産生 OUTPUT_SIZE 輸出位元組,直到
// 整個輸入已被壓縮。
do {
const input = chunk.slice(inputOffset);
const result = compressStream.compress(input, OUTPUT_SIZE);
controller.enqueue(result.buf);
resultCode = result.code;
inputOffset += result.input_offset;
} while (resultCode === brotli.BrotliStreamResultCode.NeedsMoreOutput);
if (resultCode !== brotli.BrotliStreamResultCode.NeedsMoreInput) {
controller.error(
`Brotli compression failed when transforming with code ${resultCode}`
);
}
},
flush(controller) {
// 一旦塊完成,重新整理所有剩餘資料(再次重複固定輸出)
// chunks) 來完成流:
let resultCode;
do {
const result = compressStream.compress(undefined, OUTPUT_SIZE);
controller.enqueue(result.buf);
resultCode = result.code;
} while (resultCode === brotli.BrotliStreamResultCode.NeedsMoreOutput);
if (resultCode !== brotli.BrotliStreamResultCode.ResultSuccess) {
controller.error(
`Brotli compression failed when flushing with code ${resultCode}`
);
}
controller.terminate();
},
});
const decompressStream = new brotli.DecompressStream();
const decompressionStream = new TransformStream({
transform(chunk, controller) {
let resultCode;
let inputOffset = 0;
// 解壓縮該塊,一次産生最多 OUTPUT_SIZE 輸出位元組,直到
// 整個輸入已被解壓縮。
do {
const input = chunk.slice(inputOffset);
const result = decompressStream.decompress(input, OUTPUT_SIZE);
controller.enqueue(result.buf);
resultCode = result.code;
inputOffset += result.input_offset;
} while (resultCode === brotli.BrotliStreamResultCode.NeedsMoreOutput);
if (
resultCode !== brotli.BrotliStreamResultCode.NeedsMoreInput &&
resultCode !== brotli.BrotliStreamResultCode.ResultSuccess
) {
controller.error(`Brotli decompression failed with code ${resultCode}`);
}
},
flush(controller) {
controller.terminate();
},
});
const textDecoderStream = new TextDecoderStream();
let output = "";
const outputStream = new WritableStream({
write(chunk) {
output += chunk;
},
});
await inputStream
.pipeThrough(textEncoderStream)
.pipeThrough(compressionStream)
.pipeThrough(decompressionStream)
.pipeThrough(textDecoderStream)
.pipeTo(outputStream);
console.log(output);
// 列印 'some input'
需要注意的是,自 2022 年下半年開始,TransformStream 已在所有浏覽器中可用:https://caniuse.com/mdn-api_transformstream。 自 Node.js v16.5.0 起,它也可在 Node.js 中(實驗性地)使用。
4.brotli-wasm 相關替代品介紹
還有一些其他軟體包可以做 brotli-wasm 類似的事情,但是不幸的是,大部分都無法使用/或無法維護:
- brotli-dec-wasm : 僅解壓縮器,從 Rust 編譯,功能與 brotli-wasm 類似,積極維護,但沒有可用的壓縮器(按設計)。 如果你隻需要解壓,這個包是個不錯的選擇。
- Brotli.js : 手寫的 JS 解壓縮器,在大多數情況下似乎工作正常,但在某些邊界情況下會崩潰,并且壓縮器的 emscripten 版本根本無法在浏覽器中工作,最後一次更新還是在 2017 年。
- wasm-brotli : 與 brotli-wasm 包一樣從 Rust 編譯,包括解壓縮器和壓縮器,但需要一個自定義的異步包裝器來使用 Webpack v4,并且在 Webpack v5 中根本不可用, 最後一次更新還是在 2019 年。
5.本文總結
本文主要和大家介紹 Brotli,即 Google 推出的一種無損壓縮算法,通過變種的 LZ77 算法、Huffman 編碼等方式進行資料壓縮。相信通過本文的閱讀,大家對 Brotli 會有一個初步的了解。
因為篇幅有限,關于 Brotli 的更多用法和特性文章并沒有過多展開,如果有興趣,可以在我的首頁繼續閱讀,同時文末的參考資料提供了大量優秀文檔以供學習。最後,歡迎大家點贊、評論、轉發、收藏,您的支援是我不斷創作的動力。
參考資料
https://github.com/httptoolkit/brotli-wasm
https://www.npmjs.com/package/browserify-zlib
https://kinsta.com/blog/brotli-compression/
https://www.alibabacloud.com/help/zh/cdn/user-guide/configure-brotli-compression
https://blog.csdn.net/weixin_44158429/article/details/130252308
https://devblogs.microsoft.com/dotnet/introducing-support-for-brotli-compression/
https://nickb.dev/blog/wasm-compression-benchmarks-and-the-cost-of-missing-compression-apis/
https://10web.io/site-speed-glossary/brotli-compression/