From 47ebe620032a38e63d77da2e497ea0751d7477ed Mon Sep 17 00:00:00 2001 From: Nathan Whitaker Date: Fri, 12 Jun 2026 16:28:33 -0700 Subject: [PATCH] perf(snapshot): minify snapshot sources --- .github/workflows/ci.generated.yml | 33 +++++++ .github/workflows/ci.ts | 7 ++ cli/snapshot/build.rs | 17 +++- runtime/snapshot.rs | 10 ++- runtime/transpile.rs | 136 ++++++++++++++++++++++++++++- 5 files changed, 194 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.generated.yml b/.github/workflows/ci.generated.yml index 3412e61c386299..1fb79c25fd6a41 100644 --- a/.github/workflows/ci.generated.yml +++ b/.github/workflows/ci.generated.yml @@ -234,6 +234,8 @@ jobs: run: ./tools/install_prebuilt.js wrk hyperfine - name: Build deno if: '(contains(github.event.pull_request.labels.*.name, ''ci-bench'') || github.ref == ''refs/heads/main'') && !startsWith(github.ref, ''refs/tags/'')' + env: + DENO_SNAPSHOT_MINIFY_SOURCES: '1' run: cargo build --release -p deno - name: Run benchmarks if: '(contains(github.event.pull_request.labels.*.name, ''ci-bench'') || github.ref == ''refs/heads/main'') && !startsWith(github.ref, ''refs/tags/'')' @@ -759,6 +761,8 @@ jobs: run: echo "DENO_CANARY=true" >> $GITHUB_ENV - name: Build release if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && github.repository == ''denoland/deno''' + env: + DENO_SNAPSHOT_MINIFY_SOURCES: '1' run: |- df -h cargo build --release --locked -p deno -p denort -p test_server --bin deno --bin denort --bin test_server --features=deno/panic-trace @@ -1766,6 +1770,8 @@ jobs: run: echo "DENO_CANARY=true" >> $GITHUB_ENV - name: Build release if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && github.repository == ''denoland/deno''' + env: + DENO_SNAPSHOT_MINIFY_SOURCES: '1' run: |- df -h cargo build --release --locked -p deno -p denort -p test_server --bin deno --bin denort --bin test_server --features=deno/panic-trace @@ -2627,6 +2633,11 @@ jobs: - name: Clone submodule ./tests/util/std if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'')' run: git submodule update --init --recursive --depth=1 -- ./tests/util/std + - name: Install Deno + uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 + if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && github.repository == ''denoland/deno''' + with: + deno-version: v2.x - name: Restore cache cargo home uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && !startsWith(github.ref, ''refs/tags/'')' @@ -2703,6 +2714,8 @@ jobs: run: echo "DENO_CANARY=true" >> $GITHUB_ENV - name: Build release if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && github.repository == ''denoland/deno''' + env: + DENO_SNAPSHOT_MINIFY_SOURCES: '1' run: |- df -h cargo build --release --locked -p deno -p denort -p test_server --bin deno --bin denort --bin test_server --features=deno/panic-trace @@ -3447,6 +3460,11 @@ jobs: - name: Clone submodule ./tests/util/std if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'')' run: git submodule update --init --recursive --depth=1 -- ./tests/util/std + - name: Install Deno + uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 + if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && github.repository == ''denoland/deno''' + with: + deno-version: v2.x - name: Restore cache cargo home uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && !startsWith(github.ref, ''refs/tags/'')' @@ -3523,6 +3541,8 @@ jobs: run: echo "DENO_CANARY=true" >> $GITHUB_ENV - name: Build release if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && github.repository == ''denoland/deno''' + env: + DENO_SNAPSHOT_MINIFY_SOURCES: '1' run: |- df -h cargo build --release --locked -p deno -p denort -p test_server --bin deno --bin denort --bin test_server --features=deno/panic-trace @@ -3919,6 +3939,10 @@ jobs: mkdir -p target/release tar --exclude=".git*" --exclude=target --exclude=third_party/prebuilt \ -czvf target/release/deno_src.tar.gz -C .. deno + - name: Install Deno + uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 + with: + deno-version: v2.x - name: Restore cache cargo home uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 if: '!startsWith(github.ref, ''refs/tags/'')' @@ -4085,6 +4109,8 @@ jobs: if: '!startsWith(github.ref, ''refs/tags/'')' run: echo "DENO_CANARY=true" >> $GITHUB_ENV - name: Build release + env: + DENO_SNAPSHOT_MINIFY_SOURCES: '1' run: |- df -h cargo build --release --locked -p deno -p denort -p test_server --bin deno --bin denort --bin test_server --features=deno/panic-trace @@ -5862,6 +5888,11 @@ jobs: - name: Clone submodule ./tests/util/std if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'')' run: git submodule update --init --recursive --depth=1 -- ./tests/util/std + - name: Install Deno + uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 + if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'')' + with: + deno-version: v2.x - name: Restore cache cargo home uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'') && !startsWith(github.ref, ''refs/tags/'')' @@ -6034,6 +6065,8 @@ jobs: run: echo "DENO_CANARY=true" >> $GITHUB_ENV - name: Build release if: '!(!contains(github.event.pull_request.labels.*.name, ''ci-full'') && github.event_name == ''pull_request'')' + env: + DENO_SNAPSHOT_MINIFY_SOURCES: '1' run: |- df -h cargo build --release --locked -p deno -p denort -p test_server --bin deno --bin denort --bin test_server --features=deno/panic-trace diff --git a/.github/workflows/ci.ts b/.github/workflows/ci.ts index 9a5f17c2469fcf..8d9a94be0c115f 100755 --- a/.github/workflows/ci.ts +++ b/.github/workflows/ci.ts @@ -874,6 +874,7 @@ const buildJobs = buildItems.map((rawBuildItem) => { ) .dependsOn( installLldStep, + installDenoStep, restoreCacheStep, installRustStep, sysRootStep, @@ -886,6 +887,9 @@ const buildJobs = buildItems.map((rawBuildItem) => { }, { name: "Build release", + env: { + DENO_SNAPSHOT_MINIFY_SOURCES: "1", + }, run: [ // output fs space before and after building "df -h", @@ -1474,6 +1478,9 @@ const benchJob = job( // we could optimize this to not need this. { name: "Build deno", + env: { + DENO_SNAPSHOT_MINIFY_SOURCES: "1", + }, run: "cargo build --release -p deno", }, { diff --git a/cli/snapshot/build.rs b/cli/snapshot/build.rs index 80553c21863528..6d37500a964bfb 100644 --- a/cli/snapshot/build.rs +++ b/cli/snapshot/build.rs @@ -9,6 +9,7 @@ fn main() { #[allow(clippy::print_stdout, reason = "build script output")] { println!("cargo:rerun-if-env-changed=DENO_SNAPSHOT_IMPORT_GRAPH"); + println!("cargo:rerun-if-env-changed=DENO_SNAPSHOT_MINIFY_SOURCES"); } #[cfg(not(feature = "disable"))] { @@ -51,6 +52,8 @@ fn create_cli_snapshot( let out_dir = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); let residual_sources_dir = out_dir.join("residual_sources"); std::fs::create_dir_all(&residual_sources_dir).unwrap(); + let minify_sources = + std::env::var_os("DENO_SNAPSHOT_MINIFY_SOURCES").is_some(); let mut residual_js: Vec<(&str, std::path::PathBuf)> = Vec::new(); let mut residual_esm: Vec<(&str, std::path::PathBuf)> = Vec::new(); @@ -73,6 +76,7 @@ fn create_cli_snapshot( &residual_sources_dir, &file.specifier, &file.path, + minify_sources, ); match file.kind { LazyExtensionFileKind::Js => { @@ -109,9 +113,11 @@ fn transpile_residual_source( out_dir: &std::path::Path, specifier: &str, src_path: &std::path::Path, + minify: bool, ) -> std::path::PathBuf { use deno_runtime::deno_core::ModuleCodeString; use deno_runtime::deno_core::ModuleName; + use deno_runtime::transpile::maybe_transpile_and_minify_source; use deno_runtime::transpile::maybe_transpile_source; let source = std::fs::read_to_string(src_path).unwrap_or_else(|e| { @@ -120,10 +126,13 @@ fn transpile_residual_source( src_path.display() ) }); - let (transpiled, _source_map) = maybe_transpile_source( - ModuleName::from(specifier.to_string()), - ModuleCodeString::from(source), - ) + let name = ModuleName::from(specifier.to_string()); + let source = ModuleCodeString::from(source); + let (transpiled, _source_map) = if minify { + maybe_transpile_and_minify_source(name, source) + } else { + maybe_transpile_source(name, source) + } .unwrap_or_else(|e| { panic!("failed to transpile residual lazy source {specifier}: {e}") }); diff --git a/runtime/snapshot.rs b/runtime/snapshot.rs index 96c07354922ea8..f392fc52a25aae 100644 --- a/runtime/snapshot.rs +++ b/runtime/snapshot.rs @@ -102,14 +102,20 @@ pub fn create_runtime_snapshot( extensions.extend(custom_extensions); let lazy_extension_files = collect_lazy_extension_files(&extensions); + let minify_sources = + std::env::var_os("DENO_SNAPSHOT_MINIFY_SOURCES").is_some(); let output = create_snapshot( CreateSnapshotOptions { cargo_manifest_dir: env!("CARGO_MANIFEST_DIR"), startup_snapshot: None, extensions, - extension_transpiler: Some(Rc::new(|specifier, source| { - crate::transpile::maybe_transpile_source(specifier, source) + extension_transpiler: Some(Rc::new(move |specifier, source| { + if minify_sources { + crate::transpile::maybe_transpile_and_minify_source(specifier, source) + } else { + crate::transpile::maybe_transpile_source(specifier, source) + } })), with_runtime_cb: Some(Box::new(|rt| { let isolate = rt.v8_isolate(); diff --git a/runtime/transpile.rs b/runtime/transpile.rs index 91dba30426a892..0ce1f8ee071bd3 100644 --- a/runtime/transpile.rs +++ b/runtime/transpile.rs @@ -1,6 +1,8 @@ // Copyright 2018-2026 the Deno authors. MIT license. +use std::io::Write; use std::path::Path; +use std::sync::OnceLock; use deno_ast::MediaType; use deno_ast::ParseParams; @@ -25,6 +27,22 @@ pub fn maybe_transpile_source( name: ModuleName, source: ModuleCodeString, ) -> Result<(ModuleCodeString, Option), JsErrorBox> { + maybe_transpile_source_inner(name, source, false) +} + +pub fn maybe_transpile_and_minify_source( + name: ModuleName, + source: ModuleCodeString, +) -> Result<(ModuleCodeString, Option), JsErrorBox> { + maybe_transpile_source_inner(name, source, true) +} + +fn maybe_transpile_source_inner( + name: ModuleName, + source: ModuleCodeString, + minify: bool, +) -> Result<(ModuleCodeString, Option), JsErrorBox> { + let name_string = name.to_string(); // Always transpile `node:` built-in modules, since they might be TypeScript. let media_type = if name.starts_with("node:") { MediaType::TypeScript @@ -34,8 +52,14 @@ pub fn maybe_transpile_source( match media_type { MediaType::TypeScript => {} - MediaType::JavaScript => return Ok((source, None)), - MediaType::Mjs => return Ok((source, None)), + MediaType::JavaScript | MediaType::Mjs => { + if minify { + let source = + minify_source_with_rolldown(&name_string, source.as_ref())?; + return Ok((source.into(), None)); + } + return Ok((source, None)); + } _ => panic!( "Unsupported media type for snapshotting {media_type:?} for file {}", name @@ -74,5 +98,111 @@ pub fn maybe_transpile_source( .source_map .map(|sm| sm.into_bytes().into()); let source_text = transpiled_source.text; - Ok((source_text.into(), maybe_source_map)) + if minify { + let source_text = minify_source_with_rolldown(&name_string, &source_text)?; + Ok((source_text.into(), None)) + } else { + Ok((source_text.into(), maybe_source_map)) + } +} + +#[allow( + clippy::disallowed_methods, + reason = "snapshot source minification runs at build time" +)] +fn minify_source_with_rolldown( + specifier: &str, + source: &str, +) -> Result { + static MINIFIER_PATH: OnceLock = OnceLock::new(); + let minifier_path = MINIFIER_PATH.get_or_init(|| { + let path = + std::env::temp_dir().join("deno_snapshot_rolldown_minify_source.js"); + std::fs::write( + &path, + r#"import { minifySync } from "npm:rolldown/experimental"; + +const filename = Deno.args[0] ?? "source.js"; +const source = await new Response(Deno.stdin.readable).text(); +const result = minifySync(filename, source, { + compress: false, + mangle: false, + codegen: { + removeWhitespace: true, + legalComments: "none", + }, +}); +if (result.errors?.length) { + console.error(JSON.stringify(result.errors, null, 2)); + Deno.exit(1); +} +function escapeNonAscii(code) { + let out = ""; + for (let i = 0; i < code.length; i++) { + const unit = code.charCodeAt(i); + if (unit <= 0x7f) { + out += code[i]; + } else { + out += "\\u" + unit.toString(16).padStart(4, "0"); + } + } + return out; +} +const output = new TextEncoder().encode(escapeNonAscii(result.code)); +let offset = 0; +while (offset < output.length) { + offset += await Deno.stdout.write(output.subarray(offset)); +} +"#, + ) + .unwrap(); + path + }); + + let mut child = std::process::Command::new("deno") + .arg("run") + .arg("-A") + .arg(minifier_path) + .arg(specifier) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .map_err(|e| { + JsErrorBox::generic(format!( + "failed to run rolldown source minifier for {specifier}: {e}" + )) + })?; + child + .stdin + .as_mut() + .unwrap() + .write_all(source.as_bytes()) + .map_err(|e| { + JsErrorBox::generic(format!( + "failed to write source to rolldown minifier for {specifier}: {e}" + )) + })?; + let output = child.wait_with_output().map_err(|e| { + JsErrorBox::generic(format!( + "failed to wait for rolldown source minifier for {specifier}: {e}" + )) + })?; + if !output.status.success() { + return Err(JsErrorBox::generic(format!( + "failed to minify source {specifier}\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ))); + } + let output = String::from_utf8(output.stdout).map_err(|e| { + JsErrorBox::generic(format!( + "rolldown minifier produced non-utf8 output for {specifier}: {e}" + )) + })?; + assert!( + output.is_ascii(), + "rolldown minifier produced non-ascii output for {specifier}" + ); + Ok(output) }