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 ran | Input | Time | Throughput |
|---|---|---|---|
| The npm package, on the 185 KB bundle | 185 KB | 229 ms | 0.79 MB/s |
| The engine on its own, same bundle | 185 KB | 243 ms | 0.74 MB/s |
| The npm package, on a 766 KB multi-file build | 766 KB | 1,362 ms | 0.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 preset | Node 24 |
|---|---|
110 KB bundle, light | 147 ms |
185 KB bundle, light | 237 ms |
185 KB bundle, medium | 1,011 ms |
185 KB bundle, extreme | 2,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:
| Preset | Gzipped output multiplier |
|---|---|
minify | 1.09x to 1.17x |
light | 1.29x to 1.60x |
medium | 6.04x to 8.33x |
hard | 6.62x to 9.23x |
extreme | 8.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.
| App | Without AfterPack | light | medium | hard | extreme |
|---|---|---|---|---|---|
| Vite and React | 35 ms | 52 ms | 253 ms | 295 ms | 596 ms |
| Vue on Vite | 31 ms | 36 ms | 176 ms | 117 ms | 199 ms |
| Astro islands | 44 ms | 48 ms | 205 ms | 249 ms | 622 ms |
| SvelteKit | 27 ms | 32 ms | 84 ms | 89 ms | 162 ms |
| Next.js, webpack | 42 ms | 74 ms | 462 ms | 573 ms | 1,010 ms |
| Next.js, webpack, CPU throttled 4x | 145 ms | 234 ms | 1,652 ms | 2,215 ms | 3,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.
| Preset | Run time against the original |
|---|---|
minify | 1.0x |
light | 2.1x |
medium | 32.5x |
hard | 35.0x |
extreme | 37.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.
| Preset | Ordinary context | Inside node:vm |
|---|---|---|
light | 1.00x | 2.7x |
medium | 1.06x to 1.13x | 2.0x to 2.1x |
hard | 1.04x to 1.05x | 1.9x to 2.1x |
extreme | 1.10x to 1.18x | 2.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.