From b644dc588bb102023da4b1fc25b90761a450c431 Mon Sep 17 00:00:00 2001 From: bigsaltyfishes Date: Sat, 7 Jun 2025 16:14:41 +0800 Subject: [PATCH 1/8] feat: Add Gzip compress support for wasm binary * Add data-gzip-compression attribute. * Enable compression by default on release build. * Use DecompressionStream API to decompress at runtime. Signed-off-by: bigsaltyfishes --- src/pipelines/rust/gzip.rs | 55 ++++++++++++++++++++++++ src/pipelines/rust/mod.rs | 83 +++++++++++++++++++++++++++++++++++- src/pipelines/rust/output.rs | 38 ++++++++++++++--- 3 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 src/pipelines/rust/gzip.rs diff --git a/src/pipelines/rust/gzip.rs b/src/pipelines/rust/gzip.rs new file mode 100644 index 00000000..c687c216 --- /dev/null +++ b/src/pipelines/rust/gzip.rs @@ -0,0 +1,55 @@ +use std::{ops::Deref, str::FromStr}; + +use anyhow::bail; +use flate2::Compression; + +#[derive(PartialEq, Eq)] +pub struct GzipLevel(Compression); + +impl GzipLevel { + pub const OFF: Self = Self(Compression::new(0)); +} + +impl FromStr for GzipLevel { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let level = match s { + "0" => Compression::new(0), + "1" => Compression::new(1), + "2" => Compression::new(2), + "3" => Compression::new(3), + "4" => Compression::new(4), + "5" => Compression::new(5), + "6" => Compression::new(6), + "7" => Compression::new(7), + "8" => Compression::new(8), + "9" => Compression::new(9), + "default" => Compression::default(), + "fast" => Compression::fast(), + "best" => Compression::best(), + _ => bail!("unknown gzip level `{}`", s), + }; + Ok(Self(level)) + } +} + +impl AsRef for GzipLevel { + fn as_ref(&self) -> &Compression { + &self.0 + } +} + +impl Deref for GzipLevel { + type Target = Compression; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for GzipLevel { + fn default() -> Self { + Self(Compression::default()) + } +} diff --git a/src/pipelines/rust/mod.rs b/src/pipelines/rust/mod.rs index 7a631608..90d59b29 100644 --- a/src/pipelines/rust/mod.rs +++ b/src/pipelines/rust/mod.rs @@ -1,5 +1,6 @@ //! Rust application pipeline. +mod gzip; mod output; mod sri; mod wasm_bindgen; @@ -18,12 +19,16 @@ use crate::{ types::CrossOrigin, CargoMetadata, }, - pipelines::rust::sri::{SriBuilder, SriOptions, SriType}, + pipelines::rust::{ + gzip::GzipLevel, + sri::{SriBuilder, SriOptions, SriType}, + }, processing::{integrity::IntegrityType, minify::minify_js}, tools::{self, Application, ToolInformation}, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use cargo_metadata::{Artifact, TargetKind}; +use flate2::bufread::GzEncoder; use minify_js::TopLevelMode; use seahash::SeaHasher; use std::{ @@ -97,6 +102,8 @@ pub struct RustApp { import_bindings_name: Option, /// The name of the initializer module initializer: Option, + /// Gzip compression level for the WASM file. Defaults to `GzipLevel::Default` on release build. + gzip_compression: GzipLevel, } /// Describes how the rust application is used. @@ -185,6 +192,17 @@ impl RustApp { RustAppType::Main => WasmBindgenTarget::Web, RustAppType::Worker => WasmBindgenTarget::NoModules, }); + let gzip_compression = attrs + .get("data-gzip-compression") + .map(|attr| attr.parse()) + .transpose()? + .unwrap_or_else(|| { + if cfg.release { + Default::default() + } else { + GzipLevel::OFF + } + }); let cross_origin = attrs .get("data-cross-origin") .map(|attr| CrossOrigin::from_str(attr)) @@ -307,6 +325,7 @@ impl RustApp { import_bindings_name, initializer, target_path, + gzip_compression }) } @@ -357,6 +376,7 @@ impl RustApp { import_bindings_name: None, initializer: None, target_path: None, + gzip_compression: GzipLevel::OFF, })) } @@ -386,6 +406,11 @@ impl RustApp { .await .context("running wasm-opt")?; + // (optionally) gzip the wasm file + self.gzip_compression(&output.wasm_output) + .await + .context("gzip compression")?; + // evaluate wasm integrity after all processing self.final_digest(&mut output) .await @@ -761,6 +786,7 @@ impl RustApp { import_bindings: self.import_bindings, import_bindings_name: self.import_bindings_name.clone(), initializer, + gzip_compression_enabled: self.gzip_compression != GzipLevel::OFF, wasm_bindgen_features, }) } @@ -954,6 +980,61 @@ impl RustApp { Ok(()) } + #[tracing::instrument(level = "trace", skip(self))] + async fn gzip_compression(&self, wasm_name: &str) -> Result<()> { + if !self.cfg.release { + return Ok(()); + } + + if self.gzip_compression == GzipLevel::OFF { + log::debug!("gzip compression is turned off"); + return Ok(()); + } + + let gzip_name = "wasm-gzip"; + let mode_segment = if self.cfg.release { "release" } else { "debug" }; + let output = self + .manifest + .metadata + .target_directory + .join(gzip_name) + .join(mode_segment); + fs::create_dir_all(&output) + .await + .context("error creating wasm gzip compression output dir")?; + + tracing::debug!("compressing with gzip level {}", self.gzip_compression.level()); + let output = output.join(format!("{}_bg.wasm", self.name)); + let target_wasm = self + .cfg + .staging_dist + .join(wasm_name) + .to_string_lossy() + .to_string(); + + let target_wasm_file = std::fs::File::open(&target_wasm) + .context("error opening wasm file for gzip compression")?; + let target_wasm_reader = std::io::BufReader::new(target_wasm_file); + let mut encoder = GzEncoder::new( + target_wasm_reader, + *self.gzip_compression, + ); + + let output_file = std::fs::File::create(&output) + .context("error creating gzip output file")?; + let mut output_writer = std::io::BufWriter::new(output_file); + std::io::copy(&mut encoder, &mut output_writer) + .context("error writing gzip compressed wasm file")?; + + // Copy the generated WASM file to the dist dir. + tracing::debug!("copying generated wasm-opt artifact from '{output}' to '{target_wasm}'"); + fs::copy(output, &target_wasm) + .await + .context("error copying (gzip compressed) wasm file to dist dir")?; + + Ok(()) + } + /// Build the final WASM digest #[tracing::instrument(level = "trace", skip(self, output))] async fn final_digest(&self, output: &mut RustAppOutput) -> Result<()> { diff --git a/src/pipelines/rust/output.rs b/src/pipelines/rust/output.rs index 66e926e1..2ffbeca2 100644 --- a/src/pipelines/rust/output.rs +++ b/src/pipelines/rust/output.rs @@ -29,6 +29,8 @@ pub struct RustAppOutput { pub import_bindings: bool, /// The name of the WASM bindings import pub import_bindings_name: Option, + /// Gzip compression enabled for the WASM file + pub gzip_compression_enabled: bool, /// The target of the initializer module pub initializer: Option, /// The features supported by the version of wasm-bindgen used @@ -141,21 +143,43 @@ dispatchEvent(new CustomEvent("TrunkApplicationStarted", {detail: {wasm}})); let init_with_object = self.wasm_bindgen_features.init_with_object; match &self.initializer { - None => format!( - r#" + None => { + if self.gzip_compression_enabled { + format!( + r#" "#, - init_arg = if init_with_object { - format!("{{ module_or_path: '{base}{wasm}' }}") + ) } else { - format!("'{base}{wasm}'") + format!( + r#" +"#, + init_arg = if init_with_object { + format!("{{ module_or_path: '{base}{wasm}' }}") + } else { + format!("'{base}{wasm}'") + } + ) } - ), + } Some(initializer) => format!( r#" "#, - ) - } else { - format!( - r#" + let init_with_object = self.wasm_bindgen_features.init_with_object; + + match &self.initializer { + None => { + format!( + r#" "#, - init_arg = if init_with_object { - format!("{{ module_or_path: '{base}{wasm}' }}") - } else { - format!("'{base}{wasm}'") - } - ) - } + init_arg = if self.compression_algorithm.is_some() { + "{ module_or_path: wasmBytes }".to_string() + } else if init_with_object { + format!("{{ module_or_path: '{base}{wasm}' }}") + } else { + format!("'{base}{wasm}'") + } + ) } Some(initializer) => format!( r#" From a42dc047005bc97337a08e7e4ad9a64be65299f0 Mon Sep 17 00:00:00 2001 From: bigsaltyfishes Date: Tue, 10 Jun 2025 17:21:54 +0800 Subject: [PATCH 6/8] fix: pipelines: add missing compression support for custom initializer Signed-off-by: bigsaltyfishes --- src/pipelines/rust/initializer.js | 10 ++++++++-- src/pipelines/rust/output.rs | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pipelines/rust/initializer.js b/src/pipelines/rust/initializer.js index 1a7ba223..a37c4da6 100644 --- a/src/pipelines/rust/initializer.js +++ b/src/pipelines/rust/initializer.js @@ -1,4 +1,4 @@ -async function __trunkInitializer(init, source, sourceSize, initializer, initWithObject) { +async function __trunkInitializer(init, source, sourceSize, initializer, initWithObject, compressAlgorithm) { if (initializer === undefined) { return await init(initWithObject ? { module_or_path: source } : source); } @@ -11,7 +11,6 @@ async function __trunkInitializer(init, source, sourceSize, initializer, initWit const response = fetch(source) .then((response) => { - const reader = response.body.getReader(); const headers = response.headers; const status = response.status; const statusText = response.statusText; @@ -19,6 +18,13 @@ async function __trunkInitializer(init, source, sourceSize, initializer, initWit const total = sourceSize; let current = 0; + let reader = undefined; + if (compressAlgorithm) { + reader = response.body.pipeThrough(new DecompressionStream(compressAlgorithm)).getReader(); + } else { + reader = response.body.getReader(); + } + const stream = new ReadableStream({ start(controller) { function push() { diff --git a/src/pipelines/rust/output.rs b/src/pipelines/rust/output.rs index 39efa535..d6505a45 100644 --- a/src/pipelines/rust/output.rs +++ b/src/pipelines/rust/output.rs @@ -187,13 +187,18 @@ const wasm = await init({init_arg}); import init{import} from '{base}{js}'; import initializer from '{base}{initializer}'; -const wasm = await __trunkInitializer(init, '{base}{wasm}', {size}, initializer(), {init_with_object}); +const wasm = await __trunkInitializer(init, '{base}{wasm}', {size}, initializer(), {init_with_object}{algorithm}); {bind} {fire} "#, init = include_str!("initializer.js"), size = self.wasm_size, + algorithm = if let Some(algorithm) = &self.compression_algorithm { + format!(", '{algorithm}'") + } else { + String::new() + }, ), } } From 4b7e6a178c2e319e5122b1a346716e2d9b89ea27 Mon Sep 17 00:00:00 2001 From: bigsaltyfishes Date: Tue, 10 Jun 2025 18:13:07 +0800 Subject: [PATCH 7/8] fix: pipelines: fix compression support when missing wasm-bingen feature `init_with_object` Signed-off-by: bigsaltyfishes --- src/pipelines/rust/output.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pipelines/rust/output.rs b/src/pipelines/rust/output.rs index d6505a45..9e2a1145 100644 --- a/src/pipelines/rust/output.rs +++ b/src/pipelines/rust/output.rs @@ -170,12 +170,18 @@ const wasm = await init({init_arg}); {bind} {fire} "#, - init_arg = if self.compression_algorithm.is_some() { - "{ module_or_path: wasmBytes }".to_string() - } else if init_with_object { - format!("{{ module_or_path: '{base}{wasm}' }}") - } else { - format!("'{base}{wasm}'") + init_arg = { + let param = if self.compression_algorithm.is_some() { + "wasmBytes".to_string() + } else { + format!("'{base}{wasm}'") + }; + + if init_with_object { + format!("{{ module_or_path: {param} }}") + } else { + param + } } ) } From 4758424b9c026b79cfe94fde1ef318df4f9c9216 Mon Sep 17 00:00:00 2001 From: bigsaltyfishes Date: Tue, 10 Jun 2025 19:19:55 +0800 Subject: [PATCH 8/8] fix: pipelines: lint fix Signed-off-by: bigsaltyfishes --- src/pipelines/rust/compress.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipelines/rust/compress.rs b/src/pipelines/rust/compress.rs index d4a162fc..0d11b6a1 100644 --- a/src/pipelines/rust/compress.rs +++ b/src/pipelines/rust/compress.rs @@ -96,4 +96,4 @@ impl Deref for CompressionLevel { fn deref(&self) -> &Self::Target { &self.0 } -} \ No newline at end of file +}