Steven's Knowledge
Performance

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

MetricFull NameMeasuresGoodNeeds ImprovementPoor
TTFBTime to First ByteServer response≤800ms800ms–1800ms>1800ms
FCPFirst Contentful PaintFirst content render≤1.8s1.8s–3.0s>3.0s
LCPLargest Contentful PaintLargest content render≤2.5s2.5s–4.0s>4.0s
TBTTotal Blocking TimeMain thread blocking≤200ms200ms–600ms>600ms
INPInteraction to Next PaintInteraction responsiveness≤200ms200ms–500ms>500ms
CLSCumulative Layout ShiftVisual stability≤0.10.1–0.25>0.25
FIDFirst Input DelayFirst input latency≤100ms100ms–300ms>300ms
TTITime to InteractiveTime to interactivity≤3.8s3.8s–7.3s>7.3s
SISpeed IndexVisual loading speed≤3.4s3.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

RatingThresholdDescription
Good≤800msServer responds quickly
Needs Improvement800ms–1800msRoom for optimization
Poor>1800msSeverely 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

RatingThresholdDescription
Good≤1.8sUser sees content quickly
Needs Improvement1.8s–3.0sNoticeable waiting time
Poor>3.0sUsers 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: swap to 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

RatingThresholdDescription
Good≤2.5sMain content renders quickly
Needs Improvement2.5s–4.0sUser experience is affected
Poor>4.0sSeverely 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

RatingThresholdDescription
Good≤200msMain thread is idle, responds promptly
Needs Improvement200ms–600msPerceptible jank exists
Poor>600msInteractions 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() or setTimeout to 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

RatingThresholdDescription
Good≤200msInteractions feel instant
Needs Improvement200ms–500msUser perceives delay
Poor>500msUI feels laggy and unresponsive

Three Phases of INP

User Interaction → [Input Delay] → [Processing Time] → [Presentation Delay] → Next Frame Rendered
  1. Input Delay: The main thread is busy with other tasks and cannot process the event immediately
  2. Processing Time: The time event handler functions take to execute
  3. 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 startTransition and useDeferredValue to 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: auto to 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

RatingThresholdDescription
Good≤0.1Stable layout, no unexpected jumps
Needs Improvement0.1–0.25Occasional perceptible shifts
Poor>0.25Frequent 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/height or aspect-ratio on <img> and <video>
  • Reserve space for ad slots, embeds, and dynamic areas (min-height)
  • Use font-display: swap with size-adjust to reduce font swap shifts
  • Use transform animations for dynamic content instead of changing geometric properties
  • Insert new content below the viewport or use overlay positioning
  • Use contain: layout to 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

RatingThresholdDescription
Good≤100msInteraction feels instant
Needs Improvement100ms–300msSlight perceptible delay
Poor>300msObvious 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

RatingThresholdDescription
Good≤3.8sQuickly reaches interactive state
Needs Improvement3.8s–7.3sUsers may find the page unresponsive when trying to interact
Poor>7.3sPage 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

RatingThresholdDescription
Good≤3.4sVisual content fills quickly
Needs Improvement3.4s–5.8sLoading process feels slow
Poor>5.8sPage 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:

ToolMeasurable MetricsUse Case
LighthouseTTFB, FCP, LCP, TBT, CLS, SICI/CD integration, development audits
Chrome DevTools (Performance)All metrics + detailed timelineLocal debugging, frame-by-frame analysis
WebPageTestAll metrics + waterfall charts + video replayMulti-region, multi-device testing
PageSpeed InsightsCore Web Vitals + optimization suggestionsQuick 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 / ServiceHighlights
web-vitals libraryGoogle's official lightweight library for collecting Core Web Vitals
Chrome UX Report (CrUX)Real user dataset provided by Google
Sentry PerformanceUnified error tracking + performance monitoring
Datadog RUMFull-stack observability platform

Optimization Priority

Different business scenarios call for different metric priorities:

ScenarioKey MetricsRationale
Content / News sitesLCP, CLSCore user behavior is reading — content must load fast and remain stable
E-commerce platformsLCP, INP, CLSProducts must display quickly; interactions (add to cart, filters) must be smooth
SaaS / Web appsINP, TBT, TTIHigh-frequency interactions — response speed determines user experience
Landing / Marketing pagesFCP, LCP, SIMostly first-time visits — visual information must be delivered quickly
Form / Tool pagesINP, FID, CLSHeavy 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.

On this page