* feat(instructions): update security, a11y, and performance to 2025-2026 standards Security: OWASP 2025 (55 anti-patterns, AI/LLM section, 6 frameworks) Accessibility: WCAG 2.2 AA (38 anti-patterns, legal context EAA/ADA, 4 frameworks) Performance: CWV (50 anti-patterns, Next.js 16, Angular 20, modern APIs) * fix(instructions): use globalThis.scheduler to prevent ReferenceError Access scheduler via globalThis to safely handle environments where the Scheduling API is not declared as a global variable. * fix(instructions): correct regex patterns and harden SSRF example - AU1: anchor jwt.verify lookahead inside parentheses - AU2: anchor jwt.sign lookahead, add expiresIn alternative - AU7: fix greedy .* before negative lookahead in OAuth state check - I5: resolve all DNS records, add TOCTOU production note - K2: add closing delimiters and multi-digit support to tabindex regex * fix(instructions): enhance SSRF IP validation with IPv4-mapped IPv6 Normalize IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) before checking private ranges, preventing bypass via mapped addresses. * fix(instructions): add noscript fallback for deferred CSS pattern Without JS, the media="print" + onload pattern leaves the stylesheet inactive. The noscript tag loads it normally when JS is disabled. * fix(instructions): add execFileSync to I3 command injection detection The BAD example uses execFileSync but the regex only matched exec, execSync, and execFile — missing the sync variant. * fix(instructions): cover full IPv6 link-local range in SSRF check fe80::/10 spans fe80-febf (fe8*, fe9*, fea*, feb*). Previous regex only matched fe80::. Also use normalized variable for consistency. * fix(instructions): adjust SSRF wording and downgrade reduced-motion severity - SSRF: replace "full DNS/IP validation" with accurate wording that acknowledges TOCTOU limitation - V5: downgrade prefers-reduced-motion from IMPORTANT to SUGGESTION, remove 2.2.2 (A) reference since it's an AAA enhancement * fix(instructions): rename AU4 heading to include SHA-256 The heading said "Weak Password Hash (MD5/SHA1)" but the detection regex and BAD example both use SHA-256. Renamed to "Fast Hash for Passwords" which better describes the actual anti-pattern. * fix(instructions): clarify WCAG 2.2 SC 4.1.1 status as obsolete SC 4.1.1 Parsing is still present in the WCAG 2.2 spec but marked as obsolete (always satisfied). Changed wording from "removed" to "obsolete" for accuracy. * fix(instructions): rename I1 example vars to avoid TS redeclaration Copy-pasting the I1 SQL injection example as a single block failed with a TypeScript redeclaration error because both BAD and GOOD snippets used `const result`. Rename to `unsafeResult`/`safeResult` so the block remains copy-pasteable into a single scope. * fix(instructions): migrate I3 example to async execFile with bounds The I3 command injection example used `execFileSync` in both BAD and GOOD paths, which (a) redeclared `const output` in the same block and (b) blocks the Node event loop in server handlers, amplifying DoS impact. Switch the GOOD/BEST paths to a promisified `execFile` call with explicit `timeout` and `maxBuffer` bounds, and rename variables to `unsafeOutput`/`safeOutput` so the snippet stays copy-pasteable. Add a trailing note recommending async child_process APIs for server code. * fix(instructions): align AU6 heading with session fixation example The AU6 heading claimed "Session Not Invalidated on Password Change" but the mitigation example showed `req.session.regenerate`, which is the canonical defense against session fixation on login rather than bulk invalidation after a credential change. Rename the anti-pattern to "Missing Session Regeneration on Login (Session Fixation)" so it matches the example, and add a trailing note pointing to the complementary practice of invalidating other active sessions for the user on password change (e.g., via a `tokenVersion` counter). * fix(instructions): make L1 critical CSS pattern CSP-compatible The L1 "GOOD" snippet relied on an inline `onload="this.media='all'"` handler on a `<link>` tag. Under a strict CSP that disallows `'unsafe-inline'` / `script-src-attr 'unsafe-inline'`, inline event handlers are blocked, so the stylesheet would never activate and users would hit a styling regression. Replace the pattern with build-time critical CSS extraction (Critters/Beasties/Next.js `optimizeCss`) plus a normal `<link rel="preload" as="style">` and standard `<link rel="stylesheet">`. Add a trailing note explaining why the older inline-onload trick breaks under strict CSP and how to defer non-critical CSS with an external script when deferral is truly needed.
27 KiB
applyTo, description
| applyTo | description |
|---|---|
| ** | Comprehensive web performance standards based on Core Web Vitals (LCP, INP, CLS), with 50+ anti-patterns, detection regex, framework-specific fixes for modern web frameworks, and modern API guidance. |
Performance Standards
Comprehensive performance rules for web application development. Every anti-pattern includes a severity classification, detection method, Core Web Vitals metric impacted, and corrective code examples.
Severity levels:
- CRITICAL — Directly degrades a Core Web Vital past the "poor" threshold. Must be fixed before merge.
- IMPORTANT — Measurably impacts user experience. Fix in same sprint.
- SUGGESTION — Optimization opportunity. Plan for a future iteration.
Core Web Vitals Quick Reference
LCP (Largest Contentful Paint)
Good: < 2.5s | Needs Improvement: 2.5-4s | Poor: > 4s
Measures when the largest visible content element finishes rendering. Four sequential phases:
| Phase | Target | What It Measures |
|---|---|---|
| TTFB | ~40% of budget | Server response time |
| Resource Load Delay | < 10% | Time between TTFB and LCP resource fetch start |
| Resource Load Duration | ~40% | Download time for the LCP resource |
| Element Render Delay | < 10% | Time between download and paint |
INP (Interaction to Next Paint)
Good: < 200ms | Needs Improvement: 200-500ms | Poor: > 500ms
Measures latency of all user interactions, reports the worst. Three phases:
| Phase | Optimization |
|---|---|
| Input Delay | Break long tasks, yield to browser |
| Processing Time | Keep handlers < 50ms |
| Presentation Delay | Minimize DOM size, avoid forced layout |
Diagnostic tool: Use the Long Animation Frames (LoAF) API (Chrome 123+) to debug INP issues. LoAF provides better attribution than the legacy Long Tasks API, including script source and rendering time.
CLS (Cumulative Layout Shift)
Good: < 0.1 | Needs Improvement: 0.1-0.25 | Poor: > 0.25
Layout shift sources: images without dimensions, dynamically injected content, web font FOUT, late-loading ads. Shifts within 500ms of user interaction are exempt.
Loading and LCP Anti-Patterns (L1-L10)
L1: Render-Blocking CSS Without Critical Extraction
- Severity: CRITICAL
- Detection:
<link.*rel="stylesheet"in<head>loading large CSS - CWV: LCP
<!-- BAD -->
<link rel="stylesheet" href="/styles/main.css" />
<!-- GOOD — inline critical CSS (extracted at build time), preload the rest -->
<style>/* critical above-fold CSS, inlined by a tool like Critters/Beasties */</style>
<link rel="preload" href="/styles/main.css" as="style" />
<link rel="stylesheet" href="/styles/main.css" />
Prefer build-time critical CSS extraction (e.g., Critters, Beasties, Next.js experimental.optimizeCss) plus a normal <link rel="stylesheet">. Avoid the older media="print" onload="this.media='all'" trick: inline event handlers are blocked under a strict CSP (no 'unsafe-inline' / no script-src-attr 'unsafe-inline'), which would prevent the stylesheet from ever activating and cause a styling regression. If non-critical CSS truly must be deferred, load it via an external script that swaps media, not an inline handler.
L2: Render-Blocking Synchronous Script
- Severity: CRITICAL
- Detection:
<script.*src=withoutasync|defer|type="module" - CWV: LCP
<!-- BAD -->
<script src="/vendor/analytics.js"></script>
<!-- GOOD -->
<script src="/vendor/analytics.js" defer></script>
L3: Missing Preconnect to Critical Origins
- Severity: IMPORTANT
- Detection: Third-party API/CDN URLs without
<link rel="preconnect"> - CWV: LCP
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://analytics.example.com" />
L4: Missing Preload for LCP Resource
- Severity: CRITICAL
- Detection: LCP image/font not preloaded
- CWV: LCP
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
L5: Client-Side Data Fetching for Main Content
- Severity: CRITICAL
- Detection:
useEffect.*fetch|useEffect.*axios|ngOnInit.*subscribe - CWV: LCP
// BAD — content appears after JS execution + API call
'use client';
function Page() {
const [data, setData] = useState(null);
useEffect(() => { fetch('/api/data').then(r => r.json()).then(setData); }, []);
return <div>{data?.title}</div>;
}
// GOOD — Server Component fetches data before HTML is sent
async function Page() {
const data = await fetch('https://api.example.com/data').then(r => r.json());
return <div>{data.title}</div>;
}
L6: Excessive Redirect Chains
- Severity: IMPORTANT
- Detection: Multiple sequential redirects (HTTP 301/302 chains)
- CWV: LCP
Each redirect adds 200-300ms. Maximum one redirect.
L7: Missing fetchpriority on LCP Element
- Severity: IMPORTANT
- Detection: Above-fold hero image without
fetchpriority="high"orpriorityprop - CWV: LCP
// Next.js
<Image src="/hero.webp" alt="Hero" width={1200} height={600} priority />
// Angular
<img ngSrc="/hero.webp" alt="Hero" width="1200" height="600" priority>
// Plain HTML
<img src="/hero.webp" alt="Hero" width="1200" height="600" fetchpriority="high" />
L8: Third-Party Scripts in Head Without Async/Defer
- Severity: IMPORTANT
- Detection:
<script.*src="https://withoutasync|defer - CWV: LCP
Defer non-essential scripts. Use facade pattern for chat widgets.
L9: Oversized Initial HTML (>14KB)
- Severity: SUGGESTION
- Detection: Server-rendered HTML larger than 14KB
- CWV: LCP
Reduce inline CSS/JS, remove whitespace, use streaming SSR with Suspense boundaries.
L10: Missing Compression
- Severity: IMPORTANT
- Detection: Server not returning
content-encoding: brorgzip - CWV: LCP
Enable Brotli (15-25% better than gzip) at CDN/server level.
Rendering and Hydration Anti-Patterns (R1-R8)
R1: Entire Component Tree Marked "use client"
- Severity: CRITICAL
- Detection:
"use client"at top-level layout or page component - CWV: LCP + INP
Push "use client" down to leaf components that need interactivity.
R2: Missing Suspense Boundaries for Async Data
- Severity: IMPORTANT
- Detection: Server Components doing data fetching without
<Suspense> - CWV: LCP
// GOOD — stream shell immediately, fill in data progressively
async function Page() {
const user = await getUser();
return (
<div>
<Header user={user} />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
</div>
);
}
R3: Hydration Mismatch from Dynamic Client Content
- Severity: IMPORTANT
- Detection:
Date.now()|Math.random()|window\.innerWidthin SSR components - CWV: CLS
Use useEffect for client-only values, or suppressHydrationWarning for known differences.
R4: Missing Streaming for Slow Data Sources
- Severity: IMPORTANT
- Detection: Page awaiting all data before sending HTML
- CWV: LCP (TTFB)
Use streaming SSR with Suspense boundaries. Shell streams immediately; slow data fills in progressively.
R5: Unstable References Causing Re-renders
- Severity: IMPORTANT
- Detection:
style=\{\{|onClick=\{\(\) =>inline in JSX - CWV: INP
React 19+ with React Compiler enabled (separate babel/SWC build plugin): auto-memoized. Without Compiler: extract or memoize with useMemo/useCallback. Angular: OnPush. Vue: computed().
R6: Missing Virtualization for Long Lists
- Severity: IMPORTANT
- Detection:
.map(rendering >100 items without virtual scrolling - CWV: INP
Use TanStack Virtual, react-window, Angular CDK Virtual Scroll, or vue-virtual-scroller.
R7: SSR of Immediately-Hidden Content
- Severity: SUGGESTION
- Detection: Server-rendering
display: nonecomponents - CWV: LCP (TTFB)
Use client-side rendering for modals, drawers, dropdowns. Angular: @defer. React: React.lazy.
R8: Missing key Prop on List Items
- Severity: IMPORTANT
- Detection:
.map(withoutkey=prop - CWV: INP
// GOOD — stable unique key
{items.map(item => <Row key={item.id} data={item} />)}
Never use array index as key if list can reorder.
JavaScript Runtime and INP Anti-Patterns (J1-J8)
J1: Long Synchronous Task in Event Handler
- Severity: CRITICAL
- Detection: Event handlers with heavy computation (>50ms)
- CWV: INP
// GOOD — yield to browser
async function handleClick() {
setLoading(true);
await (globalThis.scheduler?.yield?.() ?? new Promise(r => setTimeout(r, 0)));
const result = expensiveComputation(data);
setResult(result);
}
Move heavy work to Web Worker for best results.
Note:
scheduler.yield()is supported in Chrome 129+, Firefox 129+, but NOT Safari as of April 2026. Fallback:await (globalThis.scheduler?.yield?.() ?? new Promise(r => setTimeout(r, 0))).
J2: Layout Thrashing
- Severity: CRITICAL
- Detection:
offsetHeight|offsetWidth|getBoundingClientRect|clientHeightin loops - CWV: INP
// GOOD — batch reads then batch writes
const heights = elements.map(el => el.offsetHeight);
elements.forEach((el, i) => { el.style.height = `${heights[i] + 10}px`; });
J3: setInterval/setTimeout Without Cleanup
- Severity: IMPORTANT
- Detection:
setInterval|setTimeoutwithout cleanup - Impact: Memory
useEffect(() => {
const id = setInterval(() => fetchData(), 5000);
return () => clearInterval(id);
}, []);
J4: addEventListener Without removeEventListener
- Severity: IMPORTANT
- Detection:
addEventListenerwithout cleanup - Impact: Memory
useEffect(() => {
const controller = new AbortController();
window.addEventListener('resize', handleResize, { signal: controller.signal });
return () => controller.abort();
}, []);
J5: Detached DOM Node References
- Severity: SUGGESTION
- Detection: Variables holding references to removed DOM elements
- Impact: Memory
Set references to null when elements are removed.
J6: Synchronous XHR
- Severity: CRITICAL
- Detection:
XMLHttpRequestwith synchronous flag - CWV: INP
Use fetch() (always async).
J7: Heavy Computation on Main Thread
- Severity: IMPORTANT
- Detection: CPU-intensive operations in component code
- CWV: INP
Move to Web Worker or break into chunks with scheduler.yield().
J8: Missing Effect Cleanup
- Severity: IMPORTANT
- Detection:
useEffectwithout return cleanup;subscribewithout unsubscribe - Impact: Memory
React: return cleanup from useEffect. Angular: takeUntilDestroyed(). Vue: onUnmounted.
CSS Performance Anti-Patterns (C1-C7)
C1: Animation Using Layout-Triggering Properties
- Severity: CRITICAL
- Detection:
animation:|transition:withtop|left|width|height|margin|padding - CWV: INP
/* BAD — main thread, <60fps */
.card { transition: width 0.3s, height 0.3s; }
/* GOOD — GPU compositor, 60fps */
.card { transition: transform 0.3s, opacity 0.3s; }
.card:hover { transform: scale(1.05); }
C2: Missing content-visibility for Off-Screen Sections
- Severity: SUGGESTION
- Detection: Long pages without
content-visibility: auto - CWV: INP
.below-fold-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
C3: will-change Applied Permanently
- Severity: SUGGESTION
- Detection:
will-change:in base CSS (not:hover|:focus) - Impact: Memory
Apply on interaction only or let browser optimize automatically.
C4: Large Unused CSS
- Severity: IMPORTANT
- Detection: CSS where >50% of rules are unused
- CWV: LCP
Use PurgeCSS, Tailwind purge, or critters. Code-split CSS per route.
C5: Universal Selector in Hot Paths
- Severity: SUGGESTION
- Detection:
\* \{in CSS - CWV: INP
/* GOOD — zero-specificity reset */
:where(*, *::before, *::after) { box-sizing: border-box; }
C6: Missing CSS Containment
- Severity: SUGGESTION
- Detection: Complex components without
containproperty - CWV: INP
.sidebar { contain: layout style paint; }
C7: Route Transitions Without View Transitions API
- Severity: SUGGESTION
- Detection: SPA route changes without View Transitions API
- CWV: CLS (perceived)
// Use View Transitions for smooth route changes (with feature check)
if (document.startViewTransition) {
document.startViewTransition(() => {
// update DOM / navigate
});
} else {
// fallback: update DOM directly
}
Same-document transitions supported in all major browsers. Cross-document supported in Chrome/Edge 126+, Safari 18.5+. Always feature-check before calling — unsupported browsers will throw without the guard.
Images, Media and Fonts Anti-Patterns (I1-I8)
I1: Images Without Dimensions
- Severity: CRITICAL
- Detection:
<imgwithoutwidth=andheight= - CWV: CLS
Always set width and height on images, or use aspect-ratio in CSS.
I2: Lazy Loading Above-Fold Images
- Severity: CRITICAL
- Detection:
loading="lazy"on hero/banner images - CWV: LCP
<!-- GOOD — eager load with high priority -->
<img src="/hero.webp" alt="Hero" fetchpriority="high" />
I3: Legacy Format Only (JPEG/PNG)
- Severity: IMPORTANT
- Detection: Images without WebP/AVIF alternatives
- CWV: LCP
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero" width="1200" height="600" />
</picture>
I4: Missing Responsive srcset/sizes
- Severity: IMPORTANT
- Detection:
<imgwithoutsrcset - CWV: LCP
<img src="/hero-800.jpg" alt="Hero"
srcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1024px) 800px, 1200px" />
I5: Font Without font-display
- Severity: IMPORTANT
- Detection:
@font-facewithoutfont-display - CWV: CLS
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* or "optional" for best CLS */
}
I6: Critical Font Not Preloaded
- Severity: IMPORTANT
- Detection: Custom font without
<link rel="preload"> - CWV: LCP + CLS
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin />
I7: Full Font Loaded When Subset Suffices
- Severity: SUGGESTION
- Detection: Font files > 50KB WOFF2
- CWV: LCP
Use unicode-range, subset with glyphhanger, or next/font (auto-subsets Google Fonts).
I8: Unoptimized SVGs
- Severity: SUGGESTION
- Detection: SVGs with editor metadata
- CWV: LCP (minor)
npx svgo input.svg -o output.svg
Bundle and Tree Shaking Anti-Patterns (B1-B6)
B1: Barrel File Importing Entire Module
- Severity: IMPORTANT
- Detection:
from '\.\/(?:.*\/index|components)' - CWV: INP
// BAD
import { Button } from './components';
// GOOD — direct import
import { Button } from './components/Button';
B2: CommonJS require() Preventing Tree Shaking
- Severity: IMPORTANT
- Detection:
require(in frontend code - CWV: INP
Use ESM import/export. Replace require with import.
B3: Large Dependency for Small Utility
- Severity: IMPORTANT
- Detection:
from "moment"|from "lodash"(full imports) - CWV: INP
// GOOD — tree-shakeable alternatives
import { format } from 'date-fns';
import { pick } from 'lodash-es';
// BEST — native JS
const formatted = new Intl.DateTimeFormat('en').format(date);
B4: Missing Dynamic Import for Route Splitting
- Severity: CRITICAL
- Detection: All route components imported statically
- CWV: INP
// Next.js: automatic with file-based routing
// React:
const Page = React.lazy(() => import('./pages/Page'));
// Angular:
{ path: 'settings', loadComponent: () => import('./pages/settings.component') }
// Vue:
const Page = defineAsyncComponent(() => import('./pages/Page.vue'));
B5: Missing sideEffects in package.json
- Severity: SUGGESTION
- Detection: Library package.json without
"sideEffects"field - CWV: INP
{ "sideEffects": false }
B6: Duplicate Dependencies
- Severity: SUGGESTION
- Detection: Same library at multiple versions
- CWV: INP
npm dedupe
Framework-Specific: Next.js (NX1-NX6)
NX1: Not Using next/image
- Severity: IMPORTANT
- Detection:
<imgin.tsxinstead of<Image> - CWV: LCP + CLS
import Image from 'next/image';
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />
NX2: Not Using Cache Components for Partial Prerendering
- Severity: IMPORTANT
- Detection: Pages without
"use cache"directive in Next.js 16+ projects - CWV: LCP
// BAD — entire page is dynamic
export default async function Page() {
const data = await fetchData(); // blocks full page render
return <div>{data.title}</div>;
}
// GOOD — enable Partial Prerendering with "use cache"
// next.config.ts: { cacheComponents: true }
"use cache";
export default async function Page() {
const data = await fetchData(); // static shell renders instantly, dynamic holes stream
return <div>{data.title}</div>;
}
Enable in next.config.ts with cacheComponents: true. Use "use cache" at file, component, or function level. Static shell loads instantly; dynamic content streams via Suspense boundaries.
NX3: Unnecessary "use client" on Server-Renderable Component
- Severity: IMPORTANT
- Detection:
"use client"on components without hooks or browser APIs - CWV: INP
Remove "use client" from components that only render static content.
NX4: Data Fetching in useEffect Instead of Server-Side
- Severity: CRITICAL
- Detection:
useEffect+fetchin Next.js App Router pages - CWV: LCP
Fetch data in Server Components directly (async function body).
NX5: Missing next/font
- Severity: IMPORTANT
- Detection:
fonts.googleapis|fonts.gstaticin CSS/HTML - CWV: CLS + LCP
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
NX6: Missing "use cache" for Cacheable Server Functions
- Severity: IMPORTANT
- Detection: Async server functions without
"use cache"in Next.js 16+ withcacheComponents: true - CWV: LCP
// BAD — data fetched on every request
async function getProducts() {
return await db.products.findMany();
}
// GOOD — cached with revalidation
"use cache";
import { cacheLife } from 'next/cache';
async function getProducts() {
cacheLife('hours');
return await db.products.findMany();
}
"use cache" replaces the old unstable_cache and fetch cache options. Use cacheLife() and cacheTag() for fine-grained control.
Framework-Specific: Angular (NG1-NG6)
NG1: Default Change Detection on Presentational Components
- Severity: IMPORTANT
- Detection: Components without
ChangeDetectionStrategy.OnPush(Angular <19) or without signals (Angular 19+) - CWV: INP
// Angular <19: Use OnPush
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
...
})
// Angular 19+: Prefer zoneless with signals
// app.config.ts: provideZonelessChangeDetection()
@Component({ ... })
export class ProductCard {
product = input.required<Product>(); // signal input
price = computed(() => this.product().price * 1.19); // derived signal
}
Angular 19+: prefer zoneless change detection with signals. OnPush is unnecessary when using signal-based reactivity. Angular 20+ has stable zoneless support.
NG2: Not Using NgOptimizedImage
- Severity: IMPORTANT
- Detection:
<imgwithoutngSrcin.component.html - CWV: LCP + CLS
<img ngSrc="/hero.jpg" alt="Hero" width="1200" height="600" priority />
NG3: Missing @defer for Below-Fold Content
- Severity: SUGGESTION
- Detection: Heavy below-fold components loaded eagerly (Angular 17+)
- CWV: INP
@defer (on viewport) {
<app-heavy-chart [data]="chartData" />
} @placeholder {
<div class="chart-skeleton"></div>
}
NG4: Not Using Signals for Reactive State
- Severity: SUGGESTION
- Detection: Class properties without signals in Angular 19+
- CWV: INP
Use signal() for reactive state, computed() for derived values. Signal APIs (signal(), computed(), effect()) are stable since Angular 20.
NG5: Full Hydration Without Incremental Hydration
- Severity: IMPORTANT
- Detection: SSR app without
withIncrementalHydration()in Angular 19+ - CWV: LCP, INP
// BAD — full hydration blocks interactivity
provideClientHydration()
// GOOD — incremental hydration with triggers
provideClientHydration(withIncrementalHydration())
Use @defer triggers (on viewport, on interaction) to hydrate components on demand. Reduces TTI by deferring non-critical component hydration.
NG6: Still Using zone.js in Angular 20+ Projects
- Severity: SUGGESTION
- Detection:
zone.jsin polyfills array, noprovideZonelessChangeDetection()in Angular 20+ - CWV: INP
// app.config.ts
export const appConfig = {
providers: [
provideZonelessChangeDetection(), // removes ~15-30KB from bundle
// ...
]
};
Zoneless change detection with signals reduces bundle size and improves runtime performance. Stable since Angular 20.
Framework-Specific: React (RX1-RX4)
RX1: Missing React Compiler Adoption
- Severity: SUGGESTION
- Detection: Manual
useMemo|useCallbackin React 19+ project - CWV: INP
Enable React Compiler (v19+) for auto-memoization. Remove manual wrappers.
RX2: Missing useTransition for Expensive Updates
- Severity: IMPORTANT
- Detection: State updates causing expensive re-renders without
useTransition - CWV: INP
const [isPending, startTransition] = useTransition();
function handleFilter(value) {
startTransition(() => setFilter(value));
}
RX3: Missing useDeferredValue for Expensive Rendering
- Severity: IMPORTANT
- Detection: Expensive rendering from rapidly-changing input
- CWV: INP
const deferredQuery = useDeferredValue(query);
const results = expensiveFilter(items, deferredQuery);
RX4: Missing React.lazy for Route Splitting
- Severity: IMPORTANT
- Detection: Route components imported statically
- CWV: INP
const Settings = React.lazy(() => import('./pages/Settings'));
Framework-Specific: Vue (VU1-VU4)
VU1: reactive() on Large Data Structures
- Severity: IMPORTANT
- Detection:
reactive(on large arrays or deep objects - CWV: INP
Use shallowRef() or shallowReactive() for large data.
VU2: Missing v-memo on Expensive List Renders
- Severity: SUGGESTION
- Detection: Large lists without
v-memo - CWV: INP
<div v-for="item in items" :key="item.id" v-memo="[item.id, item.updatedAt]">
<ExpensiveItem :data="item" />
</div>
VU3: Missing defineAsyncComponent
- Severity: IMPORTANT
- Detection: Heavy components imported statically
- CWV: INP
const HeavyChart = defineAsyncComponent(() => import('./HeavyChart.vue'));
VU4: Not Using Vapor Mode for Performance-Critical Components
- Severity: SUGGESTION
- Detection: Performance-critical components using virtual DOM in Vue 3.6+
- CWV: INP
Vue 3.6+ Vapor Mode compiles templates to direct DOM operations, bypassing the virtual DOM. Use for performance-critical subtrees. Can be mixed with standard components.
Resource Hints Quick Reference
| Hint | Purpose | When to Use |
|---|---|---|
preconnect |
DNS + TCP + TLS early | Critical third-party origins (API, CDN, fonts) |
preload |
Fetch immediately, high priority | LCP image, critical font |
prefetch |
Low priority for future navigation | Next-page assets |
dns-prefetch |
DNS resolution only | Non-critical third-party origins |
modulepreload |
Preload + parse ES module | Critical JS modules |
<script type="speculationrules"> |
Prefetch/prerender next navigation | Likely next pages (Chrome 121+, progressive enhancement) |
Image Optimization Quick Reference
| Aspect | Recommendation |
|---|---|
| Format | WebP (25-34% smaller), AVIF (50% smaller) |
| LCP image | fetchpriority="high" or framework priority prop |
| Below-fold | loading="lazy" |
| Dimensions | Always set width + height |
| Responsive | srcset + sizes or framework Image component |
| Compression | Quality 75-85 for photos |
Font Loading Quick Reference
| Strategy | Best For | CLS Impact |
|---|---|---|
font-display: swap |
Body text | Slight FOUT, minimal CLS |
font-display: optional |
All fonts (best CLS) | No FOUT, no CLS |
next/font |
Next.js projects | Zero CLS |
| Variable fonts | Multiple weights | Single file for all weights |
Rules: preload 1-2 critical fonts only, use WOFF2, subset to needed characters, self-host when possible.
Performance Checklist (CWV)
LCP (< 2.5s)
- LCP image has
fetchpriority="high"orpriorityprop - LCP image preloaded if not in HTML source
- No
loading="lazy"on above-fold images - Critical CSS inlined or extracted
- No render-blocking scripts (use
deferorasync) - Preconnect to critical third-party origins
- Main content server-rendered (not client-side fetched)
- Images in modern format (WebP/AVIF) with responsive
srcset - Compression enabled (Brotli preferred)
- Fonts preloaded with
font-display: swaporoptional
INP (< 200ms)
- Event handlers complete in < 50ms
- Long tasks broken into smaller chunks
- Route-based code splitting implemented
- Heavy computation moved to Web Workers
- Lists with > 100 items virtualized
- No barrel file imports (direct component imports)
- ESM imports used (not CommonJS
require) "use client"only on components that need interactivity- Layout-triggering CSS properties not animated
- Effect cleanup implemented (no leaking listeners/timers)
CLS (< 0.1)
- All images have
widthandheightattributes - Fonts use
font-display: swaporoptional - No content injected above existing content dynamically
- Ads/embeds have reserved space
- No hydration mismatches
content-visibility: autohascontain-intrinsic-size