I could follow JavaScript logic. I could trace what a variable held at any point. What I couldn't see was what the engine was physically doing, how the call stack filled, how the heap mutated and what actually happened when await suspended a function. So I built something to show me.
I've been using Python Tutor for years. It's an incredible tool; it shows the logical flow, variable bindings and scope chains. But it only shows you the bones. I needed to see the muscles and the flesh and how they come together.
Nothing showed me async/await at the instruction level. Not what JavaScript looks like when await suspends a function with the stack frame disappearing, microtasks queuing and execution reconstructing from nothing. So I built Vivix.
The gap that existing tools leave open. Python Tutor works by tracing a reference implementation. It gives you a high-level snapshot of the environment at each step. That is perfect for teaching control flow. But JavaScript in 2026 is not just control flow. It is Promises, async/await, microtasks and closures that outlive their enclosing scope.
When await suspends a function, the engine does not just "pause." It tears down a stack frame, schedules a microtask and eventually reconstructs the execution context from a hidden generator state. No existing free tool lets me watch that happen instruction by instruction.
I needed something that would show me:
- The call stack growing and shrinking frame by frame
- The heap mutating as objects are allocated and referenced
- Async/await suspension and resumption as actual engine steps, not abstract diagrams
Nothing existed for modern JavaScript with async/await support. So I built it.
How Vivix works under the hood
Parsing with Acorn. The first stage is entirely client-side. JavaScript source is fed into Acorn, which produces an AST in the browser. No code ever leaves your machine.
A custom tree-walking interpreter inside a Web Worker. The AST is handed to a custom interpreter that executes it inside a Web Worker. This is the critical architectural decision: execution never touches the main thread. Even if you write an infinite loop, the UI remains responsive because the interpreter runs in its own thread, which is sandboxed.
The main interpreter handles synchronous JavaScript. For async/await, a separate executor recognises common patterns sequential await, Promise.all, Promise.race and generates synthetic execution traces. It does not execute arbitrary async code.
The worker produces a flat, immutable array of step snapshots. Each snapshot is a complete picture of the engine state at a single instruction: call stack frames, heap objects, stdout output, the current AST node and the execution phase, such as enter, evaluate and exit.
Svelte 5 renders frames as a constant-time swap. On the main thread, Vivix uses Svelte 5 runes. The entire visualization is driven by a single $state integer the current step index. When the user scrubs forward or backward, Svelte performs a constant-time array index swap. No diffing, no recomputation of derived state. The snapshot at index n is rendered directly.
Each module computes what changed between snapshots and passes explicit status flags new, changed, push, pop to GSAP actions. GSAP plays a pre-defined animation for each transition: a scale-in for new frames, a border flash for updated values, a dissolve for popped stack frames.
Why visualization matters
I've always learned best by seeing things work, not reading about them. That's what pushed me toward cognitive load research, specifically how people build mental models from partial information. The finding that stuck with me: people don't learn from static facts. They learn from watching the process.
Reading a blog post about how await schedules a microtask gives you a declarative fact. Watching the call stack frame vanish, the microtask queue being enqueued and the frame reappear gives you procedural memory. That is what Vivix is designed to produce. The visualizer is not a diagram. It is a microscope.
A concrete example: why seeing beats reading
Here is a snippet you can paste directly into Vivix's free-form editor:
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
}
}
const counter = makeCounter();
counter();
counter();
Step through this and watch the closure capture count in the heap. The inner function holds a live reference not a copy so each call mutates the same variable.
In Vivix, you watch step by step:
- makeCounter() is called: a new scope is created, count is initialised to 0 on the heap
- The inner function is returned: it captures a reference to count, not its value
- counter() is called the first time: count increments to 1, the heap object updates live
- counter() is called again: the same heap reference is incremented to 2
That is the difference between knowing closures and understanding them. When you see count persisting on the heap across both calls owned by the outer scope, modified through the inner function you stop thinking of closures as a language trick and start thinking of them as living scope references.
The stack
- Parser: Acorn AST, client-side
- Interpreter: Custom tree-walker, Web Worker
- Renderer: Svelte 5 $state
- Animation: GSAP status-driven transitions
- Editor: CodeMirror 6
Known limitations
Vivix is honest about what it cannot do. There is a 500-step cap to prevent infinite loops from hanging the worker. The scope model is flat for let and const inside blocks, block scoping works correctly but the heap visualization simplifies nested block environments for clarity. These are deliberate trade-offs for performance and readability.
Try it
Vivix is free, open-source and runs entirely in your browser. No account. No install.
→ vivix.dev — step through JavaScript execution yourself
→ GitHub — if it was useful, a star helps other developers find it
Top comments (0)