Virtualization-based obfuscation for JavaScript.
js-virtualizer transpiles selected JavaScript functions into custom bytecode and runs them inside a JavaScript VM. It is built for targeted function protection rather than whole-program virtualization.
Install dependencies:
bun installRun the test suite:
bun run testUseful demo scripts:
bun run demo:fingerprint- generates a Node.js fingerprint demo and prints previews of the obfuscated VM and virtualized outputbun run demo:fingerprint:browser- generates a browser demo atoutput/browser-fingerprint/index.html
Warning
You need to mark the functions you want to virtualize by putting a comment with the text // @virtualize above the function.
// @virtualize
function virtualize() {
console.log("hello from the virtualized function");
}
function notVirtualized() {
console.log("this function will not be virtualized");
}Tip
See examples/basic.js for a full example and the samples folder for some sample code you can try virtualizing.
const {transpile} = require("js-virtualizer");
async function main() {
const result = await transpile(`
// @virtualize
function virtualize() {
console.log("hello world from the JSVM");
}
virtualize()
`, {
fileName: 'example.js',
writeOutput: true,
vmOutputPath: "./vm_output.js",
transpiledOutputPath: "./output.js",
passes: [
"RemoveUnused",
"ObfuscateVM",
"ObfuscateTranspiled"
],
vmObfuscationTarget: "node",
transpiledObfuscationTarget: "node"
});
console.log(`Virtualized code saved to: ${result.transpiledOutputPath}`);
}
main();fileName(string, default:[randomly generated]) - the filename of the code; will be used as the default output filename where the transpiled code & the VM will be written towriteOutput(bool, defaulttrue) - whether or not the transpiler should directly write the output to a filevmOutputPath(string, default:output/[name].vm.js) - the path to write the VM for the transpiled code totranspiledOutputPath(string, default:output/[name].virtualized.js) - the path to write the transpiled code tovmObfuscationTarget(string, defaultnode) - javascript-obfuscator target for VM output whenObfuscateVMis enabled; use"browser"for browser-compatible outputtranspiledObfuscationTarget(string, defaultnode) - javascript-obfuscator target for transpiled output whenObfuscateTranspiledis enabled
All protection features default to true and are enabled simultaneously in the hardened default profile.
deadCodeInjection(bool, defaulttrue) - append unreachable decoy bytecode instruction sequences to the protected payloadmemoryProtection(bool, defaulttrue) - enable encrypted register storage with guard tokens and per-step rotation; this is the primary performance bottleneck in the hardened profilecontrolFlowFlattening(bool, defaulttrue) - replace direct jumps with a state-machine dispatch loop (CFF_DISPATCHopcode), making control flow opaque to static analysisopaquePredicates(bool, defaulttrue) - insert always-true/false predicate blocks into the bytecode stream to confuse disassemblersselfModifyingBytecode(bool, defaulttrue) - scramble executed bytecode bytes after each instruction; backward jumps transparently restore the original bytesantiDump(bool, defaulttrue) - overwrite executed bytecode with a deterministic mask after execution, preventing memory dumps of the full payloadantiDebug(enabled in wrappers by default) - arm timing-gap and DevTools heuristics that perturb dispatcher state and optionally triggerdebuggertraps when tampering is detectedtimeLock(bool, defaulttrue) - hashcash-style proof-of-work challenge solved at VM startup before execution beginsdispatchObfuscation(bool, defaulttrue) - multi-phase dispatch loop (fetch, decode, pre-exec, execute, post, dummy) with interleaved dummy phasesjunkInStream(bool, defaulttrue) - insert junk instructions between real instructions in the bytecode streamwhiteboxEncryption(bool, defaulttrue) - generate white-box T-tables for an additional bytecode encryption layerpolymorphic(bool, defaulttrue) - randomize register scramble maps and endianness (BE/LE) per function based on the integrity key
These features are opt-in (default false) and provide additional obfuscation layers for high-value targets.
advancedCFF(bool, defaultfalse) - extends control-flow flattening with indirect jump table dispatch (CFF_JUMP_TABLE) and computed goto patterns (CFF_COMPUTED_GOTO). Switch-like patterns in the opcode stream are detected and replaced with jump tables using affine-transformed case values, making case-to-target mapping opaque to static analysismemoryLayoutObfuscation(bool, defaultfalse) - injects runtime memory protection opcodes into the bytecode:- Stack canaries (
MEM_CANARY) - deterministic canary values placed between basic blocks that trigger a fail handler on corruption, detecting stack smashing or memory tampering - Register bank rotation (
REG_ROTATE) - periodic seeded permutation of register banks at runtime, preventing static memory dump analysis since register values are never in predictable locations - Memory region shuffle (
MEM_SHUFFLE) - runtime permutation of specified register regions - Fake stack frames - decoy register save/restore sequences with opaque predicates injected between real operations to confuse memory analysis
- Stack canaries (
Runtime bytecode generation and execution capabilities available through the dynamicLoader utility module.
- DYN_LOAD - decrypt and load bytecode from a register into an internal buffer using seeded position-dependent XOR
- DYN_EXEC - execute loaded bytecode in a forked VM context (supports both sync and async execution)
- DYN_PATCH - hot-patch running bytecode at a given offset, enabling runtime self-modification scenarios
randomizeVMProfiles(bool, defaulttrue) - synthesize a hardened randomized register-VM profile per function; biased toward larger register files, denser decoys, and stronger dispatcher/alias strategiesvmProfile(object, defaultnull) - explicit VM profile override. Available sub-fields:registerCount(number, 48–256) - number of VM registersdispatcherVariant("permuted"|"clustered"|"striped") - dispatch table layout strategyaliasBaseCount(number, 1–4) - base number of alias slots per opcodealiasJitter(number, 0–3) - random alias count variationdecoyCount(number, 0–64) - number of decoy handler slotsdecoyStride(number, 1–8) - spacing between decoy entriesruntimeOpcodeDerivation("hybrid"|"stateful"|"position") - how alias indices are selected at runtimepolyEndian("BE"|"LE") - endianness for DWORD encoding
nestedVM(bool, defaultfalse) - enables two-layer virtualization; critical opcode handlers (ADD, FUNC_CALL, CFF_DISPATCH) are re-virtualized inside a lightweight 16-opcode inner VM with encrypted bytecode, shuffled opcode IDs, and per-function key derivation. Works with CFF, opaque predicates, dead code injection, and browser targets. Note:nestedVM+codeInterleavingis not yet supported.
codeInterleaving(bool, defaultfalse) - merge multiple// @virtualizefunctions into a single unified bytecode blob executed by one shared VM instance. The dispatch loop uses a selector register to switch between interleaved function bodies.
environmentLock(object, defaultnull) - restrict execution to a specific environment. Example:{type: 'hostname', expected: 'example.com'}— the VM verifies the runtime hostname before executing.
decoratorsMode("legacy"|"standard", default"legacy") - Babel decorator plugin mode;"standard"uses the2023-11proposal syntax
passes(array, default:["RemoveUnused", "ObfuscateVM", "ObfuscateTranspiled"]) - passes applied to the result before returning:RemoveUnused- strip unused opcodes from the instruction setObfuscateVM- obfuscate the VM code through javascript-obfuscatorObfuscateTranspiled- obfuscate the transpiled code through javascript-obfuscator
Generated virtualized wrappers protect embedded bytecode with a per-function integrity envelope. If the protected payload is modified, the VM throws before decompression and execution.
| Area | Feature | Status | Notes |
|---|---|---|---|
| Variables | let / const scoping |
✅ | block scoping works |
| Variables | function-scoped var |
✅ | covered by regression tests |
| Variables | primitive literals | ✅ | strings, numbers, booleans, null, undefined |
| Variables | object expressions | ✅ | |
| Variables | array expressions | ✅ | |
| Variables | object destructuring | ✅ | |
| Variables | array destructuring | ✅ | |
| Variables | assignments | ✅ | includes compound assignment paths used by tests |
| Functions | arrow functions | ✅ | |
| Functions | function expressions | ✅ | |
| Functions | function declarations | ✅ | |
| Functions | generators / async generators | ✅ | supported through Babel preprocessing before Acorn parsing; direct functions and generator class methods are covered by regression tests |
| Functions | external/internal calls | ✅ | preserves this for method-style calls |
| Functions | callbacks | ✅ | |
| Functions | this inside virtualized functions |
✅ | top-level this and VM callbacks supported |
| Runtime | browser execution | ✅ | browser-aware src/vm_dist.js runs in browser-like runtimes without a compatibility wrapper; compressed payloads use globalThis.pako.inflate |
| Runtime | randomized register VM profiles | ✅ | wrappers embed hardened per-function VM profiles by default, biased toward larger register files plus stronger dispatcher/alias derivation strategies; explicit vmProfile overrides are supported |
| Async | await |
✅ | |
| Async | stored promises | ✅ | |
| Async | Promise.all(...) style concurrency |
✅ | child VM contexts prevent register clobbering |
| Async | full async surface | ✅ | async callbacks, nested helpers, and awaited try / catch / finally are covered by regression tests |
| Memory model | captured references in nested functions/protos | ✅ | escaped closures and prototype methods share captured state through nested VM contexts |
| Statements | return |
✅ | |
| Statements | if / else if / else |
✅ | |
| Statements | for |
✅ | |
| Statements | for...of |
✅ | |
| Statements | for...in |
✅ | |
| Statements | while |
✅ | |
| Statements | switch |
✅ | |
| Statements | try / catch / finally |
✅ | |
| Statements | throw |
✅ | |
| Statements | continue / break |
✅ | |
| Expressions | sequence expressions | ✅ | |
| Expressions | template literals | ✅ | |
| Expressions | ternaries | ✅ | |
| Expressions | logical operators (&&, ` |
, ??`) |
|
| Expressions | new |
✅ | |
| Expressions | unary operators | ✅ | includes typeof, void, delete |
| Expressions | binary operators | ✅ | |
| Expressions | update operators | ✅ | |
| Expressions | comparison operators | ✅ | both strict (===) and loose (==) equality |
| Expressions | bitwise operators | ✅ | &, ` |
| Expressions | spread (...) |
✅ | spread into arrays and objects |
| Classes | class declarations | ✅ | implemented through desugaring |
| Classes | class expressions | ✅ | |
| Classes | getters / setters | ✅ | public and private |
| Classes | instance fields | ✅ | public and private |
| Classes | static fields | ✅ | public and private |
| Classes | private methods | ✅ | instance and static |
| Classes | static blocks | ✅ | public and private static member access covered |
| Classes | private brand checks (#x in obj) |
✅ | |
| Classes | computed class keys | ✅ | fields, methods, accessors, and computed super[...] calls |
| Classes | async methods | ✅ | public, private, static, and inherited cases covered |
| Classes | decorators | ✅ | supported through Babel preprocessing before Acorn parsing in both legacy and standard (2023-11) modes |
| Classes | inheritance | ✅ | |
| Classes | super() and super.method() |
✅ | constructor, instance, static method, and field initializer cases |
| Obfuscation | bytecode integrity checks | ✅ | protected bytecode envelopes detect payload tampering before decompression/execution |
| Obfuscation | argument scrambling | ✅ | virtualized wrappers and internal VM callbacks load arguments through randomized aliases/order mappings |
| Obfuscation | string encryption | ✅ | bytecode string payloads are encrypted before embedding and decoded inside the VM at load time |
| Obfuscation | dead code injection | ✅ | transpiled bytecode gets unreachable decoy instruction tails by default |
| Obfuscation | junk instruction insertion | ✅ | junk instructions are inserted between real instructions in the bytecode stream |
| Obfuscation | opaque predicates | ✅ | always-true/false predicate blocks confuse disassemblers and static analysis |
| Obfuscation | VM memory protection | ✅ | generated wrappers enable protected register storage with on-read restoration |
| Obfuscation | register rotation (stack-lane encoding) | ✅ | protected register wrappers rotate on protected reads/writes and after each VM step to avoid stable stored values |
| Obfuscation | dispatcher-level indirect dispatch | ✅ | VM instances resolve decoded opcodes through a shuffled dispatch table instead of direct handler lookup |
| Obfuscation | dispatcher variants | ✅ | permuted, clustered, and striped dispatch table layouts; randomized per function |
| Obfuscation | decoy opcode handlers | ✅ | the shuffled dispatcher includes fake never-called handler slots in addition to the real opcode aliases |
| Obfuscation | runtime opcode derivation | ✅ | decoded opcodes resolve through runtime-selected alias slots driven by evolving dispatcher state (hybrid, stateful, or position modes) |
| Obfuscation | macro opcodes / superinstructions | ✅ | common traces such as paired literal loads and test+jump sequences are fused into synthesized macro-opcodes |
| Obfuscation | whole-bytecode encryption with externalized runtime key | ✅ | virtualized wrappers embed only a key id; the actual bytecode decryption keys are registered in the generated VM runtime |
| Obfuscation | white-box T-table encryption | ✅ | additional white-box cipher layer with generated T-table lookup tables |
| Obfuscation | stateful / position-dependent opcodes | ✅ | opcode bytes are encoded by byte position and decoded at runtime using a per-function seed derived from the integrity key |
| Obfuscation | per-instruction bytecode encoding | ✅ | protected instruction payload bytes are decoded just-in-time during VM execution using a per-function seed |
| Obfuscation | jump target encoding | ✅ | control-flow offsets are encoded inside protected payloads and decoded only by the VM at execution time |
| Obfuscation | control-flow flattening | ✅ | direct jumps replaced with a state-machine dispatch loop, making control flow opaque to static analysis |
| Obfuscation | self-modifying bytecode | ✅ | executed bytecode bytes are scrambled after each instruction; backward jumps transparently restore original bytes |
| Obfuscation | anti-dump | ✅ | executed bytecode is overwritten with a deterministic mask, preventing memory dumps of the full payload |
| Obfuscation | polymorphic endianness and register scramble | ✅ | random BE/LE endianness and register index scrambling per function based on integrity key |
| Obfuscation | dedicated VM anti-debug layer | ✅ | VM instances can arm timing-gap and DevTools heuristics that perturb dispatcher state and optionally trigger debugger traps |
| Obfuscation | dispatch loop obfuscation | ✅ | multi-phase dispatch (fetch, decode, pre-exec, execute, post, dummy) with interleaved dummy phases |
| Obfuscation | time-lock / proof-of-work | ✅ | hashcash-style PoW challenge solved at VM startup before execution begins |
| Obfuscation | code interleaving | ✅ | multiple virtualized functions merged into a single unified bytecode blob with shared VM instance |
| Obfuscation | environment lock | ✅ | restrict execution to specific hostnames or environments |
| Obfuscation | bytecode compression | ✅ | bytecode payloads are zlib/pako compressed before embedding and decompressed at load time |
| Obfuscation | advanced CFF (jump tables) | ✅ | indirect jump table dispatch with affine-scrambled case values and computed goto patterns; opt-in via advancedCFF: true |
| Obfuscation | memory layout obfuscation | ✅ | stack canaries, register bank rotation, memory region shuffling, and fake stack frames; opt-in via memoryLayoutObfuscation: true |
| Obfuscation | dynamic code loading | ✅ | runtime bytecode decryption (DYN_LOAD), execution (DYN_EXEC), and hot-patching (DYN_PATCH) with seeded XOR encryption |
| Nested VM | two-layer virtualization | ✅ | critical handlers (ADD, FUNC_CALL, CFF_DISPATCH) are re-virtualized inside a 16-opcode inner VM with encrypted bytecode and shuffled opcode IDs |
| Nested VM | inner opcode shuffle | ✅ | inner VM opcode IDs are permuted per-function, preventing static analysis of the inner instruction set |
| Nested VM | CFF dispatch through inner VM | ✅ | the control-flow flattening state machine is itself virtualized, hiding dispatch logic from reverse engineering |
| Nested VM | browser support | ✅ | nested VM works in browser environments (Node.js and browser targets) |
| Runtime | automatic top-level initializer virtualization | ✅ | safe top-level variable initializers are auto-wrapped into helper VMs without requiring // @virtualize markers |
js-virtualizer adds measurable overhead. The table below comes from a synthetic hot-loop benchmark (compute(50000), 10 calls per run, 3 runs) to give a worst-case picture.
| Mode | Avg per call | Slowdown vs original |
|---|---|---|
| Original JS | 0.16 ms | 1x |
| Light VM | 34 ms | ~213x |
| Hardened VM (default) | 234 ms | ~1475x |
| Hardened VM + nested VM | 264 ms | ~1660x |
Hardened VM, memoryProtection: false |
113 ms | ~714x |
Hardened VM, memoryProtection: false + nested VM |
148 ms | ~929x |
memoryProtection uses array-backed storage with dirty-bit tracking — only registers that were written to since the last step are re-protected, instead of all 253 registers every step. Nested VM adds only ~13% overhead on top of the hardened profile since memoryProtection overhead is now minimal.
Note
These numbers are a worst case. A tight compute loop is the scenario most hostile to any VM. For functions that do I/O, DOM work, or infrequent business logic the relative slowdown is much smaller. Benchmark on real project code before deciding which profile to use.
Warning
It is highly recommended that you modify and obfuscate the vm_dist.js file before using it in a production environment. For instance, including the opcode names in the VM makes it more trivial to reverse engineer the workings of the virtualized code
- performance is not guaranteed. js-virtualizer is not intended for high-performance paths or whole-program virtualization; it is better suited to protecting selected functions where slowdown is acceptable
- the distributed VM is still realistically reversible if shipped as-is. Obfuscating or hardening the VM runtime is still recommended for production use
- anti-analysis layers are still heuristic rather than bulletproof. Integrity checks, keyed payload encryption, jump-target encoding, indirect/derived dispatch, macro-op fusion, dead code, anti-debug heuristics, and protected register storage raise the bar, but they do not make the VM equivalent to a commercial protector
- automatic top-level initializer virtualization is intentionally conservative. Safe initializer shapes are virtualized automatically, while complex runtime-heavy initializers stay as plain JavaScript to avoid semantic or temp-register regressions
- syntax outside the support matrix, especially proposal-era or otherwise untested constructs, may still fail even when nearby standardized syntax works