AfterPack performance: measured speed and size

What AfterPack costs a build and a page: measured obfuscation time on a 185 KB bundle, the wasm build's time and download size, the output size, page startup time and runtime cost of each preset, what protected code pays to read a host global, and how a new seed affects caching.

Every number on this page was measured, not estimated. Unless a table says otherwise, the machine was an Apple M2 Max, 32 GB RAM, macOS 26.6.2. Nothing here is a comparison against another tool.

The fixture behind most of it is next16_framework_runtime: 185 KB of the framework runtime a real Next.js 16 build ships. It is one already-minified production bundle, which is the shape AfterPack actually runs on.

Build time

Measured on 2026-09-29, on the machine above, with AfterPack 0.2.0 at the default light preset.

What ranInputTimeThroughput
The npm package, on the 185 KB bundle185 KB229 ms0.79 MB/s
The engine on its own, same bundle185 KB243 ms0.74 MB/s
The npm package, on a 766 KB multi-file build766 KB1,362 ms0.55 MB/s

The npm row is the one to plan against. It is the published package doing the work the way a plugin or npx afterpack@latest does it, so it includes the cost of getting your code into the engine and back out. The engine row is the same bundle at the same preset with that cost removed, timed by a separate harness. The two land within 15 ms of each other, so the packaging adds no measurable cost.

Peak memory on the engine row was 20.8 MB above baseline for a single 185 KB file. The 766 KB multi-file run held 76 MB at its high-water mark.

Obfuscation is a post-build step, so this time is added to your build, not to your users' page loads. A 766 KB build in about 1.4 seconds is the shape of the cost at the default preset; a higher preset costs more.

The wasm build

@afterpack/wasm is the same engine compiled to WebAssembly, for a Cloudflare Worker, for Node, and for the playground on this site.

Time, at a presetNode 24
110 KB bundle, light147 ms
185 KB bundle, light237 ms
185 KB bundle, medium1,011 ms
185 KB bundle, extreme2,603 ms

Each time is the median of nine runs with the Protection Map on, measured on 2026-10-17 with @afterpack/wasm 0.2.6 on the machine above. The 110 KB bundle is the polyfills chunk of the same Next.js 16 build.

The download is 4.36 MiB raw and 1.25 MiB gzipped. It happens once per Worker isolate or per browser tab, not once per build.

The speed is enforced: a CI check times the engine on the 185 KB fixture at the extreme preset with the Protection Map on, timed alongside a pinned reference engine in the same run, and fails the build if the engine's fastest run takes more than 13.5 times the reference's.

Wasm runs slower than the native build, and its speed is a deliberate trade: an earlier build optimised for size came out 39% smaller gzipped and 2.1x slower, which pushed a real 66-file build past the cloud engine's deadline. Speed won.

These are laptop times, not Cloudflare times. A Worker isolate on Cloudflare's network has less CPU, a colder cache, and a per-request CPU limit, so the same input takes longer there than this table says. Measure on the platform you will run on before you size a budget against these numbers.

Output size

Obfuscation adds bytes. Measured on 2026-10-16 with AfterPack 0.2.6, across five fixtures (the 185 KB runtime above, two more Next.js 16 chunks, a Vite React bundle and a PostHog chunk), per preset, on gzipped output against the same file minified:

PresetGzipped output multiplier
minify1.09x to 1.17x
light1.29x to 1.60x
medium6.04x to 8.33x
hard6.62x to 9.23x
extreme8.25x to 11.19x

Each range is the spread across the fixtures, not a confidence interval, and it rises monotonically with the preset.

The complexity target sets output size; these ranges are measurements of what each preset produced. Set an explicit inflation.max when you want a build to fail rather than exceed a size you choose; see Presets.

Small files come out proportionally larger than any of these, because every output carries the engine's fixed runtime overhead. Read a ratio on a real bundle.

Page load

A heavier preset makes a page slower to start, because the browser parses and runs more code before the page responds. Measured on 2026-10-24 with AfterPack 1.0.0, on the machine above, in headless Chromium: five small apps from the end-to-end tests of the AfterPack plugins, each built once without AfterPack and once per preset with the whole bundle at that preset, and served from localhost, so no network time is included. Each figure is the time from navigation to the first click on the app's counter that changes it, the median of five cold loads, each in a fresh browser context.

AppWithout AfterPacklightmediumhardextreme
Vite and React35 ms52 ms253 ms295 ms596 ms
Vue on Vite31 ms36 ms176 ms117 ms199 ms
Astro islands44 ms48 ms205 ms249 ms622 ms
SvelteKit27 ms32 ms84 ms89 ms162 ms
Next.js, webpack42 ms74 ms462 ms573 ms1,010 ms
Next.js, webpack, CPU throttled 4x145 ms234 ms1,652 ms2,215 ms3,844 ms

The last row slows Chromium's CPU fourfold, a rough stand-in for a mid-range phone.

Before protection, the pages load 168 KB to 453 KB of JavaScript. Size grows by about the same factor on every app (Presets has the ratios); startup time grows with how much of that code runs before the first click. Main-thread script time at medium was 4.5x the unprotected build's on Vue and 14.7x on Next.js. The framework's own runtime is part of each bundle, and every preset protects it too.

At light, the first working click came 4 ms to 32 ms later. From medium up, measure your own pages before you ship the preset across the whole bundle; Presets has what each tier lets you scope.

Runtime cost

Protected code runs slower than the original, and the cost climbs with the preset. Measured with AfterPack 0.2.4 on the self-hosted x86_64 CI runner, Node 22: four tight-loop programs, each figure the protected run time divided by the original's, the median across the four.

PresetRun time against the original
minify1.0x
light2.1x
medium32.5x
hard35.0x
extreme37.3x

These are worst cases: the loops do nothing but protected arithmetic, and ordinary app code spends far less of its time in loops like these. A CI check fails the engine if a preset's median grows past 2.5 times its figure here.

A hot numeric kernel, such as an image filter, a physics step or a parser's inner loop, pays close to the full figure. With a Pro key, keep it at light with a region preset and leave the rest of the bundle at your global preset. A Free build ignores a lower region preset, so on Free keep the global preset at light when a hot kernel matters.

Host-global reads

Protected code reads a host global such as URL through a check that throws the same ReferenceError as your original code when that global is missing. What the check costs depends on where the code runs.

Measured on 2026-10-13 with AfterPack 0.2.4, in Node 24 on an arm64 Mac, with a loop that reads URL once per iteration. Each figure is the protected build's run time divided by that of the same protected build reading the global without the check. A range spans two seeds, each the fastest of 5 to 7 interleaved rounds.

PresetOrdinary contextInside node:vm
light1.00x2.7x
medium1.06x to 1.13x2.0x to 2.1x
hard1.04x to 1.05x1.9x to 2.1x
extreme1.10x to 1.18x2.0x to 2.1x

An ordinary context is a browser page, a worker or Node's main context; this column ran in a Node worker thread. There the check costs nothing measurable at light. From medium up, the preset's added protection wraps the check, and that is paid on every read.

A node:vm context sends every lookup on its global object through a callback, so there the same loop runs 1.9x to 2.7x as long as it does without the check. A test or benchmark that runs protected code inside node:vm sees this cost at its upper bound, so measure code that reads host globals in a hot loop where your users run it.

Caching across releases

A fresh seed changes the bytes of every protected chunk, including chunks whose source did not change. The bundler plugins fold the seed into each chunk's file name, so after a release, returning visitors download all protected JavaScript again. A fixed seed keeps an unchanged chunk byte-identical and cached, and that chunk then keeps its shape across releases. On Next.js, Angular, Vite or webpack output the CLI renames content-hashed files the way the plugins do; on other output the names stay, so pin seed or give that output a short cache lifetime. See File names and caching.

Next

  • Presets: the size budget each preset sets, and what raising one costs.
  • Build & CI: where the step belongs in a pipeline.
  • Cloudflare Workers: running the wasm build in an isolate.
  • Playground: the wasm build, running in your browser, timing itself.