--- name: performance-web-vitals description: "Expert guide for Web Performance optimization: Core Web Vitals (LCP, INP, CLS), bundle analysis, image optimization, rendering strategies, and Lighthouse score improvement / Panduan ahli untuk optimasi performa web: Core Web Vitals (LCP, INP, CLS), analisis bundle, optimasi gambar, strategi rendering, dan peningkatan skor Lighthouse." author: "Roedy Rustam" version: "4.4.2" --- # Web Performance & Core Web Vitals Expert [English](#english) | [Bahasa Indonesia](#bahasa-indonesia) --- ## English ### Orchestration & Integration - **Upstream Orchestrators**: Works under `brainstorming` and `zero-to-prod-orchestrator` (Phase 6 - Production Hardening & Web Performance) to achieve Lighthouse 95+ and green Core Web Vitals. - **Downstream Cooperating Skills**: - `senior-frontend` — Implement React 19 `useTransition`, memoization, and component virtualization. - `modern-web-guidance` — Apply CSS content-visibility, fetchpriority, and modern browser APIs. - `seo` — Align Core Web Vitals (LCP, INP, CLS) with search indexing criteria. - `cloud-hosting-expert` — Optimize edge caching, Brotli/Gzip compression, and CDN asset delivery. - `session-memory-manager` — Store performance benchmark results and bundle budgets. ### Description Expert guide for measuring, diagnosing, and optimizing web application performance with a focus on **Core Web Vitals** (LCP, INP, CLS), JavaScript bundle optimization, image and font loading strategies, rendering performance, and achieving high Lighthouse scores. Covers Next.js 15, React 19, and modern browser APIs. ### Trigger Conditions - Improving Lighthouse performance score below 90. - Diagnosing poor LCP, INP, or CLS metrics. - Reducing JavaScript bundle size or eliminating render-blocking resources. - Optimizing image loading, lazy loading, or font delivery. - Implementing React performance patterns (memo, useDeferredValue, Suspense). - Setting up Real User Monitoring (RUM) for Core Web Vitals. - Optimizing server response times (TTFB). --- ### Core Web Vitals Targets (2026 Standards) | Metric | Good | Needs Improvement | Poor | Measures | |---|---|---|---|---| | **LCP** (Largest Contentful Paint) | ≤ 2.5s | 2.5–4.0s | > 4.0s | Loading performance | | **INP** (Interaction to Next Paint) | ≤ 200ms | 200–500ms | > 500ms | Interactivity (replaced FID) | | **CLS** (Cumulative Layout Shift) | ≤ 0.1 | 0.1–0.25 | > 0.25 | Visual stability | | **FCP** (First Contentful Paint) | ≤ 1.8s | 1.8–3.0s | > 3.0s | Perceived loading | | **TTFB** (Time to First Byte) | ≤ 800ms | 800–1800ms | > 1800ms | Server responsiveness | --- ### LCP Optimization Strategies #### 1. Identify and Optimize the LCP Element ```typescript // Measure LCP with web-vitals library import { onLCP, onINP, onCLS } from 'web-vitals'; onLCP((metric) => { console.log('LCP:', metric.value, 'element:', metric.attribution.lcpEntry?.element); // Send to analytics sendToAnalytics({ name: metric.name, value: metric.value }); }); ``` #### 2. Preload LCP Image (Hero Image) ```html ``` #### 3. Next.js Image Component (LCP Hero) ```tsx import Image from 'next/image'; // LCP image: priority=true + fill or explicit size // Below-the-fold images: lazy load (default) ``` --- ### INP Optimization Strategies #### 1. Break Up Long Tasks ```typescript // BAD: Long synchronous task blocks the main thread function processLargeList(items: Item[]) { return items.map(expensiveTransform); // blocks for 500ms } // GOOD: Yield control back to browser between chunks async function processLargeListAsync(items: Item[]) { const results: Result[] = []; for (let i = 0; i < items.length; i++) { results.push(expensiveTransform(items[i])); if (i % 50 === 0) await scheduler.yield(); // yield every 50 items } return results; } ``` #### 2. React 19 — Concurrent Features for INP ```tsx import { useTransition, useDeferredValue, startTransition } from 'react'; // Non-urgent state updates — mark as transitions function SearchResults() { const [query, setQuery] = useState(''); const [isPending, startTransition] = useTransition(); const deferredQuery = useDeferredValue(query); return ( <> { setQuery(e.target.value); // urgent: update input immediately startTransition(() => { // non-urgent: defer expensive filtering }); }} /> {isPending && } {/* deferredQuery triggers re-render without blocking input */} > ); } ``` #### 3. Virtualize Long Lists ```tsx // Use TanStack Virtual for large lists (1000+ items) import { useVirtualizer } from '@tanstack/react-virtual'; function VirtualList({ items }: { items: Item[] }) { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 60, // estimated row height in px overscan: 5, }); return ( {virtualizer.getVirtualItems().map((vItem) => ( ))} ); } ``` --- ### CLS Optimization Strategies ```css /* 1. Reserve space for images with aspect-ratio */ img, video { width: 100%; height: auto; aspect-ratio: 16 / 9; /* prevents layout shift before load */ } /* 2. Reserve space for dynamic content */ .ad-container { min-height: 250px; /* reserve ad slot height */ } /* 3. Use font-display: optional or swap */ @font-face { font-family: 'Inter'; src: url('/fonts/inter.woff2') format('woff2'); font-display: optional; /* no invisible text, no layout shift */ } ``` ```tsx // 4. Avoid inserting content above existing content // BAD: inserting notification banner at top pushes content down setShowBanner(true); // causes CLS // GOOD: use position:fixed or reserve space at the top {showBanner && } ``` --- ### Bundle Optimization #### Next.js Bundle Analysis ```bash # Analyze bundle composition ANALYZE=true npm run build # Or use @next/bundle-analyzer ``` #### Code Splitting Patterns ```tsx import dynamic from 'next/dynamic'; // Dynamic import — code splits automatically const HeavyChart = dynamic(() => import('@/components/HeavyChart'), { loading: () => , ssr: false, // disable SSR for client-only components }); // Lazy load with React.lazy (non-Next.js) const Modal = React.lazy(() => import('./Modal')); ``` #### Tree-shaking Checklist - Use named imports: `import { debounce } from 'lodash-es'` (not `import _ from 'lodash'`) - Set `"sideEffects": false` in `package.json` for libraries. - Avoid `import *` from large libraries. - Replace `moment.js` with `date-fns` or `dayjs`. - Use `sharp` for server-side image processing. --- ### React Performance Patterns ```tsx // memo — skip re-render if props didn't change const ExpensiveList = memo(({ items }: { items: Item[] }) => ( {items.map((i) => {i.name})} )); // useCallback — stable function reference for memo children const handleDelete = useCallback((id: string) => { setItems((prev) => prev.filter((i) => i.id !== id)); }, []); // empty deps = stable reference // useMemo — memoize expensive calculations const sortedItems = useMemo( () => [...items].sort((a, b) => a.name.localeCompare(b.name)), [items] ); ``` --- ### Performance Monitoring Setup ```typescript // report-vitals.ts — send Core Web Vitals to analytics import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals'; function sendToAnalytics({ name, value, id, navigationType }: Metric) { // Send to PostHog, Google Analytics, or your own endpoint fetch('/api/analytics/vitals', { method: 'POST', body: JSON.stringify({ name, value: Math.round(value), id, navigationType }), keepalive: true, // ensures request completes even if page unloads }); } onCLS(sendToAnalytics); onINP(sendToAnalytics); onLCP(sendToAnalytics); onFCP(sendToAnalytics); onTTFB(sendToAnalytics); ``` --- ### Lighthouse Score Improvement Checklist - [ ] LCP image has `priority` or `fetchpriority="high"` + preload link. - [ ] All non-LCP images use `loading="lazy"`. - [ ] Fonts loaded with `font-display: optional` or preloaded with `rel="preload"`. - [ ] No render-blocking `