Obfuscation presets

The five protection levels from minify to extreme, the complexity target each one sets, and what the zero-config default (light) already guarantees.

A preset is a named set of transforms with a known size and runtime cost. A heavier preset applies more transformation; it is not a measured step up in protection. One name sets the complexity target; output size follows from that target. Setting complexity explicitly overrides the target, and a value set directly always applies to every token, even on light.

The five obfuscation presets ranked by complexity target, with light marked as the defaultextremetarget 25hardtarget 12mediumtarget 7lighttarget 5DEFAULTminifytarget 0Zero config gives you light: string literals encoded. Step up per project.

The ladder

PresetcomplexityTypical gzipped sizeString floorReach for it when
minify01.09x to 1.17xoffNo obfuscation. Identifiers are renamed and the code is compressed; string literals stay readable.
light51.29x to 1.60xonDefault. Adds string encoding to minify and rewrites syntax, with no structural layers. The right preset for a whole app bundle.
medium76.04x to 8.33xonCode you want meaningfully harder to follow. Across a whole app bundle it multiplies startup time (load cost), so measure your pages first.
hard126.62x to 9.23xonCode that matters: pricing, gating, license checks, client-side business logic. Aim it at that code with a directive; across a whole bundle it costs more than medium.
extreme258.25x to 11.19xonThe highest applied complexity. Aim it at a file or region rather than a whole bundle.

Gzipped output measured against the same file already minified, on five real minified production bundles, with AfterPack 0.2.6 on 2026-10-16; Performance has the method.

The string floor is on for any positive target.

Load cost

Measured with AfterPack 1.0.0 on 2026-10-24, on five small apps built with Vite and React, Vue, Astro, SvelteKit and Next.js. Each app ran with its whole bundle at one preset and is compared with the same app built without AfterPack; Performance has the method and the time for each app.

PresetJavaScript the page loads, uncompressedTime to first working click
light1.12x to 1.42x+4 ms to +32 ms
medium10.2x to 11.9x3.1x to 11x
hard14.3x to 15.8x3.2x to 13.7x
extreme27.1x to 30.7x5.9x to 24x

Size grows by about the same factor on every framework. Startup time grows with how much of that code runs before the page responds, so the Next.js app paid the most. The framework's own runtime is part of the bundle, and every preset protects it too.

On a whole app bundle, light adds tens of milliseconds at most. From medium up, measure your own pages before you ship the preset across the whole bundle.

The default is light

Run npx afterpack@latest dist/ with no --preset, or install a plugin and configure nothing, and you get light.

$ npx afterpack@latest dist/
npx afterpack@latest dist/ --preset=hard    # step up

Flags join their value with =. A space-separated value is rejected as an unknown flag.

light is the zero-config floor. Every string literal it can rewrite goes through a runtime decoder. It renames identifiers and rewrites syntax, and it adds no structural layers. From engine 0.2.3, loops are no longer rewritten into callback helpers at minify and light, which removes the 2-5x slowdown those helpers caused in hot loops. At light, property and global names still go through encoded constants, which has some cost in very hot code. Some strings stay readable at every preset: directive prologues, typeof comparands, property names the runtime needs, and export names. The Protection Map lists them, and the engine reports the count and byte total on every build. Raising the preset to medium or above adds structural work on every token: inflation, control-flow reshaping, and strings and integers keyed to a shared runtime value. It does not change string readability, which is already at its floor.

minify is the one rung that turns the floor off. From engine 0.2.3, it only renames identifiers and compresses the code, with no obfuscation applied. Engine 0.2.2 also rewrote syntax at minify.

Size budget

Every output carries the engine's fixed runtime overhead — the decoders and the integer-expression interpreter — so a file of a few hundred bytes comes out proportionally larger than the ranges above suggest. Read a ratio on a real bundle, not a small file.

inflation.max is unset by default, so the complexity target alone decides the output size. Set it yourself when you have a real size budget you would rather fail than exceed. Hitting an explicit value is an error: DIAG_SIZE_CAP_REACHED, no output, exit 3 from the CLI, and a failed build from a plugin. From engine 0.2.3, under-protected output is never written in its place: the finished file is checked against the limit, and string encoding always completes first. In engine 0.2.2, a tight limit could stop string encoding early and leave some strings readable.

Obfuscated bytes, and so the gzipped size, change from build to build by design. By default every build draws a new seed, and seed: "git" draws a new one on every commit, so each release ships a different program. A fixed seed reproduces the bytes only for the same input on the same engine version. Set a bundle-size budget on the code as your bundler wrote it, before AfterPack, or give a budget on the protected output a margin.

Setting the target directly

complexity is an unbounded non-negative number. --complexity=200 is a valid Free-tier override, well past what extreme sets.

npx afterpack@latest dist/ --preset=hard --complexity=40

That is hard with its complexity target overridden to 40; nothing else about the preset changes. Passing a preset name to --complexity is an error.

Applying a preset to one region

On Free, one preset covers the whole build: --preset on the CLI, or preset in your config. The only narrower control is paths.exclude, which leaves whole files out and copies them to the output unprotected. Every other file pays the preset's load cost, so keep a whole app bundle at light unless your own measurements allow more.

On Pro, a source directive puts a heavier preset on one function or file, and the rest of the bundle keeps your global preset:

/* @afterpack preset=extreme */
function pricingEngine(plan, region) {
  return resolve(plan, region);
}
/* @afterpack end */

To cover a file, wrap its code in one directive. Directives are block comments only, read from your source by a framework plugin; the CLI reads none. See Directives for the grammar and the tier rules.

Next