Benchmarks
Phasis is roughly two orders of magnitude slower than V8 on dispatch-bound JavaScript. That ceiling is the inherent cost of a PHP-hosted interpreter vs a native JIT compiler. The question for an embedder isn't "how does it compare to V8" — it's "is it fast enough for what I'm doing." For most embedding workloads the answer is yes.
When the ceiling doesn't matter
- Templating — SSR-style render of a typical page in well under 10 ms.
- Validation rules — user-supplied JS for form validation finishes in microseconds.
- Data transformations —
array.map(transform)over hundreds of items completes in tens of milliseconds. - Sandbox runners — short user-supplied scripts with a 1-second host cap leave plenty of headroom.
When the ceiling matters
- Compute-heavy inner loops — image processing, large-array reductions, simulations. Move the compute to a native PHP library and expose it as a host function.
- Real-time animation — anything driving 60 fps. That JS belongs in the client (browser, native app), not the embedded engine.
- JS reimplementations of compute-expensive algorithms — cryptography, compression, parsing. PHP has native equivalents that are 1000× faster; use those.
The right mental model: Phasis is a glue layer for JS-shaped logic embedded in PHP-shaped applications. The PHP host handles compute; the JS layer handles configuration, branching, and small transformations.
What makes the engine fast (relative to a naïve tree-walker)
- Bytecode VM — most hot dispatch runs through
VM::execute's opcode switch, not the AST. - Custom callstack — every JS call keeps state inside
VM::executeand saves the caller's frame onto a pool; no new PHP frame per JS call. See Bytecode VM § Custom callstack. - Inline caches — a per-PC monomorphic cache makes prototype-method-on-instance reads (the
c.method()shape) skip the prototype-chain walk. - Frame-snapshot generators — simple generator bodies suspend by capturing the dispatch state into a heap object instead of crossing a PHP Fiber.
- JsToPhp transpiler — numeric / locals-heavy bodies (including
obj.method(),new Ctor(), andthis.memberreads) compile to PHP closures that PHP 8.5's tracing JIT lowers to native machine code. - JSON.stringify cache —
JSON.stringify(o)caches its output on the input object with transitive version invalidation, accelerating the deep-clone idiomJSON.parse(JSON.stringify(o)).
Measuring your own workload
php bench/run.phpThis runs the microbench suite shipped under bench/microbench.js — a handful of synthetic workloads (loop arithmetic, recursive functions, object creation, prototype-method calls, array push, JSON round-trip, etc.) and prints median wall time per test. Useful as a sanity check; current numbers are committed to BENCH.md on every CI bench run.
For your own code, swap in any script:
php -d xdebug.mode=off bench/run.php path/to/your-script.jsFor PHP-level profiling, Xdebug's profiler works against Phasis exactly like any other PHP application. Run a script under xdebug.mode=profile and inspect the cachegrind output to see which Phasis\ classes dominate.
Roadmap
What would close more of the V8 gap, in rough order of tractability:
- Polymorphic inline caches — let a single
LOAD_MEMBERcall site hold 2-4 cached shapes before falling back to slow dispatch. Helps code that processes heterogeneous receivers. yield*and try-with-yield on the frame-snapshot generator path — these shapes currently route to the Fiber-backed tree-walker. Each needs explicit state-machine handling (delegate-iterator forwarding, handler stack preserved across suspend) before they're snapshot-safe.- Async generators and
awaiton snapshots — the spec composes generator suspension with the microtask queue. Once the bytecode body supports bothyieldandawait, async generators become snapshot-friendly too. - Parameter type inference in JsToPhp — function parameters default to numeric type, so
function (obj) { obj.method() }bails the compiler. Inferring object-typed parameters from how they're used in the body would unlock method calls on function arguments without requiring the receiver to be locally constructed vianew. - Direct opcache emission — skip JsToPhp's
eval()step by writing opcache-format opcodes directly. Deepest item; the others can be done within the existing architecture.
None of these are necessary for the embedding workloads Phasis targets.