--- name: frontend-performance description: performance standards for Vue/Nuxt frontends — Core Web Vitals targets, per-route rendering choice, images (@nuxt/image or plain img), lazy loading, fonts, bundle budgets, reactivity cost, third-party scripts, INP. Load when building pages, adding images/fonts/scripts/dependencies, or reviewing or debugging slow UI. --- # Frontend performance ## Targets Google "good" thresholds, measured at the **75th percentile** of real users (mobile and desktop separately): - LCP ≤ 2.5 s - INP ≤ 200 ms - CLS ≤ 0.1 Verify with Lighthouse CI (lab, per PR) and WebPageTest (lab, real devices/networks). Lab scores are a proxy; where real-user data exists (CrUX, RUM), it decides. ## Rendering mode Not standardised; it varies per project. Choose **per route** with `routeRules` (see `nuxt-project-patterns`) and **record the choice** (and why) in the project's docs or an ADR. Do not default a whole app to one mode without checking each route's data freshness and SEO needs. ## Images The LCP element is usually an image. Check which approach the project uses before adding images. **With `@nuxt/image`** (`NuxtImg` / `NuxtPicture`): - Always set `width` and `height` (or an aspect-ratio via CSS) so space is reserved; this prevents CLS. - LCP image: `loading="eager"` and `fetchpriority="high"`; never lazy-load it. Everything below the fold uses `loading="lazy"`. - Set `sizes` for responsive images; use `NuxtPicture` for modern formats (AVIF/WebP) with fallback. - Provider and format options depend on the module version and configured provider; check the installed version's docs. **With plain ``:** - Explicit `width`/`height` attributes (or `aspect-ratio`) on every image. - Serve modern formats (AVIF/WebP) via ``, and `srcset` + `sizes` for responsive widths. - Same `loading`/`fetchpriority` rules for the LCP image vs the rest. Alt text rules: `frontend-a11y-check`. ## Lazy loading - Routes are code-split by default; do not import page-level components eagerly elsewhere. - Prefix a component with `Lazy` in templates (``) to defer its chunk. Pair with `v-if` so it loads only when needed (modal opened, tab selected); a `Lazy` component rendered unconditionally still loads at start. - Outside Nuxt auto-imports use `defineAsyncComponent(() => import('./HeavyChart.vue'))`. - Never lazy-load above-the-fold content or the LCP element. ## Fonts - Self-host; preload only the one or two files used above the fold (``). `crossorigin` is required even for same-origin fonts or the preload is wasted. - `font-display: swap` (or `optional` when layout stability matters more than the custom face). Use a metrics-matched fallback (`size-adjust`, `ascent-override`) to limit CLS from swap. - Subset, and prefer WOFF2 variable fonts over many static weights. `@nuxt/fonts` can automate this if the project uses it. ## Bundle budgets and analysis - Set a budget per project, record it, and enforce it in CI (Lighthouse CI `resource-summary` budgets or a bundle-size check) rather than reviewing sizes by eye. - Inspect with the project's analyze script (e.g. `pnpm nuxt analyze`) when adding a dependency or a budget fails. - Review new dependencies for size and tree-shaking (ESM, named imports; no whole-library imports like `import _ from 'lodash'`). Dependency review rules: `frontend-security`. ## Reactivity cost - Large read-mostly objects/arrays (API payloads, chart data): `shallowRef`, and replace the value rather than mutating deep. - Class instances and third-party objects (map, chart, editor instances): `markRaw`. - `v-once` / `v-memo`: only after profiling shows a hotspot; they hide updates and are easy to get wrong. - Long lists (hundreds of rows+): virtualize or paginate. Do not render everything and hide with CSS. - Do not copy server data into extra refs or Pinia; see `nuxt-project-patterns`. ## Layout shift from async content - Skeletons and placeholders must match the final content's dimensions (`min-height`, `aspect-ratio`); a different-sized skeleton causes the shift it was meant to hide. - Reserve space for embeds, banners and late-injected components; insert new content in response to user input, not above existing content. - Animate `transform`/`opacity`, not layout properties (`top`, `height`, `width`). Respect `prefers-reduced-motion` (`frontend-a11y-check`). ## Third-party scripts - Every script needs an owner and a reason; remove unused tags. - Load with `async`/`defer`, after hydration or on interaction where possible; never as a blocking `` script. - Measure before and after adding one (LCP, INP, total blocking time). Tag managers count; audit what they inject. ## INP - Keep event handlers cheap: no heavy sync work, large loops or big JSON parsing in click/input handlers. - Break up long tasks (>50 ms): do the visual update first, then yield with `scheduler.yield()` falling back to `setTimeout(0)` (`scheduler.yield` is not in all browsers). Move heavy compute to a Web Worker. - Debounce input-driven work (search, validation) and keep per-keystroke state local to the input's component so large trees don't re-render. - Hydration cost counts on SSR pages; ship less client JS for static content. ## See also Optional, if installed: performance-engineering (the shared plugin) for the broader discipline this skill applies. This skill stands on its own.