Opt-in performance optimizations for Eloquent's hot path, built from optimizations declined upstream. Zero core changes, byte-identical output.

Maintainers

Package info

github.com/One-Learning-Community/grease

Documentation

pkg:composer/onelearningcommunity/grease

Transparency log

Statistics

Installs: 155

Dependents: 0

Suggesters: 0

Stars: 12

Open Issues: 0

v0.9.0 2026-06-28 05:38 UTC

This package is not auto-updated.

Last update: 2026-07-13 18:08:58 UTC


README

tests Latest Version Total Downloads License

Opt-in performance across Laravel's hot paths β€” a menu of byte-identical tiers.

Grease is a menu of independent, byte-identical optimizations spanning the whole request lifecycle β€” Eloquent hydration/casting/serialization, the event dispatcher, the Blade compiler, config() reads, validation, the container, the request, and the router. They share one idea: Laravel re-derives the same stable facts on every row, attribute, component, render, request, and query; Grease computes each once and reuses it. Take the tiers whose hot paths you actually run β€” the model trait is the zero-config on-ramp, not the whole package. Zero framework changes; anything that doesn't opt in runs pure vanilla.

πŸ“– Full documentation β†’

Install

composer require onelearningcommunity/grease
use Grease\Concerns\HasGrease;

class User extends Model
{
    use HasGrease;
}

That's the model tier β€” the easiest win and a fine place to stop: no config, no provider, no cache to warm. Its hydration, casting, and serialization now run the greased fast paths, byte-identical to vanilla Eloquent. Prefer inheritance? Extend \Grease\GreasedModel instead.

The rest of Grease lives across the request, not just in Eloquent. Each tier is a separate opt-in β€” take the ones whose hot paths you run. Several are a single (non-auto-discovered) provider:

// bootstrap/providers.php, or the providers array in config/app.php
Grease\Events\GreaseEventServiceProvider::class,         // faster event dispatcher, app-wide
Grease\View\GreaseViewServiceProvider::class,            // faster Blade render (+ grease:view-cache)
Grease\Config\GreaseConfigServiceProvider::class,        // memoized config() reads (+ grease:config-cache)
Grease\Validation\GreaseValidationServiceProvider::class, // memoized validation rule parsing

A few more foundation tiers go deeper into the request lifecycle. They can't be a provider (they're constructed before any provider runs), so each is a one-line swap at the application's own entry point β€” the heaviest opt-in, taken only if you want it:

// bootstrap/app.php β€” greased container (faster dependency resolution)
return Grease\Container\Application::configure(basePath: dirname(__DIR__))/* …->create() */;

// public/index.php β€” greased request (memoized input() / all())
$request = Grease\Http\Request::capture();

// bootstrap/app.php β€” greased router (cached middleware resolve+sort), before return
Grease\Routing\Router::swap($app);

The router additionally has an eager, opcache-interned middleware index β€” register Grease\Routing\GreaseRoutingServiceProvider::class and deploy with php artisan grease:route-cache (a route:cache twin) to make FPM middleware resolution ~free. That same provider also swaps in a greased URL generator (no app-entry edit needed β€” url is resolved lazily): route() replays a cached per-name [segments, params] shape instead of re-running the full assembly pipeline every call, and grease:route-cache pre-seeds that shape index too. Byte-identical or it defers. The view tier has the same: deploy with php artisan grease:view-cache (a view:cache twin) and view resolution becomes an opcache-interned lookup β€” no per-render filesystem stat-walk.

Each cache command also hooks php artisan optimize: with the provider registered, a standard optimize runs the grease cache in its slot (and optimize:clear the matching clear twin), so your deploy needs no grease-specific step.

See The Container, The Request, The Router, The URL Generator, and The View Cache.

What you get

Representative deltas, measured on Linux (reproduce on your own build β€” one command):

  • End-to-end requests (incl. SQL): βˆ’86% list-100-users, βˆ’81% eager-load, βˆ’72% m2m eager-load, βˆ’51% show, βˆ’14% bulk write.
  • Per operation: hydrate βˆ’54%, toArray βˆ’53%, set+dirty βˆ’62%, read βˆ’27%, enum βˆ’44%, date serialization βˆ’87%.
  • Many-to-many pivots (model tier, automatic with the trait): the pivot a belongsToMany hydrates per related row was the one model class the tiers couldn't reach β€” now greased too, βˆ’75% on a pivot-heavy get() (Linux). A custom using() pivot or a morphToMany pivot stays vanilla β€” unaccelerated, byte-identical.
  • Eloquent builder __call (HasGreasedQueries, a separate per-model opt-in β€” not bundled in HasGrease): memoizes the scope/passthru/forward dispatch verdict for forwarded query verbs. Deliberately Γ  la carte: it swaps a custom builder in app-wide for a sub-0.1%-of-a-request gain, so it's for query-construction-heavy paths chasing every cycle, not a default. (wrapTable() identifier quoting is likewise memoized, riding the database-connection tier.)
  • Acyclic serialization (HasGreasedAcyclicSerialization, a separate per-model opt-in β€” not bundled in HasGrease): Eloquent runs a debug_backtrace circular-reference guard on every toArray/getQueueableRelations/touchOwners/push β€” a 100% tax for a <1% (self-referential-tree) problem ordinary eager loading can't even produce. One withoutRecursion() override drops it for a model that promises an acyclic graph; byte-identical for acyclic data (the guard only changes output on a real cycle). ~1,230 ns saved per relation-bearing model β†’ βˆ’36% on a 100-row with() list response (Linux), the most common API shape. Leave it off self-referential tree models (it would recurse forever β€” your one responsibility). The broadest serialization win in Grease.
  • Decimal casts (HasGreasedDecimalCasts, a separate per-model opt-in β€” not bundled in HasGrease, because decimal means money): returns an already-canonical scaled string verbatim instead of round-tripping it through Brick\Math. It never rounds β€” anything off-scale/leading-zero/notation/non-string defers to vanilla, so the worst case is "no speedup", never a wrong number (proven over 1.1M fuzzed cases vs the Brick oracle). The win is driver-gated: MySQL/PostgreSQL return canonical decimal strings (it fires), SQLite returns floats (it defers, byte-identical). On a greased transaction model β€” several decimals, no JSON β€” ~βˆ’3.7% (Linux), ~260 ns per decimal column; nothing on decimal-free models. A narrow tier for decimal-dense financial tables, not a default.
  • Event dispatcher (app-wide): βˆ’53% no-listener dispatch, ~halves a render-dense request's event overhead.
  • Blade (render path, app-wide): βˆ’28.3% simple / βˆ’23.4% rich component renders, βˆ’26.8% a $loop-heavy table, βˆ’20.3% a layout β€” byte-identical HTML.
  • Config (config() reads, app-wide): a memoized read path (βˆ’65% on a repeat-heavy mix), and an opcache-interned flat index via grease:config-cache that cuts ~88% of config-read time β€” a per-request win that scales with how many reads your app makes (real apps make thousands).
  • Foundation tiers (container & request, app-entry opt-in): βˆ’38.8% per container resolve, βˆ’41% per input-heavy request. Layered with everything above, a real mixed page-load (JSON + Blade) request suite stacks to ~βˆ’47% end-to-end for ~+2% retained memory β€” see the cumulative-stack table.
  • Router (middleware resolve+sort, app-entry opt-in): once-per-request work, so small in isolation β€” but pure waste removed on every request. The lazy cache halves it; the eager grease:route-cache index takes it to ~βˆ’96% (FPM β‰ˆ Octane steady-state). Compounds with request volume.
  • URL generator (route() assembly, provider opt-in): βˆ’93% per route() call β€” a cached per-name shape replayed as a concat, skipping the formatParameters/replace/query pipeline (the Symfony compile is already cached, so it was never the cost). A thin slice of a vanilla response, but a compounding one: on an API-Resource payload where the model tiers already shrank everything else, it's ~βˆ’26% of the response (500 rows Γ— ~5 absolute links β†’ βˆ’84.7% full-stack vs vanilla). Byte-identical; domain / optional / scoped / query-string / signed URLs defer to vanilla.
  • View cache (grease:view-cache, provider opt-in): the resolution view:cache throws away. An opcache-interned nameβ†’path index turns each view lookup from a filesystem stat-walk into an array hit (20 views: 0 file_exists calls vs 20), and permanently kills the never-memoized dynamic-view miss β€” even under Octane.

These are :memory:/Linux figures β€” read them as Grease's share of the work, not your p99, and reproduce on your target. The Benchmarks guide has the methodology, the build-to-build variance, and the honest caveats.

Running Octane? The case is stronger still. A persistent worker amortizes the framework boot that dilutes Grease under FPM, so the work it removes becomes a visible share of every request β€” and HasGrease on your models alone is enough to justify it. See Grease & Octane.

Byte-identical, or it's a failing test

That promise is the whole product. Every cast type, edge value, null, and dirty-check is asserted equal to vanilla across PHP 8.2–8.5 and Laravel 12/13; the benchmarks run the same fixtures the parity tests prove identical. Where Grease can't guarantee byte-identity for an exotic case, it defers to vanilla β€” correct, just unaccelerated.

composer test     # the byte-identical contract
composer bench    # phpbench per-op A/B + the SQL suite

Learn more

Requirements

PHP 8.2+, Laravel 12/13.

License

Released under the MIT License β€” Copyright Β© 2026 One Learning Community LTD.

Built with Claude

Grease was built proudly in collaboration with Claude β€” a small proof of what a strong engineering mindset and AI can do together: measure first, keep the parity spine honest, and ship wins that compound.