# Frontend Optimizer — Improvement & Growth Plan

> Package: capell-app/frontend-optimizer · Kind: package · Tier: premium · Product group: Capell Foundation · Bundle: foundation · Status: Complete

## 1. Snapshot

Frontend Optimizer overrides Capell's `FrontendAssetManifestRenderer` contract to convert the public `<head>` asset manifest into a layout-scoped _render profile_ (a sha256 of asset list + context + critical-CSS settings), then defers non-critical CSS/JS and inlines generated above-the-fold CSS. The live entry point is real: `vendor/capell-app/frontend/resources/views/components/app/head/index.blade.php` calls `app(FrontendAssetManifestRenderer::class)->render(...)`, the optimizer rebinds that contract as a `singleton` at boot (`src/Providers/FrontendOptimizerServiceProvider.php:69`), and `CapellFrontendAssetManifestRenderer` falls back to the default renderer on any throwable. Critical CSS is generated out-of-band by `GenerateCriticalCssJob` → `GenerateCriticalCssAction` → `PlaywrightCriticalCssGenerator`, which shells out via `symfony/process` to `resources/js/generate-critical-css.mjs` (real Chromium, two viewports). `BuildOptimizedImageMarkupAction` now provides a package-owned responsive image seam for lazy/eager images, width candidates, and AVIF/WebP `<picture>` source candidates after callers provide hydrated media URLs. Surfaces: `admin` + `frontend`. Models/tables: `FrontendRenderProfile` + `FrontendOptimizationRun` (`frontend_render_profiles`, `frontend_optimization_runs`). Deps: `lorisleiva/laravel-actions`, `spatie/laravel-data`, `spatie/laravel-package-tools`, `symfony/process`; JS dep `playwright ^1.59.1`. Marketplace summary now leads with faster first paint and real-render critical CSS. `capell.json` declares the extension card plus four committed Capell screenshot-runner PNGs for profile asset output and critical-CSS output in light and dark modes.

## 2. Improvements (existing functionality)

1. **Done/Shipped: real critical health probes.** `FrontendOptimizerHealthCheck::runDiagnostics()` now asserts the public asset-manifest renderer is bound to the optimizer, render-profile manifest storage is writable, generated critical-CSS storage is writable, Node/Playwright generator prerequisites are present, and automatic generation is not configured to run on the synchronous queue. Evidence: `tests/Unit/Health/FrontendOptimizerHealthCheckTest.php` covers healthy diagnostics plus renderer, Node, script, Playwright dependency, and sync-queue failures. — `src/Health/FrontendOptimizerHealthCheck.php` — M

2. **Done/Shipped: optimization scope selection is wired into the live renderer.** `CapellFrontendAssetManifestRenderer` now resolves the configured `CriticalCssSettings::scope()` through `ResolveOptimizationScopeAction` and shapes the profile signature context accordingly: layout scope uses layout/theme, render-profile scope keeps the full context, and page-url scope adds the current URL. — `src/Actions/ResolveOptimizationScopeAction.php`, `src/Support/CapellFrontendAssetManifestRenderer.php` — M

3. **Remove the dead `debug_query_support` setting or implement it.** `CriticalCssSettings::debugQuerySupportEnabled()` (`src/Support/CriticalCssSettings.php:91`) is defined, exposed as a Filament toggle, seeded in settings migration, and documented — but never read anywhere in `src`. It is pure surface-area noise that implies a debugging capability that does not exist. — `src/Support/CriticalCssSettings.php`, `src/Filament/Settings/FrontendOptimizerSettingsSchema.php`, `database/settings/2026_05_24_000001_add_frontend_optimizer_settings.php` — S

4. **Fix the `PersistRenderProfileAction` `critical_css_path` reset-on-create only.** On `createOrFirst` for an existing profile, the update branch fills `label/manifest/scope/signature` but never touches `critical_css_path` or `status` — correct. But on the _create_ branch the seeded `values` array hardcodes `'critical_css_path' => null`; that is fine for a brand-new row, yet `status` is hardcoded to `Pending` on create while the update branch leaves `status` untouched, meaning a signature-stable profile that previously `Failed` keeps `Failed` and never re-pends. Combined with `shouldDispatchGeneration` only re-dispatching on `Failed`, this is intended — but document the state machine or add a test, because the create/update asymmetry is a latent bug magnet. — `src/Actions/PersistRenderProfileAction.php:17-46` — S

5. **Done/Shipped: critical eligibility now follows manifest hints.** `CapellFrontendAssetManifestRenderer` now reads optional manifest requirement hints such as `criticalEligible`, `critical_eligible`, and `frontendOptimizerCriticalEligible`, plus package-name hints, before falling back to the legacy `foundation-theme:css` compatibility path. Non-Foundation assets can opt into critical CSS without string-literal coupling. Evidence: `tests/Unit/Support/CapellFrontendAssetManifestRendererTest.php`. — `src/Support/CapellFrontendAssetManifestRenderer.php` — M

6. **Done/Shipped: JS loading now follows manifest hints.** The renderer now prefers per-asset optimizer loading-strategy hints and core presentation loading strategies (`Idle`, `Interaction`, `Visible`) before falling back to the legacy `foundation-theme:runtime` idle behavior. This keeps existing Foundation output stable while letting other packages request idle/intersection/lazy behavior through manifest data. Evidence: `tests/Unit/Support/CapellFrontendAssetManifestRendererTest.php`. — `src/Support/CapellFrontendAssetManifestRenderer.php` — M

7. **Done/Shipped: stale profile GC is available.** `PruneRenderProfilesAction` and `capell:frontend-optimizer:prune-profiles` prune render profiles older than a retention window, remove their manifest and generated critical-CSS files from local storage, and rely on the existing FK cascade to delete optimization runs. The command supports dry-run, JSON, retention days, and limit options, and the manifest now declares the console surface and prune command. Evidence: `tests/Unit/Actions/PruneRenderProfilesActionTest.php`. — `src/Actions/PruneRenderProfilesAction.php`, `src/Console/Commands/PruneRenderProfilesCommand.php`, `capell.json` — M

8. **`composer.json` PHP floor (`^8.3`) contradicts repo standard (8.4).** The skill states PHP 8.4 / typed constants; `RenderProfileAssetRenderer` already uses a typed `private const string`. Align the constraint to avoid a package that won't install on the documented baseline mismatch the other way. — `composer.json:7` — S

## 3. Missing Features (gaps)

Manifest `capabilities[]`: `frontend-optimizer`, `frontend-optimizer-frontend`, `frontend-assets`, `cache-blocking`.

- **Asset minification / bundling — absent (table stakes).** The package re-emits the existing manifest URLs verbatim (`assetUrl()` just resolves the Vite/public path). There is no concatenation, no minify, no HTTP/2-aware bundling. "Profile-based CSS and JavaScript delivery" implies optimization the code does not perform beyond load-strategy rewriting. Either build it or stop implying it in copy.
- **Done/Shipped: image optimization seam.** `BuildOptimizedImageMarkupAction` returns safe `<picture>` / `<img>` markup with configurable width candidates, AVIF/WebP source candidates, lazy/eager loading, async decoding, sizes/srcset attributes, and high-priority eager images. It intentionally accepts already-hydrated URLs instead of querying media records from public Blade, and `capell.json` now advertises `frontend-optimizer-images` plus a `media-library` support pairing. — `src/Actions/BuildOptimizedImageMarkupAction.php`, `config/capell-frontend-optimizer.php`, `tests/Unit/Actions/BuildOptimizedImageMarkupActionTest.php`, `tests/Unit/FrontendOptimizerManifestTest.php`
- **Done/Shipped: manifest-driven preload/fetchpriority/font hints.** The optimizer now carries manifest `preloads` into render-profile `resource_hints`, includes them in the profile hash/manifest, and renders escaped `<link>` resource hints before optimized assets. Font preload hints can declare `as=font`, type, and crossorigin; LCP image hints can carry `fetchpriority=high`; modulepreload is preserved. Remaining future depth: automatic preconnect/dns-prefetch origin discovery and font-display/subsetting. — `src/Data/FrontendResourceHintData.php`, `src/Support/CapellFrontendAssetManifestRenderer.php`, `src/Support/RenderProfileAssetRenderer.php`
- **Font optimization — partial.** Font preload is now supported through manifest resource hints, but no `font-display: swap` enforcement or subsetting exists yet. Critical CSS extracts `@font-face` rules but does not infer underlying font files automatically.
- **Lazy-loading of below-the-fold content — only partial.** `AssetSlot::BelowFold` exists in the enum but is never assigned (grep confirms it appears only as a fallback comparison). No DOM-level lazy-load (iframes, embeds, images). The `WidgetAssetRegistry` condition mechanism is the natural home for this but is dead (see §4).
- **Core Web Vitals / RUM reporting — absent.** No CLS guard beyond the `body{margin:0}` reset, no LCP/INP measurement, no field-data loop to prove the optimizer helps. A "before/after" CWV panel would be the headline selling screenshot.
- **Per-page / per-route profile scope — declared but unreachable.** `OptimizationScope::PageUrl` and `RenderProfile` exist but the live path forces `Layout`. Page-level critical CSS is the differentiator over layout-level (hero-heavy landing pages differ from article pages sharing a layout).
- **JS `defer`/`async` is coarse.** JS strategy is a 3-way (`Blocking`/`Idle`/`Deferred`→module-defer). No per-package async, no module/nomodule split, no inline-small-script threshold.

Differentiator vs table-stakes: minify/bundle, font/image optimization, and preload hints are _table stakes_ a buyer assumes are present given the name. The genuine differentiator already half-built is **real-browser, layout-scoped critical CSS via Playwright against final rendered output** (`docs/critical-css.md`) — that is rare and good; lead with it and finish the table-stakes around it.

## 4. Issues / Risks

- **Two dead public registries advertised as primary surfaces.** `LayoutAssetRegistry` and `WidgetAssetRegistry` are bound as singletons (`src/Providers/FrontendOptimizerServiceProvider.php:51-52`) and headlined in `docs/assets-and-render-profiles.md` as the top two "Main Surfaces", with full registration examples. Grep across the whole monorepo (`packages/`) shows **no caller anywhere** outside their own definition, the provider binding, and tests — and the live renderer (`CapellFrontendAssetManifestRenderer`) never consults either registry; it builds the asset set directly from the manifest. `WidgetAssetRegistry::resolve()` is never invoked even though the frontend `FrontendAssetContextData` carries `widgetResourceUsages` that would feed it. This is shipped dead code presented as the headline API. — `src/Support/LayoutAssetRegistry.php`, `src/Support/WidgetAssetRegistry.php`
- **Done/Shipped: critical health diagnostics are real.** Diagnostics now reports concrete renderer binding, storage, generator, and queue-driver readiness instead of a version-only pass. Evidence: `tests/Unit/Health/FrontendOptimizerHealthCheckTest.php`. — `src/Health/FrontendOptimizerHealthCheck.php`
- **`@frontendOptimizerAssets` Blade directive has no consumer.** The directive is registered (`...ServiceProvider.php:56`) and documented, but no theme/layout Blade in the monorepo calls it — the production path goes through the manifest renderer override instead. It takes a raw `$profileHash` string and `echo`s `RenderProfileAssetsAction::run(...)`; if a theme author ever passes attacker-influenced input it is a profile-hash lookup only (low risk) but the unused directive is confusing dual-surface debt.
- **Public-output safety: mostly good, one gap.** `RenderProfileAssetRenderer` escapes hrefs/srcs with `htmlspecialchars`, neutralises `</style` in inline CSS, and JSON-encodes the idle-loader src with hex flags — anonymous output reveals no model IDs/paths/labels. **However** the renderer reads `$profile->signature['assets']` (a stored array) and emits `path` values directly; those paths originate from manifest registration so are trusted today, but there is **no test proving anonymous/non-admin rendering safety end-to-end through the real `head` blade** — the skill mandates such a test for any rendering/cache change. Test gap, not a confirmed leak. — `src/Support/RenderProfileAssetRenderer.php`
- **Interaction with `html-cache` / `cacheable:false` contradiction.** Resolved for generation completion: `capell.json` now declares `FrontendRenderProfile` updates as an invalidation source, the provider registers the model with the package-neutral frontend cache invalidation registry, and `GenerateCriticalCssAction` invalidates after successful CSS generation. This intentionally avoids importing `capell-app/html-cache` internals. Remaining risk: invalidation is broad because render profiles do not yet record exact cached URL coverage. — `capell.json:63-74`, `src/Actions/GenerateCriticalCssAction.php`, `src/Actions/InvalidateGeneratedCriticalCssCacheAction.php`
- **Done/Shipped: manifest writes moved off the normal render path.** The live `render()` path still creates or updates the render profile record for new signatures, but `PrepareRenderProfileAction` now writes the JSON manifest only after asynchronous critical-CSS generation is eligible and a generation claim succeeds. Sync-queue public renders and already-queued profiles avoid local disk writes, and `PersistRenderProfileAction` preserves an existing manifest path when called without a new manifest. Remaining budget risk: the first profile render still performs DB work, so the `frontendRenderBudgetMs:20` claim should be treated as a cold-profile target rather than a zero-I/O guarantee. `adminQueryBudget:0` is also implausible given the configurator/settings surfaces. — `src/Actions/PrepareRenderProfileAction.php`, `src/Actions/PersistRenderProfileAction.php`, `src/Actions/StoreRenderProfileManifestAction.php`, `capell.json:59-62`
- **`createOrFirst` race under concurrent first-render.** Two simultaneous requests for a new layout can both miss, both attempt `createOrFirst`; the unique index on `hash` saves correctness but one request eats a duplicate-key round trip. Acceptable, and the duplicate request now avoids rewriting the manifest unless it wins the generation claim. — `src/Actions/PersistRenderProfileAction.php`, `src/Actions/StoreRenderProfileManifestAction.php`
- **Critical-CSS sanitiser is fragile regex over CSS.** `sanitizeInlineCriticalCss()` strips `@property` and rewrites `margin-trim`/`container-type` rules with brittle escaped-class regexes tied to Tailwind v4 output (`src/Support/RenderProfileAssetRenderer.php:185-200`). This will silently break or pass-through on a theme that does not emit exactly that markup, and is undocumented as Tailwind-specific. Tech debt + correctness risk on non-Foundation themes.
- **i18n.** Settings/configurator labels are translated (`capell-frontend-optimizer::settings.*`), good. But `ExtensionManagementSurfaceData::settings(label: 'capell-frontend-optimizer::settings.critical_css')` and the `heroicon-o-bolt` are fine; no hardcoded user-facing strings spotted. `profileLabel()` concatenates raw layout/theme keys — internal only, never shown to anonymous users. OK.
- **Done/Shipped: test gap reconciliation.** The previously listed gaps are now covered: `CaptureCriticalCssPageTypeOptOut` has hydrated/no-query and lazy-load listener coverage, `FrontendOptimizerHealthCheck` has real diagnostics coverage, `GenerateCriticalCssJob` covers generated short-circuit and `WithoutOverlapping`, `CriticalCssSettings` covers persisted/config fallback and viewport parsing, and `PublicHeadOutputSafetyTest` covers anonymous/non-admin head rendering. Remaining useful depth is command-level coverage for newer console surfaces and broader browser-generation failure modes, not the original listener/job/settings/health gap. — `tests/Unit/Listeners/CaptureCriticalCssPageTypeOptOutTest.php`, `tests/Unit/Actions/GenerateCriticalCssActionTest.php`, `tests/Unit/Support/CriticalCssSettingsTest.php`, `tests/Unit/Health/FrontendOptimizerHealthCheckTest.php`, `tests/Feature/PublicHeadOutputSafetyTest.php`

## 5. Marketplace & Selling

**Current copy critique.** Manifest `summary` and composer `description` are near-identical and weak: _"Profile-based CSS and JavaScript delivery for public Capell pages."_ "Profile-based delivery" is internal jargon (it describes the _mechanism_, a hash bucket), names no benefit, and undersells the one genuinely strong feature — real-browser critical CSS. A buyer scanning the marketplace cannot tell this improves page speed / Core Web Vitals. The word "Optimizer" promises minify/images/bundling that the code does not do (§3), creating an expectation gap that will drive refunds/support.

**Improved 1-sentence summary:**

> Faster first paint for public Capell pages: automatic above-the-fold critical CSS extracted from your real rendered pages, plus smart deferring of non-critical CSS and JavaScript.

**Improved 3–4 sentence description:**

> Frontend Optimizer speeds up your public Capell site by inlining the exact above-the-fold CSS each layout needs and deferring everything else, so visitors see styled content sooner and your Core Web Vitals improve. Unlike source-scanning tools, it generates critical CSS from your pages as a real browser renders them — Layout Builder content, theme markup, render hooks, and responsive styles all included — across mobile and desktop viewports. Generation runs on your queue, never in the public response, and falls back safely to standard stylesheet delivery until critical CSS exists. Per-layout render profiles mean equivalent pages reuse one optimized result, and page types you don't want to optimize can opt out with a single toggle.

**Screenshot / media gaps.** The required Capell screenshot-runner captures are committed and listed in `docs/screenshots.json`: profile asset output and critical-CSS output, each with a dark-mode variant. Marketplace promotion still keeps only the extension card because the product-specific captures need recapture before sales use. Remaining media upside: add a "before/after Lighthouse / CWV" comparison (the strongest sales asset) and surface the settings panel screenshot when the admin workflow is buyer-ready.

**Pricing / tier / bundle.** Positioned `premium` / `foundation` bundle — correct: performance is a cross-cutting concern every site wants, so bundling with the foundation drives adoption and it is a natural upgrade-justifier for the premium tier. Keep it foundation-bundled but ensure the _image/font_ gaps (§3) are closed before leaning on "Optimizer" as a premium differentiator, or competitors' all-in-one perf packages will look more complete.

**Cross-sell (deps + Extension Suites).**

- **html-cache** — natural pair: critical CSS makes the _first_ (cacheable) paint fast; html-cache makes _repeat_ paints instant. Sell as "Speed Suite". Must fix the generation→cache-invalidation gap (§4) first so the story holds.
- **media-library** — the image-optimization seam now accepts hydrated media URLs and renders responsive/AVIF/WebP candidates; bundle "Optimizer + Media Library" to own LCP end-to-end.
- **seo-suite** — Core Web Vitals are a ranking factor; co-position "Optimizer + SEO Suite" as "rank faster, load faster".
- **foundation-theme** — already the only theme with critical-eligibility wired (§2.5); make that explicit ("optimized out of the box with Foundation").

**Differentiators / value props / target buyer.** Differentiator: real-browser, layout-scoped, queue-driven critical CSS with safe fallback and per-page-type opt-out. Value props: faster LCP/FCP, no theme rewrite, zero risk to public output (always falls back). Target buyer: agency/site owner running content/marketing sites on Capell who cares about Lighthouse scores and SEO but won't hand-tune asset loading.

**Keywords/tags (8–12):** critical-css, core-web-vitals, page-speed, lcp, render-blocking, css-optimization, lazy-loading, defer-javascript, above-the-fold, performance, frontend-performance, lighthouse.

## 6. Prioritized Roadmap

| Item                                                                                                                                                          | Bucket | Effort | Impact | Section ref    |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ------ | -------------- |
| Implement real health-check probes (it's `critical`-severity but a stub)                                                                                      | Done   | M      | High   | §2.1, §4       |
| Add anonymous/non-admin public-output safety integration test through real head render                                                                        | Done   | M      | High   | §4 (test gaps) |
| Wire critical-CSS generation completion → html-cache invalidation source                                                                                      | Done   | M      | High   | §4             |
| Recapture optimizer-specific profile asset and critical CSS screenshots before promoting marketplace media; generic Demo/consent-banner captures were demoted | Future | S      | Med    | §5             |
| Remove or wire dead registries (`LayoutAssetRegistry`/`WidgetAssetRegistry`) + fix docs headline                                                              | Done   | S      | Med    | §4             |
| Rewrite marketplace summary + composer description (benefit-led)                                                                                              | Done   | S      | High   | §5             |
| Delete/implement dead `debug_query_support` setting                                                                                                           | Done   | S      | Med    | §2.3           |
| Delete or wire orphaned `ResolveOptimizationScopeAction`; reconcile unreachable scopes                                                                        | Done   | S      | Med    | §2.2, §3       |
| Validate/relax `frontendRenderBudgetMs:20` claim; move manifest disk-write off the hot render path                                                            | Done   | M      | High   | §4             |
| Drive critical-eligibility + JS-idle from manifest hints, not hardcoded `foundation-theme:*`                                                                  | Done   | M      | High   | §2.5, §2.6     |
| Shipped 2026-06-08: add image optimization seam (responsive/lazy/eager/AVIF-WebP candidates) paired with media-library URLs                                   | Done   | M      | High   | §3             |
| Add preload/preconnect/fetchpriority + font optimization hints                                                                                                | Done   | M      | High   | §3             |
| Add profile/critical-CSS GC (prune on layout/theme delete + stale signatures)                                                                                 | Done   | M      | Med    | §2.7           |
| Cover listener, job, settings, health in tests                                                                                                                | Done   | M      | Med    | §4 (test gaps) |
| Implement page-level (`PageUrl`) scope for hero-heavy landing pages                                                                                           | Later  | L      | Med    | §3             |
| Add CWV/RUM before-after reporting panel (headline sales asset)                                                                                               | Later  | L      | High   | §3, §5         |
| Add CSS/JS minify + bundling to match "Optimizer" naming expectation                                                                                          | Later  | L      | Med    | §3, §5         |
| Document Tailwind-v4-specific critical-CSS sanitiser coupling; harden for other themes                                                                        | Later  | M      | Med    | §4             |