Performance Metrics
Frontend performance metrics system, rating thresholds, and targeted optimization strategies
Frontend Performance Metrics
Performance metrics are quantitative standards for measuring user experience. Understanding the meaning, rating levels, and optimization methods of each metric is the foundation for systematically improving frontend performance.
Metrics Overview
| Metric | Full Name | Measures | Good | Needs Improvement | Poor |
|---|---|---|---|---|---|
| TTFB | Time to First Byte | Server response | ≤800ms | 800ms–1800ms | >1800ms |
| FCP | First Contentful Paint | First content render | ≤1.8s | 1.8s–3.0s | >3.0s |
| LCP | Largest Contentful Paint | Largest content render | ≤2.5s | 2.5s–4.0s | >4.0s |
| TBT | Total Blocking Time | Main thread blocking | ≤200ms | 200ms–600ms | >600ms |
| INP | Interaction to Next Paint | Interaction responsiveness | ≤200ms | 200ms–500ms | >500ms |
| CLS | Cumulative Layout Shift | Visual stability | ≤0.1 | 0.1–0.25 | >0.25 |
| FID | First Input Delay | First input latency | ≤100ms | 100ms–300ms | >300ms |
| TTI | Time to Interactive | Time to interactivity | ≤3.8s | 3.8s–7.3s | >7.3s |
| SI | Speed Index | Visual loading speed | ≤3.4s | 3.4s–5.8s | >5.8s |
Core Web Vitals are Google's key metrics for measuring real user experience. The current set includes LCP, INP, and CLS, which directly impact search rankings. INP officially replaced FID as a Core Web Vital in March 2024.
TTFB — Time to First Byte
Measures the time from when a user initiates a request to when the browser receives the first byte of the response. It reflects server processing capability and network latency.
Rating Thresholds
| Rating | Threshold | Description |
|---|---|---|
| Good | ≤800ms | Server responds quickly |
| Needs Improvement | 800ms–1800ms | Room for optimization |
| Poor | >1800ms | Severely impacts all downstream metrics |
Common Bottlenecks
- Overly complex server-side logic or unoptimized database queries
- No CDN usage, users far from origin server
- Lack of server-side caching, full rendering on every request
- High DNS resolution and TLS handshake overhead
- Insufficient server resources (CPU, memory)
Optimization Strategies
# 1. Use CDN with edge caching
location / {
proxy_cache_valid 200 10m;
proxy_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
}// 2. Implement server-side caching (Node.js example)
import { LRUCache } from 'lru-cache';
const pageCache = new LRUCache<string, string>({
max: 500,
ttl: 1000 * 60 * 5, // 5 minutes
});
app.get('*', (req, res) => {
const cached = pageCache.get(req.url);
if (cached) {
return res.send(cached);
}
const html = renderPage(req.url);
pageCache.set(req.url, html);
res.send(html);
});Key Strategies:
- Use a CDN to distribute content to edge nodes close to users
- Implement caching for pages and API responses (Redis / in-memory cache / HTTP cache)
- Optimize database queries and add appropriate indexes
- Use HTTP/2 or HTTP/3 to reduce connection overhead
- Use
<link rel="preconnect">to establish connections early - Adopt Streaming SSR to start sending content as early as possible
- Streamline server-side middleware chains
FCP — First Contentful Paint
Measures the time from when a page starts loading to when the first text, image, SVG, or other content element is rendered on screen.
Rating Thresholds
| Rating | Threshold | Description |
|---|---|---|
| Good | ≤1.8s | User sees content quickly |
| Needs Improvement | 1.8s–3.0s | Noticeable waiting time |
| Poor | >3.0s | Users may abandon the page |
Common Bottlenecks
- Render-blocking CSS and JavaScript
- Unoptimized font loading causing invisible text (FOIT)
- Slow server response (high TTFB)
- Too many unoptimized resources in the document head
Optimization Strategies
<!-- 1. Eliminate render-blocking resources -->
<head>
<!-- Inline critical CSS -->
<style>
/* Above-the-fold critical styles */
body { margin: 0; font-family: system-ui, sans-serif; }
.header { height: 64px; background: #fff; }
.hero { min-height: 50vh; }
</style>
<!-- Async load non-critical CSS -->
<link rel="preload" href="/styles/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<!-- Defer JS loading -->
<script defer src="/app.js"></script>
</head>/* 2. Optimize font loading — avoid FOIT */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* Show fallback font first, swap when loaded */
}Key Strategies:
- Inline Critical CSS and async-load remaining styles
- Remove or defer render-blocking JavaScript
- Use
font-display: swapto prevent invisible text - Preload critical resources with
<link rel="preload"> - Reduce server response time (optimize TTFB)
- Use SSR or SSG to deliver above-the-fold content as soon as possible
LCP — Largest Contentful Paint
Measures the time it takes for the largest visible content element in the viewport (images, videos, large text blocks, etc.) to finish rendering. This is one of the Core Web Vitals.
Rating Thresholds
| Rating | Threshold | Description |
|---|---|---|
| Good | ≤2.5s | Main content renders quickly |
| Needs Improvement | 2.5s–4.0s | User experience is affected |
| Poor | >4.0s | Severely impacts user retention and SEO |
LCP Candidate Elements
<img>elements<image>elements inside<svg><video>poster images- Elements with
background-image: url()loaded via CSS - Block-level elements containing text nodes
Common Bottlenecks
- Large, unoptimized hero images
- Client-side rendering (CSR) delaying content appearance
- Improper resource loading priorities
- Font loading blocking text rendering
- Slow third-party resources blocking the critical path
Optimization Strategies
<!-- 1. Preload the LCP image resource -->
<head>
<link rel="preload" as="image" href="/hero.webp"
fetchpriority="high">
</head>
<!-- 2. Use fetchpriority to boost critical image priority -->
<img src="/hero.webp" alt="Hero"
fetchpriority="high"
decoding="async"
width="1200" height="600"><!-- 3. Use modern image formats + responsive images -->
<picture>
<source srcset="/hero.avif" type="image/avif">
<source srcset="/hero.webp" type="image/webp">
<img src="/hero.jpg" alt="Hero"
fetchpriority="high"
width="1200" height="600">
</picture>// 4. Avoid client-side waterfalls — use SSR or data prefetching
// Next.js example
export async function getServerSideProps() {
const data = await fetchHeroContent();
return { props: { data } };
}
// Or use React Server Components
async function HeroSection() {
const data = await fetchHeroContent(); // Fetched directly on the server
return <section><img src={data.imageUrl} fetchPriority="high" /></section>;
}Key Strategies:
- Preload LCP resources and set
fetchpriority="high" - Use modern image formats like AVIF/WebP to reduce file size
- Never use
loading="lazy"on the LCP element - Use SSR/SSG to reduce client-side rendering delays
- Optimize TTFB and the critical resource loading chain
- Serve static assets via CDN
- Ensure the LCP element doesn't depend on JavaScript to render
TBT — Total Blocking Time
Measures the total time the main thread is blocked by long tasks (>50ms) between FCP and TTI. TBT is a lab metric that strongly correlates with real-user INP experience.
Rating Thresholds
| Rating | Threshold | Description |
|---|---|---|
| Good | ≤200ms | Main thread is idle, responds promptly |
| Needs Improvement | 200ms–600ms | Perceptible jank exists |
| Poor | >600ms | Interactions are severely blocked |
Common Bottlenecks
- Large amounts of synchronous JavaScript execution
- Oversized third-party scripts (analytics, ads, chat widgets, etc.)
- Complex component initialization and hydration
- Heavy DOM manipulation on the main thread
Optimization Strategies
// 1. Break up long tasks — yield to the main thread
function yieldToMain(): Promise<void> {
return new Promise((resolve) => {
if ('scheduler' in globalThis && 'yield' in scheduler) {
scheduler.yield().then(resolve);
} else {
setTimeout(resolve, 0);
}
});
}
async function processLargeList(items: Item[]) {
const CHUNK_SIZE = 50;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
processChunk(chunk);
// Yield to main thread after each chunk
await yieldToMain();
}
}// 2. Move compute-intensive tasks to a Web Worker
// worker.ts
self.onmessage = (e: MessageEvent) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage(data);
worker.onmessage = (e) => {
updateUI(e.data);
};// 3. Defer non-critical third-party scripts
function loadThirdPartyAfterInteraction() {
const events = ['scroll', 'click', 'touchstart', 'keydown'];
const load = () => {
events.forEach((e) => window.removeEventListener(e, load));
// Only load after user's first interaction
import('./analytics').then((m) => m.init());
import('./chat-widget').then((m) => m.init());
};
events.forEach((e) => window.addEventListener(e, load, { once: true }));
}Key Strategies:
- Break up Long Tasks, keeping each task under 50ms
- Use
scheduler.yield()orsetTimeoutto yield to the main thread - Offload CPU-intensive computation to Web Workers
- Defer non-critical third-party scripts
- Code-split to reduce initial JavaScript size
- Reduce hydration overhead (use Selective Hydration or Islands Architecture)
INP — Interaction to Next Paint
Measures the latency from user interaction (click, keypress, tap) to the browser rendering the next frame. INP considers all interactions throughout the page lifecycle and uses the 98th percentile value as the final score. This is one of the Core Web Vitals.
Rating Thresholds
| Rating | Threshold | Description |
|---|---|---|
| Good | ≤200ms | Interactions feel instant |
| Needs Improvement | 200ms–500ms | User perceives delay |
| Poor | >500ms | UI feels laggy and unresponsive |
Three Phases of INP
User Interaction → [Input Delay] → [Processing Time] → [Presentation Delay] → Next Frame Rendered- Input Delay: The main thread is busy with other tasks and cannot process the event immediately
- Processing Time: The time event handler functions take to execute
- Presentation Delay: The time the browser takes to calculate styles, layout, and paint
Common Bottlenecks
- Main thread blocked by long tasks, causing high Input Delay
- Event handlers performing expensive operations (synchronous network requests, heavy DOM manipulation)
- Triggering Layout Thrashing
- Large-scale re-renders in frameworks like React
Optimization Strategies
// 1. Reduce Input Delay — break up long tasks (same as TBT optimization)
// 2. Reduce Processing Time — optimize event handlers
function handleSearch(query: string) {
// Bad: synchronously filter a large list
// const results = hugeList.filter(item => item.name.includes(query));
// setResults(results);
// Good: use startTransition to lower priority
startTransition(() => {
const results = hugeList.filter(item => item.name.includes(query));
setResults(results);
});
}
// Use useDeferredValue to defer non-critical updates
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const results = useMemo(
() => hugeList.filter(item => item.name.includes(deferredQuery)),
[deferredQuery]
);
return <List items={results} />;
}// 3. Reduce Presentation Delay — avoid forced synchronous layouts
// Bad: interleaved reads and writes trigger layout thrashing
elements.forEach((el) => {
const height = el.offsetHeight; // Forces layout
el.style.height = height * 2 + 'px'; // Write
});
// Good: batch reads then batch writes
const heights = elements.map((el) => el.offsetHeight);
elements.forEach((el, i) => {
el.style.height = heights[i] * 2 + 'px';
});// 4. Debounce/throttle frequently fired events
import { useMemo } from 'react';
function useThrottle<T extends (...args: any[]) => void>(fn: T, ms: number) {
return useMemo(() => {
let lastCall = 0;
return (...args: Parameters<T>) => {
const now = Date.now();
if (now - lastCall >= ms) {
lastCall = now;
fn(...args);
}
};
}, [fn, ms]);
}Key Strategies:
- Break up long tasks to keep the main thread idle and reduce Input Delay
- Use
startTransitionanduseDeferredValueto prioritize updates - Avoid forced synchronous layouts (Layout Thrashing)
- Use virtual lists for rendering large datasets (
react-window/@tanstack/virtual) - Debounce/throttle frequently fired events
- Use CSS
content-visibility: autoto reduce rendering cost for off-screen elements
CLS — Cumulative Layout Shift
Measures the cumulative score of all unexpected layout shifts that occur throughout the page's entire lifecycle. This is one of the Core Web Vitals.
Rating Thresholds
| Rating | Threshold | Description |
|---|---|---|
| Good | ≤0.1 | Stable layout, no unexpected jumps |
| Needs Improvement | 0.1–0.25 | Occasional perceptible shifts |
| Poor | >0.25 | Frequent layout jumps, poor experience |
How It's Calculated
Layout Shift Score = Impact Fraction × Distance Fraction- Impact Fraction: The proportion of the viewport area affected by unstable elements
- Distance Fraction: The ratio of the maximum distance unstable elements have moved to the viewport size
Common Bottlenecks
- Images and videos without predefined dimensions
- Dynamically injected ads, banners, or embedded content
- Web font loading causing text size changes (FOUT)
- Dynamically injected DOM elements pushing existing content
- Async-loaded content changing page height
Optimization Strategies
<!-- 1. Always specify dimensions for images and videos -->
<img src="/photo.webp" alt="Photo"
width="800" height="600"
style="aspect-ratio: 800/600; width: 100%; height: auto;">
<!-- 2. Reserve space for embedded content -->
<div style="aspect-ratio: 16/9; width: 100%; background: #f0f0f0;">
<iframe src="https://www.youtube.com/embed/xxx"
style="width: 100%; height: 100%;"
loading="lazy"></iframe>
</div>/* 3. Reserve minimum height for dynamic content */
.ad-slot {
min-height: 250px;
background: #f5f5f5;
}
.notification-bar {
/* Use transform instead of changing top/height — doesn't trigger layout shift */
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.notification-bar.visible {
transform: translateY(0);
}/* 4. Use CSS contain to limit layout impact scope */
.card {
contain: layout style;
}
/* 5. Font optimization — use size-adjust to reduce font swap shifts */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap;
size-adjust: 105%; /* Adjust fallback font size to match */
ascent-override: 90%;
descent-override: 20%;
line-gap-override: 0%;
}// 6. Use CSS animations instead of layout changes for dynamic content insertion
function Toast({ message }: { message: string }) {
return (
// Use fixed/absolute positioning to avoid affecting document flow
<div style={{
position: 'fixed',
bottom: '20px',
right: '20px',
animation: 'slideIn 0.3s ease',
}}>
{message}
</div>
);
}Key Strategies:
- Always set
width/heightoraspect-ratioon<img>and<video> - Reserve space for ad slots, embeds, and dynamic areas (
min-height) - Use
font-display: swapwithsize-adjustto reduce font swap shifts - Use
transformanimations for dynamic content instead of changing geometric properties - Insert new content below the viewport or use overlay positioning
- Use
contain: layoutto isolate layout impact - Avoid dynamically inserting elements above existing content
FID — First Input Delay (Replaced by INP)
Measures the delay between a user's first interaction with the page (click, tap, etc.) and when the browser begins processing the event handler.
FID was replaced by INP as a Core Web Vital in March 2024. INP is a more comprehensive metric because it measures all interactions rather than just the first one. However, FID can still be useful for historical data comparison.
Rating Thresholds
| Rating | Threshold | Description |
|---|---|---|
| Good | ≤100ms | Interaction feels instant |
| Needs Improvement | 100ms–300ms | Slight perceptible delay |
| Poor | >300ms | Obvious interaction lag |
Optimization Strategies
FID optimization is the same as optimizing the Input Delay phase of INP:
- Reduce main thread blocking time
- Break up long tasks
- Reduce initial JavaScript execution
- Defer non-critical script loading
TTI — Time to Interactive
Measures the time from when a page starts loading until it becomes fully interactive (main thread is idle, event handlers are registered).
Rating Thresholds
| Rating | Threshold | Description |
|---|---|---|
| Good | ≤3.8s | Quickly reaches interactive state |
| Needs Improvement | 3.8s–7.3s | Users may find the page unresponsive when trying to interact |
| Poor | >7.3s | Page appears loaded but cannot be operated |
Common Bottlenecks
- Oversized JavaScript bundles with long parse and execution times
- Large amounts of synchronous hydration work
- Third-party scripts competing for the main thread
- Waterfall resource loading chains
Optimization Strategies
// 1. Route-level code splitting
const routes = [
{
path: '/dashboard',
component: lazy(() => import('./pages/Dashboard')),
},
{
path: '/settings',
component: lazy(() => import('./pages/Settings')),
},
];
// 2. Progressive Hydration (React 18+ example)
import { hydrateRoot } from 'react-dom/client';
import { Suspense } from 'react';
// Use Suspense to defer hydration for non-critical sections
function App() {
return (
<>
<Header />
<HeroSection />
<Suspense fallback={<Skeleton />}>
<BelowTheFoldContent />
</Suspense>
</>
);
}Key Strategies:
- Aggressive code splitting — only load code needed for the current route
- Use Progressive Hydration or Islands Architecture
- Reduce and defer third-party scripts
- Use Tree Shaking to remove unused code
- Preload critical route code chunks
SI — Speed Index
Measures how quickly the visible area of the page is populated with content, reflecting the user's perception of visual loading speed during the loading process. A lower Speed Index means visual content appears faster.
Rating Thresholds
| Rating | Threshold | Description |
|---|---|---|
| Good | ≤3.4s | Visual content fills quickly |
| Needs Improvement | 3.4s–5.8s | Loading process feels slow |
| Poor | >5.8s | Page remains blank or incomplete for a long time |
Optimization Strategies
Speed Index is influenced by multiple other metrics. Optimization directions include:
- Optimize FCP (speed up first content appearance)
- Optimize LCP (speed up main content appearance)
- Use skeleton screens for progressive content display
- Use SSR to make content visible as early as possible
- Optimize the critical rendering path and reduce blocking resources
Measurement Tools
Lab Tools
Measured in controlled environments, suitable for development and debugging:
| Tool | Measurable Metrics | Use Case |
|---|---|---|
| Lighthouse | TTFB, FCP, LCP, TBT, CLS, SI | CI/CD integration, development audits |
| Chrome DevTools (Performance) | All metrics + detailed timeline | Local debugging, frame-by-frame analysis |
| WebPageTest | All metrics + waterfall charts + video replay | Multi-region, multi-device testing |
| PageSpeed Insights | Core Web Vitals + optimization suggestions | Quick checks + real user data |
Real User Monitoring (RUM)
Collected from real user environments, reflecting actual experience:
// Collect Core Web Vitals using the web-vitals library
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric: { name: string; value: number; id: string }) {
// Report to monitoring platform
navigator.sendBeacon('/api/metrics', JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
url: location.href,
timestamp: Date.now(),
}));
}
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);| Tool / Service | Highlights |
|---|---|
| web-vitals library | Google's official lightweight library for collecting Core Web Vitals |
| Chrome UX Report (CrUX) | Real user dataset provided by Google |
| Sentry Performance | Unified error tracking + performance monitoring |
| Datadog RUM | Full-stack observability platform |
Optimization Priority
Different business scenarios call for different metric priorities:
| Scenario | Key Metrics | Rationale |
|---|---|---|
| Content / News sites | LCP, CLS | Core user behavior is reading — content must load fast and remain stable |
| E-commerce platforms | LCP, INP, CLS | Products must display quickly; interactions (add to cart, filters) must be smooth |
| SaaS / Web apps | INP, TBT, TTI | High-frequency interactions — response speed determines user experience |
| Landing / Marketing pages | FCP, LCP, SI | Mostly first-time visits — visual information must be delivered quickly |
| Form / Tool pages | INP, FID, CLS | Heavy input and interaction — must be immediately responsive without layout shifts |
Recommended optimization order: First ensure TTFB is healthy (it's the foundation for all other metrics), then optimize in order: FCP → LCP → CLS → INP/TBT. Fixing foundational issues often improves multiple metrics simultaneously.