Skip to content
Published on

Web Performance Optimization Complete Guide 2025: Core Web Vitals, Loading Strategies, and Rendering Patterns

Authors

Introduction

Web performance has a direct impact on user experience and business outcomes. According to Google research, when page load time increases from 1 to 3 seconds, bounce rate goes up by 32%; at 5 seconds, it increases by 90%. Amazon reported that every 100ms increase in page load time results in a 1% decrease in sales.

In 2025, Google has strengthened Core Web Vitals as a search ranking factor, and with INP (Interaction to Next Paint) replacing FID, the importance of interaction performance has grown. This article covers everything about web performance: Core Web Vitals optimization, images, JavaScript, CSS, fonts, rendering patterns, caching, networking, monitoring, and Next.js-specific optimizations.


1. Why Performance Matters

1.1 Business Impact

Performance and business metrics:
├── 3s+ load time → 53% bounce (Google)
├── 100ms delay → 1% revenue loss (Amazon)
├── 500ms delay → 20% traffic drop (Google)
├── 1s improvement → 7% conversion boost (Walmart)
└── 2s improvement → 50% bounce rate reduction (COOK)

1.2 SEO and Core Web Vitals

In 2025, Google has confirmed Core Web Vitals as a key search ranking factor.

MetricGoodNeeds ImprovementPoor
LCPUnder 2.5s2.5~4.0sOver 4.0s
INPUnder 200ms200~500msOver 500ms
CLSUnder 0.10.1~0.25Over 0.25

1.3 Performance Budget

Performance budget example:
├── Initial JS load: under 200KB (gzip)
├── Initial CSS load: under 50KB (gzip)
├── Total page weight: under 1.5MB
├── LCP: under 2.5s
├── INP: under 200ms
├── CLS: under 0.1
├── Time to First Byte: under 600ms
└── Request count: under 50

2. Core Web Vitals Deep Dive

2.1 LCP (Largest Contentful Paint)

LCP measures the time it takes for the largest content element in the viewport to render.

LCP target elements:
├── img elements
├── video elements (poster image)
├── Elements with CSS background-image
├── Block-level elements containing text nodes
└── image elements within svg

LCP Optimization Strategies:

<!-- 1. Preload hero image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />

<!-- 2. Set fetchpriority on LCP image -->
<img src="/hero.webp" alt="Hero" fetchpriority="high" loading="eager" />

<!-- 3. Optimize server response time -->
<!-- TTFB target: under 200ms -->
/* 4. Font loading optimization */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: swap;
}

2.2 INP (Interaction to Next Paint)

INP measures the delay from user interactions (clicks, taps, keyboard) to the next paint.

INP optimization strategies:
1. Break up long tasks
   - Split tasks over 50ms into smaller units
   - Use requestIdleCallback, scheduler.yield()

2. Free the main thread
   - Move heavy computations to Web Workers
   - Remove unnecessary synchronous JS

3. Optimize event handlers
   - Apply debounce / throttle
   - Use passive event listeners
// Long task splitting example
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);

    // Give the browser a chance to render
    await scheduler.yield();
  }
}

// scheduler.yield polyfill
if (!('scheduler' in globalThis)) {
  (globalThis as any).scheduler = {
    yield: () => new Promise(resolve => setTimeout(resolve, 0))
  };
}

2.3 CLS (Cumulative Layout Shift)

CLS measures the total of all unexpected layout shifts during page load.

<!-- Prevent CLS: specify image dimensions -->
<img src="/photo.webp" width="800" height="600" alt="Photo" />

<!-- Prevent CLS: set min-height for dynamic content -->
<div style="min-height: 250px;">
  <!-- Ad or dynamic content -->
</div>
/* Prevent CLS: minimize layout shift during font loading */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
  font-display: optional; /* or swap */
  size-adjust: 100.5%;
  ascent-override: 95%;
  descent-override: 22%;
  line-gap-override: 0%;
}

CLS Common Causes and Solutions:

CauseSolution
Images without dimensionsUse width/height or aspect-ratio
Dynamically injected contentReserve space with min-height
Web font FOUT/FOITfont-display: optional + preload
Dynamic adsUse fixed-size containers
Late-loading CSSInline critical CSS

3. Image Optimization

3.1 Next-Gen Formats

Image format comparison (same quality):
├── JPEG: 100KB (baseline)
├── WebP: 70KB (-30%)
├── AVIF: 50KB (-50%)
└── JXL (JPEG XL): 55KB (-45%)

Browser support (2025):
├── WebP: 97%+ (no IE)
├── AVIF: 92%+ (Safari 16.4+)
└── JXL: Removed from Chrome, supported in Safari/Firefox
<!-- picture element for format fallback -->
<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" alt="Hero image" width="1200" height="600" />
</picture>

3.2 Responsive Images

<!-- Responsive images with srcset and sizes -->
<img
  srcset="
    /hero-400w.webp 400w,
    /hero-800w.webp 800w,
    /hero-1200w.webp 1200w,
    /hero-1600w.webp 1600w
  "
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
  src="/hero-800w.webp"
  alt="Hero image"
  width="1200"
  height="600"
  loading="lazy"
  decoding="async"
/>

3.3 Lazy Loading and Blur Placeholders

<!-- Native lazy loading -->
<img src="/photo.webp" loading="lazy" decoding="async" alt="Photo" />
// Blur placeholder implementation
function BlurImage({ src, alt, width, height, blurDataURL }: ImageProps) {
  return (
    <div style={{ position: 'relative', width, height }}>
      {/* Blur placeholder */}
      <img
        src={blurDataURL}
        alt=""
        style={{
          position: 'absolute',
          inset: 0,
          filter: 'blur(20px)',
          transform: 'scale(1.1)',
        }}
      />
      {/* Actual image */}
      <img
        src={src}
        alt={alt}
        width={width}
        height={height}
        loading="lazy"
        decoding="async"
        onLoad={(e) => {
          e.currentTarget.style.opacity = '1';
        }}
        style={{ position: 'relative', opacity: 0, transition: 'opacity 0.3s' }}
      />
    </div>
  );
}

4. JavaScript Optimization

4.1 Code Splitting

// React lazy + Suspense (Route-based splitting)
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
// Conditional dynamic import
async function handleExport() {
  // Load xlsx library only when needed
  const XLSX = await import('xlsx');
  const workbook = XLSX.utils.book_new();
  // ...
}

4.2 Tree Shaking

// Bad: import entire library
import _ from 'lodash';
_.debounce(fn, 300);

// Good: import only what you need
import debounce from 'lodash/debounce';
debounce(fn, 300);

// Better: use native or lightweight alternatives
// lodash.debounce: 1.4KB vs lodash: 72KB
// Bundle analysis with webpack-bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // Next.js config
});

4.3 Bundle Optimization

// webpack splitChunks configuration
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
        },
        common: {
          minChunks: 2,
          priority: -10,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

4.4 Script Loading Strategies

<!-- Normal: blocks parsing -->
<script src="app.js"></script>

<!-- async: download async, execute immediately (may block parsing) -->
<script src="analytics.js" async></script>

<!-- defer: download async, execute after DOM parsing (order preserved) -->
<script src="app.js" defer></script>

<!-- module: same behavior as defer -->
<script type="module" src="app.js"></script>
Script loading timeline:
normal:  [HTML parsing...] [download] [execute] [HTML parsing...]
async:   [HTML parsing......download......] [execute] [HTML parsing...]
defer:   [HTML parsing......download.............] [execute]

5. CSS Optimization

5.1 Critical CSS

<!-- Inline critical CSS -->
<head>
  <style>
    /* Only minimum CSS needed for above-the-fold content */
    body { margin: 0; font-family: system-ui; }
    .header { height: 60px; background: #fff; }
    .hero { height: 400px; display: flex; align-items: center; }
  </style>

  <!-- Load remaining CSS asynchronously -->
  <link rel="preload" href="/styles.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'" />
  <noscript><link rel="stylesheet" href="/styles.css" /></noscript>
</head>

5.2 CSS Containment

/* Limit rendering scope with contain */
.card {
  contain: layout style paint;
  /* Or optimize offscreen elements with content-visibility */
  content-visibility: auto;
  contain-intrinsic-size: 0 300px;
}

/* GPU acceleration hints with will-change */
.animated-element {
  will-change: transform;
  /* Warning: overuse wastes memory */
}

5.3 Remove Unused CSS

// PurgeCSS configuration (postcss.config.js)
module.exports = {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: [
        './src/**/*.{js,jsx,ts,tsx}',
        './public/index.html'
      ],
      defaultExtractor: content =>
        content.match(/[\w-/:]+(?<!:)/g) || [],
      safelist: ['html', 'body', /^data-/]
    })
  ]
};

6. Font Optimization

6.1 font-display Strategy

/* swap: fallback font → web font (FOUT occurs, text shown immediately) */
@font-face {
  font-family: 'MyFont';
  font-display: swap;
  src: url('/fonts/myfont.woff2') format('woff2');
}

/* optional: use web font if fast, keep fallback if slow (minimal CLS) */
@font-face {
  font-family: 'MyFont';
  font-display: optional;
  src: url('/fonts/myfont.woff2') format('woff2');
}

6.2 Font Preloading

<!-- Preload critical fonts -->
<link
  rel="preload"
  href="/fonts/inter-var.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

6.3 Variable Fonts

/* Traditional: separate file per weight (400, 500, 600, 700 = 4 files) */
/* Variable Font: single file for all weights */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-variable.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;
}

/* Usage */
.light { font-weight: 300; }
.regular { font-weight: 400; }
.bold { font-weight: 700; }

6.4 Font Subsetting

# Generate CJK font subset with pyftsubset
# pip install fonttools brotli
pyftsubset NotoSansKR-Regular.otf \
  --text-file=korean-chars.txt \
  --output-file=NotoSansKR-subset.woff2 \
  --flavor=woff2

# Result: 4.5MB → 300KB (with 2,350 Korean characters)

7. Rendering Patterns

7.1 Pattern Comparison Table

PatternTTFBFCPLCPTTISEOUse Cases
CSRFastSlowSlowSlowPoorSPA, Dashboards
SSRSlowFastFastSlowGoodDynamic content, SEO needed
SSGVery fastVery fastVery fastFastExcellentBlogs, docs, marketing
ISRVery fastVery fastVery fastFastExcellentE-commerce, news
Streaming SSRFastVery fastFastFastGoodComplex dynamic pages

7.2 CSR (Client-Side Rendering)

CSR flow:
Browser        Server
  │─ HTML req ────→│
  │←─ Empty HTML ──│
  │─ JS req ──────→│
  │←─ JS bundle ───│
[Parse/Execute]  │─ API req ─────→│
  │←─ Data ────────│
[Render]Display

7.3 SSR (Server-Side Rendering)

SSR flow:
Browser        Server
  │─ HTML req ────→│
  │                │ [Data fetching]
  │                │ [HTML rendering]
  │←─ Full HTML ───│
[Display]  │─ JS req ──────→│
  │←─ JS bundle ───│
[Hydration]Interactive

7.4 Streaming SSR

// Next.js App Router Streaming SSR
import { Suspense } from 'react';

async function SlowComponent() {
  const data = await fetchSlowData(); // Takes 3 seconds
  return <div>{/* render data */}</div>;
}

export default function Page() {
  return (
    <div>
      {/* Rendered immediately */}
      <Header />
      <Hero />

      {/* Streaming: sent progressively when ready */}
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>

      {/* Rendered immediately */}
      <Footer />
    </div>
  );
}

7.5 ISR (Incremental Static Regeneration)

// Next.js ISR
// app/products/[id]/page.tsx
export const revalidate = 3600; // Regenerate every hour

async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

export default ProductPage;

// Generate static paths
export async function generateStaticParams() {
  const products = await getTopProducts(100);
  return products.map(p => ({ id: p.id }));
}

8. Caching Strategies

8.1 HTTP Cache

HTTP cache strategies:
├── Static assets (JS/CSS/images)
Cache-Control: public, max-age=31536000, immutable
   (filename includes hash: app.abc123.js)
├── HTML
Cache-Control: public, max-age=0, must-revalidate
   (always verify with server)
├── API responses
Cache-Control: private, max-age=60, stale-while-revalidate=300
   (60s cache, 5min stale allowed)
└── User-specific data
    Cache-Control: private, no-cache
    (verify with server every time)

8.2 stale-while-revalidate

stale-while-revalidate behavior:
Request 1: [Cache miss]ServerResponse + cache stored
Request 2: [Cache hit, fresh]Immediate response
Request 3: [Cache hit, stale]Immediate response (stale) + background refresh
Request 4: [Cache hit, fresh]Immediate response (updated data)
// SWR library (React)
import useSWR from 'swr';

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher, {
    revalidateOnFocus: true,
    revalidateOnReconnect: true,
    refreshInterval: 30000, // Refresh every 30s
  });

  if (isLoading) return <Skeleton />;
  if (error) return <Error />;
  return <UserCard user={data} />;
}

8.3 Service Worker Cache

// Service Worker caching strategies
// sw.js
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/styles.css',
  '/app.js',
  '/offline.html'
];

// Install: pre-cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache =>
      cache.addAll(STATIC_ASSETS)
    )
  );
});

// Fetch: Cache First + Network Fallback
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      if (cached) return cached;

      return fetch(event.request).then(response => {
        const clone = response.clone();
        caches.open(CACHE_NAME).then(cache =>
          cache.put(event.request, clone)
        );
        return response;
      }).catch(() => {
        return caches.match('/offline.html');
      });
    })
  );
});

8.4 CDN Cache

CDN cache hierarchy:
User[Browser Cache][CDN Edge][CDN Origin Shield][Server]

CDN header examples:
Static assets:
  Cache-Control: public, max-age=31536000, immutable
  CDN-Cache-Control: public, max-age=31536000

Dynamic content:
  Cache-Control: public, max-age=0, must-revalidate
  CDN-Cache-Control: public, max-age=60, stale-while-revalidate=300
  Surrogate-Control: max-age=3600

9. Network Optimization

9.1 Resource Hints

<!-- DNS Prefetch: resolve external domain DNS early -->
<link rel="dns-prefetch" href="//api.example.com" />
<link rel="dns-prefetch" href="//cdn.example.com" />

<!-- Preconnect: DNS + TCP + TLS early connection -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

<!-- Prefetch: preload next-page resources -->
<link rel="prefetch" href="/next-page.js" />

<!-- Preload: prioritize current-page critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/hero.webp" as="image" />

9.2 Priority Hints

<!-- fetchpriority for resource prioritization -->
<!-- Hero image: high priority -->
<img src="/hero.webp" fetchpriority="high" />

<!-- Offscreen image: low priority -->
<img src="/below-fold.webp" fetchpriority="low" loading="lazy" />

<!-- Critical script: high priority -->
<script src="/critical.js" fetchpriority="high"></script>

9.3 HTTP/2 and HTTP/3

HTTP/1.1 vs HTTP/2 vs HTTP/3:
┌──────────────┬──────────────┬──────────────┬──────────────┐
FeatureHTTP/1.1HTTP/2HTTP/3├──────────────┼──────────────┼──────────────┼──────────────┤
MultiplexingNoSupportedSupportedHeader CompNoneHPACKQPACKServer PushNoneSupportedRemovedHOL BlockingTCP level    │ TCP level    │ Solved(QUIC)TransportTCPTCPQUIC(UDP)ConnectionTCP+TLSTCP+TLS0-RTT able   │
└──────────────┴──────────────┴──────────────┴──────────────┘

10. Performance Monitoring

10.1 Lighthouse CI

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v11
        with:
          configPath: './lighthouserc.json'
          uploadArtifacts: true
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "categories:accessibility": ["error", { "minScore": 0.9 }],
        "categories:best-practices": ["warn", { "minScore": 0.9 }],
        "categories:seo": ["error", { "minScore": 0.9 }],
        "first-contentful-paint": ["error", { "maxNumericValue": 2000 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "interactive": ["error", { "maxNumericValue": 5000 }]
      }
    }
  }
}

10.2 Real User Monitoring (RUM)

// Measure and report Web Vitals
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';

function sendToAnalytics(metric: any) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
  });

  // Send asynchronously with Beacon API
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/vitals', body);
  } else {
    fetch('/api/vitals', { body, method: 'POST', keepalive: true });
  }
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);

10.3 Performance Dashboard

Performance monitoring tools:
├── Synthetic Monitoring (Lab Data)
│   ├── Lighthouse CI (automated)
│   ├── WebPageTest (detailed analysis)
│   └── PageSpeed Insights (Google)
├── Real User Monitoring (Field Data)
│   ├── Chrome UX Report (CrUX)
│   ├── web-vitals library
│   └── Commercial: Datadog RUM, New Relic
└── Bundle Analysis
    ├── webpack-bundle-analyzer
    ├── source-map-explorer
    └── bundlephobia.com

11. Next.js-Specific Optimizations

11.1 App Router and Server Components

// Server Component (default - NOT included in JS bundle)
// app/products/page.tsx
async function ProductsPage() {
  // Fetch data directly on server (no API route needed)
  const products = await db.product.findMany();

  return (
    <div>
      <h1>Products</h1>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

// Client Component (only for parts needing interaction)
// components/AddToCart.tsx
'use client';

import { useState } from 'react';

export function AddToCart({ productId }: { productId: string }) {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Add to Cart ({count})
    </button>
  );
}

11.2 Next.js Image Optimization

import Image from 'next/image';

// Auto WebP/AVIF conversion, responsive, lazy loading
function HeroSection() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority  // Set priority for LCP image
      sizes="(max-width: 768px) 100vw, 1200px"
      quality={85}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}

// Remote images
function Avatar({ user }: { user: User }) {
  return (
    <Image
      src={user.avatarUrl}
      alt={user.name}
      width={48}
      height={48}
      loading="lazy"
    />
  );
}

11.3 Next.js Font Optimization

// Auto-optimization with next/font (self-hosted, zero CLS)
import { Inter, Noto_Sans_KR } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});

const notoSansKR = Noto_Sans_KR({
  subsets: ['latin'],
  weight: ['400', '500', '700'],
  display: 'swap',
  variable: '--font-noto-sans-kr',
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko" className={`${inter.variable} ${notoSansKR.variable}`}>
      <body>{children}</body>
    </html>
  );
}

11.4 Route Segment Config

// app/blog/[slug]/page.tsx

// Static generation + ISR (60 seconds)
export const revalidate = 60;

// Or fully static
export const dynamic = 'force-static';

// Or always dynamic
export const dynamic = 'force-dynamic';

// Runtime selection
export const runtime = 'edge'; // Edge Runtime (fast TTFB)
// export const runtime = 'nodejs'; // Node.js Runtime (default)

12. Interview Questions

Basic Concepts

Q1. Explain the three Core Web Vitals metrics.
  1. LCP (Largest Contentful Paint): Time for the largest content element in the viewport to render. Target: under 2.5s. Measures loading performance.

  2. INP (Interaction to Next Paint): Delay from user interaction (click, keyboard, etc.) to the next paint. Target: under 200ms. Measures responsiveness.

  3. CLS (Cumulative Layout Shift): Total of all unexpected layout shifts during page load. Target: under 0.1. Measures visual stability.

Q2. Explain the differences between CSR, SSR, SSG, and ISR.

CSR (Client-Side Rendering): Browser renders with JS. Empty HTML sent, client renders everything.

  • Pros: Low server load, ideal for SPAs
  • Cons: Slow initial load, poor SEO

SSR (Server-Side Rendering): Server generates HTML per request.

  • Pros: Fast FCP, good SEO
  • Cons: Server load, slower TTFB

SSG (Static Site Generation): HTML pre-generated at build time.

  • Pros: Best performance, CDN caching
  • Cons: Build time, limited dynamic content

ISR (Incremental Static Regeneration): SSG + background regeneration.

  • Pros: SSG performance + data freshness
  • Cons: Implementation complexity
Q3. Explain the difference between Code Splitting and Tree Shaking.

Code Splitting: Dividing JS bundles into multiple chunks so only what is needed gets loaded. Split by route (dynamic import) or by component. Reduces initial load time.

Tree Shaking: Build-time optimization that removes unused (dead) code. Analyzes the static structure of ES Modules to remove un-imported exports. Reduces final bundle size.

Difference: Code Splitting is about "when to load" (When). Tree Shaking is about "what to remove" (What).

Q4. How do you improve LCP?
  1. Optimize server response: TTFB under 200ms, use CDN, caching
  2. Preload resources: Preload LCP image + fetchpriority="high"
  3. Image optimization: WebP/AVIF, proper sizing, compression
  4. Remove render blocking: Inline critical CSS, defer JS
  5. Font optimization: font-display: swap, preload
  6. Use SSR/SSG: Send complete HTML from server
  7. Third-party optimization: Lazy load external scripts
Q5. Explain CLS causes and solutions.

Main causes:

  1. Images/videos without dimensions
  2. Dynamically injected content (ads, banners)
  3. Web font loading layout shifts
  4. Late-loading CSS
  5. DOM-manipulating JavaScript

Solutions:

  1. Specify width/height or aspect-ratio CSS on img/video
  2. Reserve space for dynamic content with min-height
  3. font-display: optional, font preloading
  4. Inline critical CSS
  5. Use transform animations instead of top/left

Advanced Questions

Q6. Explain Service Worker caching strategies.
  1. Cache First: Check cache, fallback to network. Ideal for static assets.
  2. Network First: Try network, fallback to cache on failure. Ideal for API responses.
  3. Stale While Revalidate: Return cached response immediately + refresh in background. Ideal for frequently updated assets.
  4. Cache Only: Use cache exclusively. For offline assets.
  5. Network Only: Use network exclusively. For real-time data.

Criteria: Choose based on data freshness requirements and offline support needs.

Q7. Explain HTTP cache headers.
  • Cache-Control: Core caching policy header

    • max-age: Cache validity period (seconds)
    • no-cache: Server validation required each time
    • no-store: Never cache
    • public/private: CDN cacheability
    • immutable: Resource never changes
    • stale-while-revalidate: Allow stale response period
  • ETag: Resource version identifier. Used with conditional requests (If-None-Match).

  • Last-Modified: Last modification time. Used with If-Modified-Since.

Recommended strategy:

  • Static assets (with hash): max-age=31536000, immutable
  • HTML: no-cache or max-age=0, must-revalidate
Q8. Describe a comprehensive image optimization strategy.
  1. Format: Use WebP/AVIF, picture element for fallback
  2. Size: Responsive images (srcset + sizes), match actual display size
  3. Compression: Quality 75-85, adjust by use case
  4. Loading: LCP image uses eager + fetchpriority="high", rest use lazy
  5. Framework: Leverage auto-optimization tools like Next.js Image
  6. CDN: Use image CDNs (Cloudinary, imgix)
  7. Placeholder: Blur or LQIP for improved UX
Q9. How do you optimize INP (Interaction to Next Paint)?
  1. Break long tasks: Split 50ms+ tasks with scheduler.yield() or requestIdleCallback
  2. Free main thread: Move heavy computations to Web Workers
  3. Optimize event handlers: debounce/throttle, passive event listeners
  4. Minimize JS bundle: Code splitting, tree shaking
  5. Virtualization: Virtualize long lists with react-virtuoso
  6. startTransition: Mark non-urgent updates as transitions
  7. React optimization: Use useMemo, useCallback, React.memo appropriately
Q10. Explain how CDNs work and their performance benefits.

How they work:

  1. Cache content on Edge servers (PoPs) distributed globally
  2. Route user DNS requests to the nearest Edge
  3. If Edge has cache, respond immediately; otherwise, fetch from Origin and cache

Performance benefits:

  • Reduced physical distance: Lower RTT (Round Trip Time)
  • Origin server load distribution
  • DDoS protection
  • TLS optimization (TLS termination at Edge)
  • Automatic compression (Brotli, gzip)
  • HTTP/2, HTTP/3 support
Q11. Explain webpack code splitting configuration.

Code splitting with webpack's splitChunks plugin:

  1. Entry Points: Manual splitting via multiple entry points
  2. Dynamic Imports: Dynamic splitting via import()
  3. splitChunks: Automatic splitting rules

Key settings:

  • chunks: 'all' splits both sync and async
  • cacheGroups: Separate vendor (node_modules) and common (shared modules)
  • minSize: Minimum chunk size (default 20KB)
  • maxSize: Maximum chunk size (auto-split)

Benefits: Reduced initial load time, improved cache efficiency, load only needed code.

Q12. Explain the benefits and implementation of Streaming SSR.

Benefits:

  • Reduced TTFB: HTML sent progressively
  • Improved FCP: Display ready parts first
  • Isolate slow data sources: Wrap in Suspense for independent loading
  • Better UX: Show skeleton/loading states immediately

Implementation (Next.js App Router):

  • Use Server Components by default
  • Wrap slow components with Suspense
  • Provide skeleton UI as fallback
  • Automatically streams when data is ready

Key: Do not wait for the entire page to be ready; progressively send parts as they become available.

Q13. Explain the performance benefits of Next.js Server Components.
  1. Zero JS bundle: Server Components are not included in client JS bundle
  2. Direct data access: Access DB/API directly on server (no API routes needed)
  3. Automatic code splitting: Only Client Components are bundled
  4. Streaming: Progressive rendering combined with Suspense
  5. Caching: Server-side caching optimizes repeated requests
  6. Security: Sensitive logic/keys are not exposed to client

Principle: Components without interaction should be Server; only parts needing useState/useEffect should be Client.

Q14. How do you set and enforce a performance budget?

Setting:

  1. Competitor analysis: Measure key competitor performance metrics
  2. User device analysis: Understand average target user device/network
  3. Reflect business goals: Align with conversion rate and bounce rate targets
  4. Specific numbers: JS 200KB, CSS 50KB, LCP 2.5s, INP 200ms, etc.

Enforcement:

  1. Lighthouse CI for automated checks on every PR
  2. Fail build or warn on budget violations
  3. bundlesize or size-limit for bundle size limits
  4. Team dashboard for trend monitoring
  5. Regular budget review and adjustment
Q15. Describe web font optimization best practices.
  1. Variable Fonts: Single file for all weights/styles (fewer files)
  2. Subsetting: Include only needed characters (CJK: 4MB to 300KB)
  3. WOFF2 format: Best compression for web fonts
  4. font-display: swap (show text immediately) or optional (minimal CLS)
  5. preload: Early download critical fonts via link preload
  6. Self-hosting: Host fonts directly instead of Google Fonts (save DNS/connection cost)
  7. size-adjust: Match fallback and web font sizes to prevent CLS

13. Quiz

Q1. What is the "Good" threshold for LCP?

Answer: Under 2.5 seconds

LCP (Largest Contentful Paint) thresholds:

  • Good: Under 2.5s
  • Needs Improvement: 2.5-4.0s
  • Poor: Over 4.0s

LCP measures the time for the largest content element in the viewport to render.

Q2. Which Core Web Vitals metric replaced FID?

Answer: INP (Interaction to Next Paint)

Starting March 2024, FID (First Input Delay) was replaced by INP. While FID only measured the first interaction, INP measures all interactions throughout the entire page lifecycle, providing a more comprehensive responsiveness metric.

Q3. What does "immutable" mean in HTTP cache?

Answer: Indicates the resource will never change, preventing revalidation requests

Cache-Control: immutable tells the browser the resource will not change. This prevents the browser from sending revalidation (304) requests within the max-age period. Ideal for static assets with hashes in filenames (app.abc123.js).

Q4. What is the prerequisite for Tree Shaking to work?

Answer: ES Modules (import/export) must be used

Tree Shaking analyzes the static structure of ES Modules to remove unused exports. CommonJS (require/module.exports) is dynamic and cannot be tree-shaken. The "sideEffects": false setting in package.json is also important.

Q5. Are Next.js Server Components included in the client bundle?

Answer: No, Server Components are NOT included in the client JS bundle

Server Components run only on the server, and only the resulting HTML is sent to the client. This can significantly reduce JS bundle size. Only Client Components with the 'use client' directive are included in the bundle.


14. References

Official Documentation

Measurement Tools

Image Optimization

Performance Resources

Frameworks