Lazy Page Hydration
Who is this for? Developers working on Capell public rendering, cache hits, and runtime selection.
TL;DR: Public rendering should not lazy-load page or site relations while deciding how a page renders. Capell keeps runtime decisions on already-loaded data and provides
LazyLoadedSiteContextas an explicit helper, not as a default kernel step.
When To Use This
Section titled “When To Use This”Use this when a cached page response starts issuing database queries, Livewire assets appear on Blade-only pages, or a rendering strategy change touches page type metadata.
The practical goal is simple:
- Cache hits should be able to return public HTML without hydrating unrelated site data.
- Public views should receive hydrated render data instead of querying models directly.
- Runtime decisions should use page meta first, then only loaded type metadata.
Current Runtime Shape
Section titled “Current Runtime Shape”These pieces are involved in lazy-safe rendering decisions:
RenderingStrategyMiddlewarereads the resolved page after the response has rendered and adds theX-Rendering-Strategyheader.RenderingStrategyViewComposeris registered oncapell::appand adds aruntimeManifestpluslivewireEnabledflag when one has not already been supplied.AbstractPage::pageRecordRequiresLivewire()checks page meta first. It only consults page type meta oris_livewirewhen thetyperelation is already loaded.ResolveFrontendRuntimeActionowns the main Blade, Livewire, and Inertia runtime decision for normal frontend rendering.LazyLoadedSiteContextis an explicit helper for code paths that want a minimalSitewrapper. It is not currently part of the default frontend kernel pipeline.
The default frontend kernel steps are registered in FrontendServiceProvider through config('frontend.kernel.steps'). See Frontend Page And Site Loading for the current step list.
Rendering Strategies
Section titled “Rendering Strategies”RenderingStrategyEnum defines the public page rendering modes:
- BladeOnly (
blade): plain Blade rendering with no Livewire runtime requirement. - BladeWithIslands (
blade-islands): Blade output with isolated Livewire components where needed. - FullLivewire (
livewire): the page itself renders as a Livewire component.
Page-level meta.rendering_strategy is the safest source because it is already on the page record. Type-level strategy and is_livewire are valid only when the page type relation has been loaded by the resolver or page cache.
Loaded Type Rule
Section titled “Loaded Type Rule”Do not write runtime checks that call $page->blueprint unless the relation is already loaded.
Use this pattern:
$blueprint = $page instanceof Model && $page->relationLoaded('blueprint') ? $page->blueprint : null;This protects public rendering from hidden database queries and from strict-model lazy-loading violations in tests.
Public API: LazyLoadedSiteContext
Section titled “Public API: LazyLoadedSiteContext”LazyLoadedSiteContext lives at packages/frontend/src/Support/Cache/LazyLoadedSiteContext.php.
| Method | Returns | Purpose |
|---|---|---|
__construct(Site $minimalSite, Language $language) | - | Creates a wrapper around a minimal site and language. |
site() | Site | Loads the full site through SiteLoader on first access, then returns the loaded site. |
language() | Language | Returns the language passed at construction. |
isFullyLoaded() | bool | Reports whether site() has loaded the full site. |
preloadSite() | void | Explicitly loads the full site. |
Because this helper is explicit, code that adopts it should document where the wrapper is created and add tests proving when hydration does and does not happen.
Detecting Unintended Hydration
Section titled “Detecting Unintended Hydration”Use query counts or strict lazy-loading tests for the code path you changed.
- For rendering strategy checks, assert that an unloaded
typerelation stays unloaded. - For site wrappers, assert
LazyLoadedSiteContext::isFullyLoaded()before and after the operation. - For cache-hit behavior, listen for database queries around the request and prove the expected query budget.
Gotchas
Section titled “Gotchas”- Accessing
$page->blueprintor$site->relationin public rendering code can trigger a query. FullLivewirepages intentionally need Livewire runtime state. Do not treat that as a cache leak by itself.BladeWithIslandsshould load Livewire assets for the island runtime, but it should not require full page Livewire hydration.- Cached page models may already include
type; ad hoc models in tests or package code may not. LazyLoadedSiteContextdoes not cache individual relations. Oncesite()is called, it stores the fully loadedSiteobject.