Performance Standards
April 10, 2026 · View on GitHub
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