Core Web Vitals Architecture: Engineering for LCP, INP, and CLS
May 22, 2026112 min read0 views
Core Web Vitals Architecture: Engineering for LCP, INP, and CLS
Core Web Vitals directly impact user experience and search rankings. Understanding the browser rendering pipeline, compositor thread mechanics, layout shift detection algorithms, and interaction timing measurement is essential for building high-performance web applications.
Browser Rendering Pipeline Deep Dive
┌─────────────────────────────────────────────────────────────────────────┐
│ Browser Rendering Pipeline │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Main Thread │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Parse ──▶ Style ──▶ Layout ──▶ Paint ──▶ Composite │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ DOM CSSOM Layout Paint Layer │ │
│ │ Tree Tree Tree Records Tree │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Compositor Thread │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Tiling ──▶ Rasterize ──▶ Draw ──▶ Display │ │
│ │ │ │ │ │ │ │
│ │ Split GPU Work Quads V-Sync │ │
│ │ Layers │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ Timeline │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ FCP LCP INP events CLS shifts │ │
│ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ │ │
│ │ First Largest Input Layout │ │
│ │ Paint Content Delay Instability │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
LCP (Largest Contentful Paint) Optimization
LCP Element Detection Algorithm
// LCP measurement and optimization
class LCPOptimizer {
private observer: PerformanceObserver | null = null;
private lcpCandidates: LCPCandidate[] = [];
private finalLCP: LCPCandidate | null = null;
startObserving(): void {
this.observer = new PerformanceObserver((list) => {
const entries = list.getEntries() as LargestContentfulPaint[];
for (const entry of entries) {
this.lcpCandidates.push({
element: entry.element,
startTime: entry.startTime,
renderTime: entry.renderTime,
loadTime: entry.loadTime,
size: entry.size,
url: entry.url,
id: entry.id
});
}
});
this.observer.observe({
type: 'largest-contentful-paint',
buffered: true
});
// Finalize LCP on user interaction or visibility change
['keydown', 'click', 'scroll'].forEach(type => {
addEventListener(type, () => this.finalizeLCP(), { once: true });
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.finalizeLCP();
}
});
}
private finalizeLCP(): void {
if (this.finalLCP) return;
this.observer?.disconnect();
// Last candidate is the final LCP
this.finalLCP = this.lcpCandidates[this.lcpCandidates.length - 1];
// Analyze LCP breakdown
if (this.finalLCP) {
this.analyzeLCPBreakdown(this.finalLCP);
}
}
private analyzeLCPBreakdown(lcp: LCPCandidate): LCPBreakdown {
// Get navigation timing
const navEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
// Calculate time breakdown
const ttfb = navEntry.responseStart - navEntry.requestStart;
const resourceLoadDelay = lcp.loadTime ? lcp.loadTime - navEntry.responseEnd : 0;
const resourceLoadDuration = lcp.loadTime && lcp.url
? this.getResourceLoadDuration(lcp.url)
: 0;
const elementRenderDelay = lcp.renderTime - (lcp.loadTime || navEntry.responseEnd);
return {
ttfb,
resourceLoadDelay,
resourceLoadDuration,
elementRenderDelay,
total: lcp.renderTime,
// Optimization recommendations
recommendations: this.generateRecommendations({
ttfb,
resourceLoadDelay,
resourceLoadDuration,
elementRenderDelay
})
};
}
private generateRecommendations(breakdown: TimeBreakdown): string[] {
const recommendations: string[] = [];
// TTFB issues
if (breakdown.ttfb > 800) {
recommendations.push(
'High TTFB detected. Consider: CDN, server-side caching, edge computing'
);
}
// Resource delay issues
if (breakdown.resourceLoadDelay > 100) {
recommendations.push(
'LCP resource discovery is delayed. Use preload or inline critical resources'
);
}
// Resource load duration
if (breakdown.resourceLoadDuration > 500) {
recommendations.push(
'LCP resource is large. Optimize image size, use modern formats (WebP/AVIF)'
);
}
// Render delay
if (breakdown.elementRenderDelay > 100) {
recommendations.push(
'Render blocking detected. Check for render-blocking JS/CSS, font loading'
);
}
return recommendations;
}
private getResourceLoadDuration(url: string): number {
const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
const resource = resources.find(r => r.name === url);
return resource ? resource.responseEnd - resource.requestStart : 0;
}
}
interface LCPCandidate {
element: Element | null;
startTime: number;
renderTime: number;
loadTime: number;
size: number;
url: string;
id: string;
}
interface LCPBreakdown {
ttfb: number;
resourceLoadDelay: number;
resourceLoadDuration: number;
elementRenderDelay: number;
total: number;
recommendations: string[];
}
LCP Resource Prioritization
// Priority hints and resource loading optimization
class LCPResourceOptimizer {
// Preload critical LCP resources
injectPreloadHints(lcpResources: LCPResource[]): void {
const head = document.head;
for (const resource of lcpResources) {
const link = document.createElement('link');
link.rel = 'preload';
link.href = resource.url;
link.as = resource.type;
// High priority for LCP
link.setAttribute('fetchpriority', 'high');
// Add crossorigin if needed
if (resource.crossOrigin) {
link.crossOrigin = resource.crossOrigin;
}
// For images, add imagesrcset and imagesizes
if (resource.type === 'image' && resource.srcset) {
link.setAttribute('imagesrcset', resource.srcset);
link.setAttribute('imagesizes', resource.sizes || '100vw');
}
head.appendChild(link);
}
}
// Server-side: Generate optimal resource hints
generateResourceHints(pageAnalysis: PageAnalysis): string {
const hints: string[] = [];
// Preconnect to critical origins
for (const origin of pageAnalysis.criticalOrigins) {
hints.push(
`<link rel="preconnect" href="${origin}" crossorigin>`
);
}
// Preload LCP image
if (pageAnalysis.lcpImage) {
const img = pageAnalysis.lcpImage;
hints.push(
`<link rel="preload" as="image" href="${img.src}" ` +
`imagesrcset="${img.srcset}" ` +
`imagesizes="${img.sizes}" ` +
`fetchpriority="high">`
);
}
// Preload LCP fonts
for (const font of pageAnalysis.lcpFonts) {
hints.push(
`<link rel="preload" as="font" href="${font.url}" ` +
`type="${font.type}" crossorigin>`
);
}
return hints.join('\n');
}
// Responsive image optimization for LCP
generateResponsiveLCPImage(image: ImageConfig): string {
const breakpoints = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
const srcset = breakpoints
.filter(bp => bp <= image.maxWidth * 2)
.map(bp => `${image.baseUrl}?w=${bp}&q=75 ${bp}w`)
.join(', ');
// Generate sizes based on layout
const sizes = this.calculateOptimalSizes(image);
return `
<img
src="${image.baseUrl}?w=1200&q=75"
srcset="${srcset}"
sizes="${sizes}"
alt="${image.alt}"
width="${image.width}"
height="${image.height}"
fetchpriority="high"
decoding="sync"
loading="eager"
/>
`;
}
private calculateOptimalSizes(image: ImageConfig): string {
// Generate sizes attribute based on layout breakpoints
const rules: string[] = [];
// Mobile first
rules.push('(max-width: 640px) 100vw');
// Tablet
if (image.layout === 'full') {
rules.push('(max-width: 1024px) 100vw');
} else {
rules.push('(max-width: 1024px) 50vw');
}
// Desktop
rules.push(`${image.displayWidth}px`);
return rules.join(', ');
}
}
interface LCPResource {
url: string;
type: 'image' | 'font' | 'script' | 'style';
crossOrigin?: 'anonymous' | 'use-credentials';
srcset?: string;
sizes?: string;
}
INP (Interaction to Next Paint) Optimization
Event Processing Pipeline
// INP measurement and optimization
class INPOptimizer {
private interactions: InteractionEntry[] = [];
private eventTimings: Map<number, EventTiming> = new Map();
startObserving(): void {
// Observe event timing
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries() as PerformanceEventTiming[];
for (const entry of entries) {
// Only track discrete events (not continuous like mousemove)
if (!entry.interactionId) continue;
this.recordInteraction(entry);
}
});
observer.observe({
type: 'event',
buffered: true,
durationThreshold: 16 // 1 frame at 60fps
});
// Report INP on page hide
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.reportINP();
}
});
}
private recordInteraction(entry: PerformanceEventTiming): void {
const interactionId = entry.interactionId;
// Get or create interaction entry
let interaction = this.interactions.find(i => i.id === interactionId);
if (!interaction) {
interaction = {
id: interactionId,
startTime: entry.startTime,
duration: entry.duration,
events: [],
breakdown: null
};
this.interactions.push(interaction);
}
// Update with longest event in this interaction
if (entry.duration > interaction.duration) {
interaction.duration = entry.duration;
}
// Record event breakdown
interaction.events.push({
name: entry.name,
startTime: entry.startTime,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
duration: entry.duration,
cancelable: entry.cancelable,
target: this.getElementIdentifier(entry.target as Element)
});
// Calculate timing breakdown
interaction.breakdown = this.calculateBreakdown(entry);
}
private calculateBreakdown(entry: PerformanceEventTiming): INPBreakdown {
// Input delay: time from event timestamp to processing start
const inputDelay = entry.processingStart - entry.startTime;
// Processing time: event handler execution
const processingTime = entry.processingEnd - entry.processingStart;
// Presentation delay: time from processing end to next paint
const presentationDelay = entry.duration - inputDelay - processingTime;
return {
inputDelay,
processingTime,
presentationDelay,
total: entry.duration
};
}
private reportINP(): void {
if (this.interactions.length === 0) return;
// Sort by duration
const sorted = [...this.interactions].sort(
(a, b) => b.duration - a.duration
);
// INP is the 98th percentile interaction
const percentileIndex = Math.min(
sorted.length - 1,
Math.floor(sorted.length * 0.98)
);
const inp = sorted[percentileIndex];
// Report with breakdown
console.log('INP Report:', {
value: inp.duration,
breakdown: inp.breakdown,
interactionCount: this.interactions.length,
worstInteractions: sorted.slice(0, 5).map(i => ({
duration: i.duration,
events: i.events.map(e => e.name),
target: i.events[0]?.target
}))
});
}
private getElementIdentifier(element: Element | null): string {
if (!element) return 'unknown';
const parts: string[] = [element.tagName.toLowerCase()];
if (element.id) {
parts.push(`#${element.id}`);
}
if (element.className) {
parts.push(`.${element.className.split(' ').join('.')}`);
}
return parts.join('');
}
}
interface InteractionEntry {
id: number;
startTime: number;
duration: number;
events: EventDetail[];
breakdown: INPBreakdown | null;
}
interface EventDetail {
name: string;
startTime: number;
processingStart: number;
processingEnd: number;
duration: number;
cancelable: boolean;
target: string;
}
interface INPBreakdown {
inputDelay: number; // Time until handler starts
processingTime: number; // Handler execution time
presentationDelay: number; // Time to next paint
total: number;
}
Input Delay Reduction Strategies
// Strategies for reducing input delay
class InputDelayOptimizer {
// Yield to main thread to reduce input delay
async yieldToMain(): Promise<void> {
// scheduler.yield() is the modern API
if ('scheduler' in globalThis && 'yield' in (globalThis as any).scheduler) {
return (globalThis as any).scheduler.yield();
}
// Fallback: setTimeout with 0 delay
return new Promise(resolve => setTimeout(resolve, 0));
}
// Break up long tasks
async processInChunks<T>(
items: T[],
processor: (item: T) => void,
chunkSize: number = 5
): Promise<void> {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
// Process chunk
for (const item of chunk) {
processor(item);
}
// Yield between chunks if more work remains
if (i + chunkSize < items.length) {
await this.yieldToMain();
}
}
}
// Idle callback for non-critical work
scheduleIdleWork(
work: () => void,
options: IdleWorkOptions = {}
): number {
const deadline = options.timeout || 1000;
return requestIdleCallback(
(idleDeadline) => {
if (idleDeadline.timeRemaining() > 0 || idleDeadline.didTimeout) {
work();
} else {
// Reschedule if no time available
this.scheduleIdleWork(work, options);
}
},
{ timeout: deadline }
);
}
// Debounce expensive handlers
createDebouncedHandler<T extends (...args: any[]) => void>(
handler: T,
wait: number
): T {
let timeoutId: number | null = null;
return ((...args: Parameters<T>) => {
if (timeoutId) {
cancelAnimationFrame(timeoutId);
}
timeoutId = requestAnimationFrame(() => {
// Use double-RAF for after-paint execution
requestAnimationFrame(() => {
handler(...args);
});
});
}) as T;
}
// Passive event listeners for scroll/touch
addPassiveListener(
element: EventTarget,
event: string,
handler: EventListener
): void {
element.addEventListener(event, handler, { passive: true });
}
// Event delegation to reduce listener count
setupDelegatedHandler(
container: Element,
selector: string,
event: string,
handler: (target: Element, event: Event) => void
): void {
container.addEventListener(event, (e) => {
const target = (e.target as Element).closest(selector);
if (target && container.contains(target)) {
handler(target, e);
}
});
}
}
interface IdleWorkOptions {
timeout?: number;
}
// React hook for INP-friendly state updates
function useINPFriendlyState<T>(initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
const [state, setState] = React.useState(initialValue);
const pendingUpdate = React.useRef<T | null>(null);
const setStateDeferred = React.useCallback((value: T | ((prev: T) => T)) => {
// Calculate new value immediately for optimistic UI
const newValue = typeof value === 'function'
? (value as (prev: T) => T)(state)
: value;
pendingUpdate.current = newValue;
// Use startTransition for non-urgent update
React.startTransition(() => {
setState(pendingUpdate.current as T);
pendingUpdate.current = null;
});
}, [state]);
return [state, setStateDeferred];
}
// Worker offloading for heavy computation
class ComputeWorkerPool {
private workers: Worker[] = [];
private taskQueue: QueuedTask[] = [];
private idleWorkers: Worker[] = [];
constructor(workerScript: string, poolSize: number = navigator.hardwareConcurrency || 4) {
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerScript);
worker.onmessage = (e) => this.handleWorkerMessage(worker, e);
this.workers.push(worker);
this.idleWorkers.push(worker);
}
}
async compute<T>(taskType: string, data: any): Promise<T> {
return new Promise((resolve, reject) => {
const task: QueuedTask = {
type: taskType,
data,
resolve,
reject
};
const worker = this.idleWorkers.pop();
if (worker) {
this.runTask(worker, task);
} else {
this.taskQueue.push(task);
}
});
}
private runTask(worker: Worker, task: QueuedTask): void {
(worker as any).currentTask = task;
worker.postMessage({ type: task.type, data: task.data });
}
private handleWorkerMessage(worker: Worker, event: MessageEvent): void {
const task = (worker as any).currentTask as QueuedTask;
(worker as any).currentTask = null;
if (event.data.error) {
task.reject(new Error(event.data.error));
} else {
task.resolve(event.data.result);
}
// Process next queued task
const nextTask = this.taskQueue.shift();
if (nextTask) {
this.runTask(worker, nextTask);
} else {
this.idleWorkers.push(worker);
}
}
}
interface QueuedTask {
type: string;
data: any;
resolve: (value: any) => void;
reject: (error: Error) => void;
}
CLS (Cumulative Layout Shift) Prevention
Layout Shift Detection
// CLS measurement and prevention
class CLSOptimizer {
private shifts: LayoutShiftEntry[] = [];
private sessionWindow: LayoutShiftEntry[] = [];
private sessionStartTime = 0;
private sessionValue = 0;
private maxSessionValue = 0;
startObserving(): void {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries() as LayoutShift[];
for (const entry of entries) {
// Ignore user-initiated shifts
if (entry.hadRecentInput) continue;
this.recordShift(entry);
}
});
observer.observe({ type: 'layout-shift', buffered: true });
}
private recordShift(entry: LayoutShift): void {
const shiftEntry: LayoutShiftEntry = {
value: entry.value,
startTime: entry.startTime,
sources: entry.sources?.map(source => ({
node: source.node,
previousRect: source.previousRect,
currentRect: source.currentRect,
element: this.identifyElement(source.node)
})) || []
};
this.shifts.push(shiftEntry);
// Update session window (5 second max, 1 second gap)
if (
this.sessionWindow.length === 0 ||
entry.startTime - this.sessionStartTime > 5000 ||
entry.startTime - this.sessionWindow[this.sessionWindow.length - 1].startTime > 1000
) {
// Start new session
this.sessionWindow = [shiftEntry];
this.sessionStartTime = entry.startTime;
this.sessionValue = entry.value;
} else {
// Continue session
this.sessionWindow.push(shiftEntry);
this.sessionValue += entry.value;
}
// Track max session value (CLS)
if (this.sessionValue > this.maxSessionValue) {
this.maxSessionValue = this.sessionValue;
}
}
getCLS(): CLSReport {
// Find the worst shift sources
const worstShifts = [...this.shifts]
.sort((a, b) => b.value - a.value)
.slice(0, 5);
return {
value: this.maxSessionValue,
totalShifts: this.shifts.length,
worstSources: worstShifts.flatMap(s => s.sources),
recommendations: this.generateRecommendations(worstShifts)
};
}
private identifyElement(node: Node | null): string {
if (!node || !(node instanceof Element)) return 'unknown';
let identifier = node.tagName.toLowerCase();
if (node.id) {
identifier += `#${node.id}`;
} else if (node.className) {
identifier += `.${node.className.split(' ')[0]}`;
}
// Add parent context
if (node.parentElement) {
const parent = node.parentElement.tagName.toLowerCase();
identifier = `${parent} > ${identifier}`;
}
return identifier;
}
private generateRecommendations(worstShifts: LayoutShiftEntry[]): CLSRecommendation[] {
const recommendations: CLSRecommendation[] = [];
for (const shift of worstShifts) {
for (const source of shift.sources) {
const element = source.element;
const prevRect = source.previousRect;
const currRect = source.currentRect;
// Image without dimensions
if (element.includes('img') && prevRect.width === 0 && currRect.width > 0) {
recommendations.push({
element,
issue: 'Image loaded without reserved space',
fix: 'Add width and height attributes, use aspect-ratio CSS',
priority: 'high'
});
}
// Dynamic content injection
if (prevRect.height === 0 && currRect.height > 0) {
recommendations.push({
element,
issue: 'Content dynamically injected',
fix: 'Reserve space with min-height or skeleton placeholder',
priority: 'high'
});
}
// Font swap causing shift
if (element.includes('text') || element.includes('p') || element.includes('h')) {
recommendations.push({
element,
issue: 'Possible font swap causing shift',
fix: 'Use font-display: optional or size-adjust',
priority: 'medium'
});
}
}
}
return recommendations;
}
}
interface LayoutShiftEntry {
value: number;
startTime: number;
sources: ShiftSource[];
}
interface ShiftSource {
node: Node | null;
previousRect: DOMRectReadOnly;
currentRect: DOMRectReadOnly;
element: string;
}
interface CLSReport {
value: number;
totalShifts: number;
worstSources: ShiftSource[];
recommendations: CLSRecommendation[];
}
interface CLSRecommendation {
element: string;
issue: string;
fix: string;
priority: 'high' | 'medium' | 'low';
}
Layout Shift Prevention Techniques
// Techniques for preventing layout shifts
class LayoutShiftPreventer {
// Reserve space for images
createResponsiveImageWithSpace(config: ImageConfig): string {
// Calculate aspect ratio
const aspectRatio = (config.height / config.width) * 100;
return `
<div class="img-container" style="
position: relative;
width: 100%;
padding-bottom: ${aspectRatio}%;
">
<img
src="${config.src}"
srcset="${config.srcset}"
sizes="${config.sizes}"
alt="${config.alt}"
width="${config.width}"
height="${config.height}"
loading="${config.loading || 'lazy'}"
decoding="async"
style="
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
"
/>
</div>
`;
}
// Font loading without CLS
generateFontLoadingCSS(fonts: FontConfig[]): string {
let css = '';
for (const font of fonts) {
// Calculate size-adjust to match fallback metrics
const sizeAdjust = this.calculateSizeAdjust(font);
css += `
@font-face {
font-family: '${font.family}';
src: url('${font.src}') format('${font.format}');
font-weight: ${font.weight || 'normal'};
font-style: ${font.style || 'normal'};
font-display: optional;
size-adjust: ${sizeAdjust}%;
ascent-override: ${font.ascentOverride || 'normal'};
descent-override: ${font.descentOverride || 'normal'};
line-gap-override: ${font.lineGapOverride || 'normal'};
}
`;
}
return css;
}
private calculateSizeAdjust(font: FontConfig): number {
// Font-specific adjustments (simplified)
const adjustments: Record<string, number> = {
'inter': 107,
'roboto': 100,
'open-sans': 105,
'lato': 97,
'arial': 100
};
return adjustments[font.family.toLowerCase()] || 100;
}
// Skeleton placeholder for async content
createSkeletonPlaceholder(config: SkeletonConfig): string {
return `
<div
class="skeleton-placeholder"
style="
width: ${config.width || '100%'};
height: ${config.height};
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
border-radius: ${config.borderRadius || '4px'};
"
aria-hidden="true"
></div>
<style>
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
`;
}
// Transform animations instead of layout-triggering properties
createSafeAnimation(config: AnimationConfig): string {
// Use compositor-only properties
const safeProperties = ['transform', 'opacity', 'filter'];
// Validate animation doesn't use layout-triggering properties
const unsafeProperties = ['width', 'height', 'top', 'left', 'right', 'bottom', 'margin', 'padding'];
for (const keyframe of config.keyframes) {
for (const prop of Object.keys(keyframe)) {
if (unsafeProperties.includes(prop)) {
console.warn(
`Animation uses layout-triggering property: ${prop}. ` +
`Consider using transform instead.`
);
}
}
}
return `
@keyframes ${config.name} {
${config.keyframes.map((kf, i) => `
${(i / (config.keyframes.length - 1)) * 100}% {
${Object.entries(kf).map(([k, v]) => `${k}: ${v};`).join('\n')}
}
`).join('\n')}
}
.${config.className} {
animation: ${config.name} ${config.duration}ms ${config.easing || 'ease'};
will-change: ${safeProperties.filter(p => config.keyframes.some(kf => p in kf)).join(', ')};
}
`;
}
}
interface FontConfig {
family: string;
src: string;
format: string;
weight?: string | number;
style?: string;
ascentOverride?: string;
descentOverride?: string;
lineGapOverride?: string;
}
interface SkeletonConfig {
width?: string;
height: string;
borderRadius?: string;
}
interface AnimationConfig {
name: string;
className: string;
duration: number;
easing?: string;
keyframes: Record<string, string | number>[];
}
// React component for CLS-safe dynamic content
function CLSSafeContent({
children,
minHeight,
aspectRatio,
fallback
}: {
children: React.ReactNode;
minHeight?: string;
aspectRatio?: string;
fallback?: React.ReactNode;
}) {
const [isLoaded, setIsLoaded] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
const [measuredHeight, setMeasuredHeight] = React.useState<number | null>(null);
React.useEffect(() => {
if (containerRef.current) {
const observer = new ResizeObserver(entries => {
const entry = entries[0];
if (entry && !isLoaded) {
setMeasuredHeight(entry.contentRect.height);
}
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}
}, [isLoaded]);
return (
<div
ref={containerRef}
style={{
minHeight: measuredHeight ? `${measuredHeight}px` : minHeight,
aspectRatio: aspectRatio,
contain: 'layout'
}}
>
{isLoaded ? children : fallback}
</div>
);
}
Performance Monitoring Pipeline
// Unified Core Web Vitals monitoring
class CoreWebVitalsMonitor {
private metrics: Map<string, MetricValue> = new Map();
async collectMetrics(): Promise<WebVitalsReport> {
const [lcp, inp, cls, fcp, ttfb] = await Promise.all([
this.getLCP(),
this.getINP(),
this.getCLS(),
this.getFCP(),
this.getTTFB()
]);
return {
lcp,
inp,
cls,
fcp,
ttfb,
timestamp: Date.now(),
url: window.location.href,
deviceType: this.getDeviceType(),
connectionType: this.getConnectionType(),
navigationType: this.getNavigationType()
};
}
private async getLCP(): Promise<MetricValue> {
return new Promise(resolve => {
new PerformanceObserver(list => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1] as LargestContentfulPaint;
resolve({
value: lastEntry.renderTime || lastEntry.loadTime,
rating: this.getRating('LCP', lastEntry.renderTime || lastEntry.loadTime),
attribution: {
element: lastEntry.element?.tagName,
url: lastEntry.url,
size: lastEntry.size
}
});
}).observe({ type: 'largest-contentful-paint', buffered: true });
});
}
private getRating(metric: string, value: number): 'good' | 'needs-improvement' | 'poor' {
const thresholds: Record<string, [number, number]> = {
'LCP': [2500, 4000],
'INP': [200, 500],
'CLS': [0.1, 0.25],
'FCP': [1800, 3000],
'TTFB': [800, 1800]
};
const [good, poor] = thresholds[metric];
if (value <= good) return 'good';
if (value <= poor) return 'needs-improvement';
return 'poor';
}
// Send metrics to analytics
async reportMetrics(report: WebVitalsReport): Promise<void> {
// Use sendBeacon for reliability
const payload = JSON.stringify(report);
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/vitals', payload);
} else {
await fetch('/api/vitals', {
method: 'POST',
body: payload,
keepalive: true
});
}
}
}
interface WebVitalsReport {
lcp: MetricValue;
inp: MetricValue;
cls: MetricValue;
fcp: MetricValue;
ttfb: MetricValue;
timestamp: number;
url: string;
deviceType: string;
connectionType: string;
navigationType: string;
}
interface MetricValue {
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
attribution?: Record<string, any>;
}
Key Takeaways
- LCP optimization requires fast TTFB, early resource discovery, and optimized rendering
- Preload hints with fetchpriority="high" can dramatically improve LCP resource loading
- INP has three components: input delay, processing time, and presentation delay
- Yielding to main thread between long tasks improves input responsiveness
- CLS prevention requires reserving space for dynamic content before it loads
- Font metrics (size-adjust, ascent-override) can eliminate font-swap layout shifts
- Compositor-only animations (transform, opacity) don't cause layout shifts
- Continuous monitoring with attribution data enables targeted optimization
What did you think?