Laravel · Php

PHP 8 JIT Compiler — A Field Guide

PHP 8's JIT compiler explained: tracing vs function JIT, why it barely moves the needle for I/O-bound web requests, and where it actually pays off in CPU-bound numeric code.

John Kihiu12 min read

PHP 8.0 shipped with a JIT compiler and, in the years since, I've seen the same pattern play out on almost every project that turned it on expecting a free win: nothing measurable happens. That's not a bug or a disappointing implementation — it's a direct consequence of what a typical Laravel request actually spends its time doing, which is not the kind of work a JIT accelerates. Understanding why is more useful than the opcache.jit config flags themselves.

Tracing JIT vs function JIT

PHP's JIT operates in one of two modes. Function JIT compiles an entire function to machine code the first time it's called, on the bet that the whole function is worth compiling. Tracing JIT — the default and the one almost everyone means when they say "PHP JIT" — instead watches opcache execute the bytecode, identifies hot loops and hot paths through actual execution, and compiles only those traced paths to machine code, with guards that fall back to the interpreter if execution takes an untraced branch. Tracing JIT wins on typical numeric hot loops because it specializes for the types and branches actually taken, not every possible path through the function.

INI · OPCACHE.JIT CONFIG
; php.ini — JIT requires opcache to be enabled first
opcache.enable=1
opcache.enable_cli=1
opcache.jit_buffer_size=100M

; opcache.jit is a 4-digit code: CRTO
;   C = CPU-specific optimization level (0-5)
;   R = register allocation (0=none, 1=local, 2=global)
;   T = trigger (0=disabled, 1=always, 4=hot function+loop tracing)
;   O = optimization level (0-5)
opcache.jit=1254   ; the commonly recommended tracing JIT default

Setting opcache.jit_buffer_size to 0 disables JIT entirely regardless of the opcache.jit value — that's the actual on/off switch, and it's a common source of "I set opcache.jit but nothing changed" confusion.

Why a typical web request barely moves

A Laravel request spends its time doing things the JIT has no leverage over: querying a database over a network socket, waiting on Redis, serializing a response, running framework bootstrap code that executes each opcode once per request rather than in a hot loop. Tracing JIT needs a loop or a function to run enough times, within the same request or worker process, to be identified as hot and get compiled — and PHP-FPM's typical shared-nothing, process-per-request model recycles or restarts workers, so the JIT's compiled traces often don't get the chance to pay off before the process cycles. Opcache itself — caching parsed bytecode so PHP doesn't re-parse and re-compile source on every request — already captured the biggest, cheapest performance win for web workloads years before JIT existed; JIT is compiling that already-cached bytecode further, into native machine code, which only matters if the bytecode itself is the bottleneck.

Benchmarks showing 3x wins are usually numeric microbenchmarks

The demo numbers that circulated when JIT shipped — Mandelbrot generation, ray tracing, tight numeric loops — are real, but they're not representative of a Laravel app's request path. If you benchmark JIT against your own app and see single-digit percent or no change, that's the expected outcome, not a misconfiguration.

Where JIT actually helps

The wins show up in CPU-bound code with tight loops over primitive types: image processing, encryption/hashing-heavy code, math-heavy data transformation, or long-running scripts (a CLI import job, a queue worker processing thousands of records with the same code path repeatedly) where the same function executes enough times within one process for tracing to kick in and the compiled trace to be reused. If your bottleneck profile shows CPU time in PHP userland code rather than time waiting on I/O, JIT is worth measuring. If a profiler shows most of your request time in database queries or external HTTP calls, JIT changes nothing about that.

Measure before you touch config

Enable JIT, benchmark the actual endpoints or jobs you care about with something like ab, wrk, or a proper load test, and compare against the same test with JIT off. Don't trust a synthetic benchmark from someone else's workload to tell you what will happen to yours.

Wrapping up

The JIT compiler is a real feature with real wins, just not for the workload most PHP web apps run. Opcache already solved the expensive part of PHP's request lifecycle — re-parsing source on every request — years before JIT arrived, which leaves JIT to compile already-fast bytecode into native code for workloads that are actually CPU-bound in tight loops. Turn it on if you're running numeric or CPU-heavy PHP, benchmark it if you're curious about a typical Laravel app, and don't expect it to fix a request path that's slow because of a database query.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.