A few weeks ago, I posted a carousel on LinkedIn about TypePHP, the ahead-of-time compiler the Swoole team open sourced. It turns PHP source into a native executable that starts on its own, with no PHP CLI and no separate interpreter process involved.
The response surprised me. Almost nobody wanted to argue about the benchmarks. What people kept asking, in one form or another, was: isn't this just JIT? Doesn't opcache already do this? Where does this fit with what I'm already running?
Thnking about it, I realized my own mental model had gone fuzzy. I knew opcache was important and I knew JIT existed, but I couldn't have clearly explained the difference to someone who asked. So I went back and worked it out.
This is that refresher, written up for anyone else who wants it.
The short version
If you've got two minutes, here's the whole thing.
PHP has to do two separate jobs before your code produces a result.
1) First it has to understand your code, which means reading the text and turning it into instructions.
2) Then it has to execute those instructions. The four approaches each attack a different part of that.
Raw PHP does both jobs on every single request. It reads your files, compiles them, runs them, and throws all the work away. Then the next request comes in and it does the whole thing again.
Opcache fixes the first job. It saves the compiled instructions in memory so PHP stops re-reading your source on every request. This is the single biggest performance win available to a PHP app and it costs you nothing but memory. If you take one thing from this article, take this one.
JIT attacks the second job, with limited success. While your code is running, it watches for hot spots and converts them to native machine code on the fly. It produces impressive numbers on math-heavy code and almost nothing on a normal web request.
AOT attacks the second job by changing the rules. It compiles your PHP all the way to a native binary before you ever deploy it. It goes far faster than JIT can, but only because it gives up part of the PHP language to get there.
The first three are about making your existing app faster. AOT is more about redefining what you can build with PHP at all... and that's the part I find really interesting.
First, what PHP is doing with your code
When a request hits your app, PHP reads your .php files as plain text. It parses them, checks the syntax, and compiles them into something called opcodes. Opcodes are small, simple instructions, roughly the PHP equivalent of assembly. Something like $a + $b becomes an ADD opcode with two operands.
Then the Zend VM takes over. The VM is a loop. It grabs the next opcode, figures out what it means, does it, and moves to the next one. Over and over until your script finishes.
There are two specific things you need to keep in mind.
The first is that PHP variables aren't raw values. They're zvals, which are little containers that hold both the value and a tag saying what type it is. When the VM adds two variables, it has to check the tags first, because $a + $b means something different for two integers than for two strings. That type check happens at runtime, every time, because PHP doesn't know the types ahead of time.
The second is that all of this work gets discarded when the request ends. PHP's process model throws everything away and starts clean. That's actually a feature. It's why PHP is so hard to leak memory in and so forgiving of sloppy code, but it also means any work you do during a request has to be redone on the next one. And the next one. And the next one.
These strategies offer different answers to the question: "which part of that can we stop repeating?"
1. Raw PHP - goldfish memory
This is PHP with nothing turned on. Every request: read the files, parse them, compile them, run the opcodes, throw it all away.
Nobody should be running this on purpose in 2026, but it's the baseline and it's worth understanding why it's so bad.
The problem isn't your code. The problem is your framework. A modern Drupal or Laravel install loads hundreds or thousands of PHP files to serve one page. Every one of those files has to be read off disk, tokenized, parsed, and compiled before a single line of your actual application logic runs. On a typical framework request, that setup work can eat more time than the work you actually care about.
You'll still run into this sometimes. A misconfigured container image, a dev environment nobody set up properly, a hosting provider cutting corners. If an app feels inexplicably slow and the database looks fine, checking whether opcache is actually enabled is a good first move.
2. Opcache - caching is your friend
Opcache fixes the obvious waste. The first time a file is requested, PHP compiles it as usual, but then opcache stores the resulting opcodes in shared memory. Every request after that skips straight to execution.
Your source files stop being read. The parser stops running. All that setup work happens once instead of thousands of times a day.
On a framework-heavy app this commonly gets you several times the throughput, which is an enormous return for a config flag. It's built into PHP, it's on by default in most modern builds, and the only real cost is a chunk of memory.
But notice what opcache does not do. The Zend VM is still there, still walking through opcodes one at a time, still checking zval types on every operation. Opcache removed the cost of understanding your code. It did nothing about the cost of running it.
however, a tight loop that does math a million times gets essentially nothing out of opcache. Opcache removed the cost of compiling that loop, which happens once. The time is going into the VM executing it a million times, and opcache never touches that part.
That gap is what the next two approaches go after.
3. JIT - good on paper, but...
JIT stands for just-in-time compilation, and it arrived in PHP 8.0. It builds directly on top of opcache — it's not a separate thing you run instead, it's a layer that sits on top.
The idea is that while your code is running, PHP watches which parts run most often. When something crosses a threshold, PHP compiles that section into real native machine code and runs the machine code instead of interpreting opcodes. The compilation happens during execution, which is where "just in time" comes from.
On paper this should be huge. In practice, for most web apps, it's roughly nothing. There are a few key reasons why.
PHP's dynamic types get in the way.
The JIT wants to compile $a + $b into a single machine instruction. To do that it needs to know that $a and $b are both integers. But PHP can't promise that because anything could have been assigned to those variables. So the JIT emits the fast machine code plus a guard check that verifies the types are what it assumed. If the guard fails, it bails back to the interpreter. Those guards cost time, and they're everywhere.
It has to preserve all of PHP's behavior.
References, magic methods, error handlers, the ability to redefine things at runtime. The JIT can't optimize away anything that might be observable, and in PHP an awful lot is observable.
The work doesn't survive.
The compiled machine code lives in one worker process. When that process recycles, it's gone, and the next one has to warm up from scratch.
Most web requests aren't CPU-bound anyway.
A typical page load spends its time waiting on MySQL, waiting on Redis, waiting on an API. Making the PHP execution faster doesn't help when the PHP was already sitting around waiting.
Where JIT genuinely does earn its keep is code that's actually doing arithmetic in a loop: image manipulation, numeric simulation, machine learning inference, statistical work, anything that grinds on numbers without touching the network.
If that's your workload, turn it on and measure. If it isn't, JIT is a config option you can safely leave alone.
4. AOT - now this is interesting
AOT stands for ahead-of-time. Instead of compiling while your program runs, it compiles before you deploy at all. That makes sense.
TypePHP takes your PHP source, translates it into C++17, and hands that to gcc or clang. What comes out the other end is a native binary. No interpreter, no opcodes, no VM. Just machine code, the same as if you'd written the thing in C.
And here's the part that matters: the reason it goes so much faster than JIT isn't that compiling beats interpreting. It's that AOT gets to know the types.
What you gain
By requiring you to write in a typed subset of PHP, the compiler can turn a PHP int into an actual machine integer instead of a zval. No box, no tag, no runtime type check, no guard, no bailout path. Once the types are real, it can hand the whole program to a C++ optimizer that's had thirty years of work poured into it... inlining, loop unrolling, constant folding, vectorization, all of it, with full visibility into your code.
That's where the impressive numbers come from. The project reports around 69x on a hundred-million-iteration pi calculation and roughly 135x on a recursive Fibonacci. Broader language benchmarks are more modest, at about 8x on bench.php and 6.5x on micro_bench.php. Not bad, that's for sure.
What you lose
The catch is exactly symmetrical to JIT's. JIT keeps the entire PHP language and accepts a performance ceiling. AOT breaks through the ceiling by giving up part of the language. TypePHP compiles a defined subset and publishes the incompatibility list openly, which I respect. Global scope is declaration-only. Binary mode wants a main() with a specific signature. Some dynamic reference and reflection patterns simply don't compile.
So no, you can't compile Drupal core. But that was never really the point.
Check the fine print
Binary mode produces an executable that starts directly, with no PHP CLI and no separate interpreter process. That's real and it's useful. But the executable still links libphp and PHPX, and those have to ship in your deployment package.
So, this isn't a statically linked Go binary you can scp anywhere and run. It's closer to any other compiled program with shared library dependencies where you ship the program and the libraries it needs.
That's an important detail that I missed in the initial reporting, and not really "no PHP required." It's also the kind of thing worth knowing before you promise it to anyone.
You don't have to compile all of it
Here's the thing that isn't get talked about enough. Binary mode gets the attention, but TypePHP also has -m ext, which outputs a .so (or .dll) that PHP loads as a normal extension.
That inverts the whole question. You're not compiling your application. You're compiling a piece of it, and the rest of your code base stays ordinary interpreted PHP that calls into it like it would any other extension.
Think about where that lands in a large app. A router. A cache backend. A template compiler. A serializer. A pricing or rules engine that runs on every request. In most mature code bases there's a small percentage of the code responsible for a large percentage of the CPU time, and it tends to be the stable, well-tested, rarely-touched part. That's exactly the profile you want for something you compile.
PHP extensions have always been able to do this. The catch was that writing one meant writing C, which put it out of reach for basically every team that wasn't already maintaining an extension. Now the thing you write is PHP!
So the realistic question isn't "can we compile Drupal." It's "which two percent of our code is hot enough to be worth compiling, and can we carve it out cleanly?" That's a much smaller, much more answerable question.
Opcache gets you a large, reliable win. JIT on top of it gets you approximately nothing on a normal web app, but sometimes it can help.
The AOT bar has no length because there isn't an honest one to draw. Nobody can compile a whole framework app today, so "AOT throughput on a standard Laravel install" is not a number that exists. What you can say is that on whatever portion you do compile, you'd see somewhere between 6.5x and 135x depending on what that code is doing.
Which is the real reason AOT sits awkwardly in a performance comparison. It isn't competing with opcache and JIT. It's answering a different question.
So which one do you actually use?
Opcache is not optional.
Turn it on, give it enough memory to hold your whole codebase, and verify it's actually running in production. This is the highest-return thing on the list by a wide margin. If you do nothing else after reading this, go check.
JIT is a targeted tool, not a general upgrade.
Enable it if your PHP is doing real computation, and measure before and after. Don't turn it on across the board expecting your web app to get faster, because it won't, and you'll have spent memory for nothing. The default for most applications is to leave it off.
AOT is a different kind of decision entirely. Don't evaluate it as "should we make our app faster." Evaluate it as "is PHP execution genuinely our bottleneck, or do we need to ship something PHP couldn't ship before?" If it's the first, you're looking at extension mode and a profiler, not a rewrite.
That second part is probably the most useful in the near term. A native executable that starts on its own, with no PHP CLI to install and no vendor directory to ship, is a distribution capability that no amount of opcache or JIT tuning will ever give you. Neither will linking against a Rust library, or handing a customer something they can run without installing PHP first, or putting PHP code somewhere a PHP runtime was never going to fit.
Super cool, but doesn't change things that much ... yet
Anyone running PHP apps probably doesn't need anything else right now. For a typical request that's waiting on a database, none of the four helps much beyond opcache. If your p99 is dominated by queries, the answer is indexes and caching, not compilers. AOT matters when PHP execution itself is the bottleneck, or when the thing you want to ship isn't a website at all.
Wrapping up
Four approaches, and they're not really competing with each other.
Raw PHP re-does all the work on every request. It's the baseline, and if you find yourself here it's a misconfiguration rather than a choice.
Opcache stops PHP re-reading and re-compiling your source. Several times the throughput for a config flag and some memory. Always on, no exceptions, this is where the biggest and cheapest win lives.
JIT compiles hot code to machine code while your program runs, but PHP's dynamic types force it to hedge with guard checks, and the work vanishes when the worker recycles. Real gains on numeric code, near-zero on ordinary web requests. Reach for it deliberately and measure, because turning it on by default costs memory and buys most apps nothing.
AOT compiles everything up front and gets to eliminate the type checks entirely, which is where the 69x and 135x figures come from. The price is a restricted language subset, a build step, and a compatibility audit.
You don't have to swallow it whole, though. Extension mode lets you compile one hot subsystem and leave everything else exactly as it is. Worth it when execution is genuinely your bottleneck, or when you need to ship something in a shape PHP could not produce before.
The first three make your existing app cheaper to run. The fourth changes what you can point a PHP team at.
That's why I don't think AOT belongs in the same conversation as opcache and JIT, even though it keeps landing there. Opcache and JIT are answers to "how fast is our website." AOT is an answer to "what can we build." Those are different questions.
Whether TypePHP's supported subset grows fast enough to matter is genuinely open. But languages that find a way out of their original niche tend to stick around a lot longer than the ones that don't.


Top comments (0)