Lazy Page Hydration
Who is this for? Developers working on cached page rendering who want to understand why certain database queries fire (or don’t) on cache hits, and how to prevent unintended eager-loading from defeating cache savings.
TL;DR: Lazy page hydration defers loading of site relations until they’re actually accessed, letting cached HTML responses skip database queries entirely.
When to use this
Section titled “When to use this”When a cached page response is served, you don’t need to load all site relations (menus, themes, metadata, etc.) — the HTML is already rendered. But if a Blade fragment or component accidentally accesses a relation, it forces a full site load on every cache miss (and potentially on every hit). Lazy hydration solves this by wrapping the site in a proxy that only loads relations on first access, not on middleware entry.
The problem it solves:
- Full-page caching saves rendering time, but only if you don’t load the database during page renders.
- Without lazy hydration, eager-loading all site relations happens before the view layer, defeating cache savings.
The solution:
- Lazy proxy: On cache hits, the site is passed to Blade as a minimal object that defers relation loading.
- Detect hydration: Use the
isFullyLoaded()method or inspect query logs to know if a fragment forced full hydration. - Preload when needed: Call
preloadSite()in the view composer if you know a relation will be accessed.
How it’s wired
Section titled “How it’s wired”Three components work together:
-
RenderingStrategyMiddleware(packages/frontend/src/Http/Middleware/RenderingStrategyMiddleware.php) — reads the page’srendering_strategymeta field and sets anX-Rendering-Strategyresponse header. The middleware doesn’t short-circuit responses or inject context; it just reports which strategy the page uses. -
LazyLoadedSiteContext(packages/frontend/src/Support/Cache/LazyLoadedSiteContext.php) — a proxy around theSitemodel that keeps a minimal (unloaded) site and defers full hydration untilsite()is called. Other code instantiates this and passes it into the view layer. -
RenderingStrategyViewComposer(packages/frontend/src/Http/View/RenderingStrategyViewComposer.php) — registered on thecapell::appview; it reads the rendering strategy and passes alivewireEnabledboolean to the view. If the page uses Livewire, it must request it during view composition to avoid hydrating the site too early.
The glue:
- The middleware is registered as
frontend.rendering_strategy(seepackages/frontend/src/Providers/FrontendServiceProvider.phpline ~505). - The view composer is registered in
FrontendServiceProvider::registerViewComposers()(~549). LazyLoadedSiteContextis instantiated elsewhere (likely in the frontend kernel or context builder) and passed through view data.
Strategies
Section titled “Strategies”The RenderingStrategyEnum defines three page rendering modes. The middleware reports which one the page uses; the view composer uses the same enum to decide whether to boot Livewire:
-
BladeOnly (
'blade') — plain Blade rendering, no Livewire. Fully compatible with lazy hydration; no JavaScript overhead. Use for static pages, blog posts, marketing content. -
BladeWithIslands (
'blade-islands') — Blade with isolated Livewire components (“islands”) for specific interactive features. Islands are lazy-loaded Livewire components (form-builder, filters, toggles) that don’t require full page hydration. More efficient than full-page Livewire. -
FullLivewire (
'livewire') — entire page is a Livewire component. Requires full site hydration and boots Livewire on every request. Use only for complex interactive pages (dashboards, real-time content) where the entire page needs reactivity.
The middleware sets the header; the view composer uses the strategy to decide whether to pass livewireEnabled => true to the view.
Public API (LazyLoadedSiteContext)
Section titled “Public API (LazyLoadedSiteContext)”| Method | Returns | Purpose |
|---|---|---|
__construct(Site $minimalSite, Language $language) | — | Create a lazy proxy with an unloaded site. |
site(): Site | Site | Return the fully-loaded site if hydration has occurred, or the minimal site passed at construction if hydration hasn’t happened (or failed silently). Subsequent calls return the cached instance. |
language(): Language | Language | Return the language passed at construction (no hydration). |
isFullyLoaded(): bool | bool | Check if the site has been fully loaded (true if site() has been called, false otherwise). |
preloadSite(): void | void | Explicitly trigger site hydration by calling site(). Use in the view composer if you know a relation will be accessed. |
Detecting unintended hydration
Section titled “Detecting unintended hydration”To know if your Blade fragment or component is forcing hydration on cache hits:
Option 1: Check the lazy proxy state
LazyLoadedSiteContext is instantiated internally during request handling and passed through view data. You typically don’t construct it yourself in application code. To inspect the state during local development, create a middleware that checks if the context in view data has been hydrated:
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Capell\Frontend\Support\Cache\LazyLoadedSiteContext;use Closure;use Illuminate\Http\Request;
final class DebugHydrationMiddleware{ public function handle(Request $request, Closure $next) { $response = $next($request);
// Check during response if the context was hydrated // (requires passing context through view data and reading it back) if ($context = $request->attributes->get('lazy_context')) { if ($context instanceof LazyLoadedSiteContext && $context->isFullyLoaded()) { error_log('WARNING: Site was hydrated on this request.'); } }
return $response; }}Option 2: Monitor database queries
In a service provider, enable query logging and watch for site-relation queries on requests that should be cache hits:
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Database\Events\QueryExecuted;use Illuminate\Support\Facades\DB;use Illuminate\Support\ServiceProvider;
final class DebugQueryProvider extends ServiceProvider{ public function boot(): void { DB::listen(function (QueryExecuted $query): void { // Log queries that load site relations if (str_contains($query->sql, 'site') || str_contains($query->sql, 'language')) { error_log("HYDRATION QUERY: {$query->sql}"); } }); }}Option 3: Use Laravel Telescope (production-safe)
Install Telescope and inspect the Database panel to see all queries executed. Filter by request; on cache-hit requests, you should see zero site-relation queries if lazy hydration is working.
Gotchas
Section titled “Gotchas”-
Accessing
$site->relationin a cached fragment forces hydration on every miss. If your fragment cache is invalidated or misses, and the fragment accesses a site relation, it triggers a full site load. Move the relation access outside the fragment cache scope, or preload the site in the view composer. -
site()is not idempotent in terms of performance. The first call hydrates; subsequent calls return the cached instance. But code that doesn’t expect hydration will be surprised if a deeply nested component callssite(). -
Lazy context only defers relation loading, not basic site attributes. Properties like
id,slug, andnameon the minimal site are always available. Only relations (menus, themes, settings, etc.) trigger hydration. -
FullLivewire pages bypass lazy hydration. If a page uses
FullLivewirestrategy, full site hydration happens before the view is composed, so the lazy proxy provides no benefit (the site is already fully loaded). Use lazy hydration only withBladeOnlyorBladeWithIslands. -
The lazy proxy doesn’t cache loaded relations separately. Once hydrated, the entire site object is loaded into memory. Fragment caching doesn’t gain anything from lazy hydration if the site is already loaded. Use both strategies together: lazy hydration for cache hits, fragment caching for expensive Blade partials that compute output.
Related
Section titled “Related”- Fragment caching — caching Blade partials inside the view layer.
- Cache invalidation — strategies for invalidating caches across your application.
- ETag and conditional responses — HTTP caching headers for client-side deduplication.