Phasis
Advanced

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 transformationsarray.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::execute and 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(), and this.member reads) compile to PHP closures that PHP 8.5's tracing JIT lowers to native machine code.
  • JSON.stringify cacheJSON.stringify(o) caches its output on the input object with transitive version invalidation, accelerating the deep-clone idiom JSON.parse(JSON.stringify(o)).

Measuring your own workload

php bench/run.php

This 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.js

For 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_MEMBER call 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 await on snapshots — the spec composes generator suspension with the microtask queue. Once the bytecode body supports both yield and await, 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 via new.
  • 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.

On this page