45+ Web Performance Interview Questions 2025: Core Web Vitals, Caching & Optimization

·21 min read
web-performancecore-web-vitalsfrontendlighthouseoptimizationjavascriptinterview-preparation

Web performance isn't optional anymore. Google uses Core Web Vitals as a ranking factor. Users abandon slow sites. And interviewers expect frontend developers to understand optimization beyond "it should load fast."

This guide covers the performance concepts that come up in frontend interviews—from Core Web Vitals to caching strategies to framework-specific patterns.

Table of Contents

  1. Core Web Vitals Overview Questions
  2. LCP Optimization Questions
  3. INP and Interactivity Questions
  4. CLS and Visual Stability Questions
  5. JavaScript Performance Questions
  6. Rendering Performance Questions
  7. Image Optimization Questions
  8. Caching Strategy Questions
  9. Performance Measurement Questions
  10. Framework Performance Questions

Core Web Vitals Overview Questions

Google's Core Web Vitals are the foundation of modern performance discussions and affect SEO rankings.

What are Core Web Vitals?

Core Web Vitals are three metrics that Google uses to measure real user experience on websites. They were introduced as a ranking factor and represent the most important aspects of page performance from a user's perspective.

The three metrics are LCP (Largest Contentful Paint) for loading speed, INP (Interaction to Next Paint) for interactivity, and CLS (Cumulative Layout Shift) for visual stability. Together, they capture whether a page loads quickly, responds to user input promptly, and doesn't shift around unexpectedly.

MetricMeasuresGoodNeeds WorkPoor
LCP (Largest Contentful Paint)Loading speed< 2.5s2.5-4s> 4s
INP (Interaction to Next Paint)Interactivity< 200ms200-500ms> 500ms
CLS (Cumulative Layout Shift)Visual stability< 0.10.1-0.25> 0.25

Why do Core Web Vitals matter for SEO?

Google incorporated Core Web Vitals into its ranking algorithm because they directly correlate with user experience. Sites that fail Core Web Vitals tend to have higher bounce rates and lower conversions. Users leave when pages load slowly or shift unexpectedly.

Beyond SEO, these metrics provide actionable targets for optimization. Rather than vague goals like "make it faster," Core Web Vitals give specific thresholds to aim for and specific areas to improve.

What replaced FID in Core Web Vitals?

INP (Interaction to Next Paint) replaced FID (First Input Delay) in March 2024. While FID only measured the delay of the first interaction, INP measures the responsiveness of all interactions throughout the page lifecycle.

This change was significant because FID could show good scores even if subsequent interactions were slow. INP provides a more complete picture of how responsive a page feels during actual use.


LCP Optimization Questions

LCP is often the most impactful metric to optimize because it directly affects perceived load time.

What is Largest Contentful Paint (LCP)?

LCP measures when the largest visible element finishes rendering in the viewport. This is usually a hero image, heading, or large text block. It represents the point at which users perceive the page as "loaded" even if other elements are still loading.

The target is under 2.5 seconds. Pages that exceed 4 seconds are considered to have poor LCP and may be penalized in search rankings.

What causes poor LCP scores?

Poor LCP typically stems from four main causes: slow server response time (TTFB), render-blocking JavaScript and CSS, slow resource load times for images and fonts, and client-side rendering delays.

Server response time sets a floor for LCP—if the HTML takes 2 seconds to arrive, LCP cannot be under 2 seconds. Render-blocking resources delay when the browser can start rendering. Large images or fonts on slow connections extend the time until the LCP element appears.

How do you improve LCP?

Improving LCP requires addressing the bottleneck in your specific situation. Start by measuring what's causing the delay, then apply the appropriate fix.

For server response time, use a CDN, cache responses, and optimize backend processing. For render-blocking resources, inline critical CSS and defer non-critical stylesheets. For slow images, preload the LCP image and use modern formats.

<!-- Preload critical resources -->
<link rel="preload" href="/hero-image.webp" as="image">
<link rel="preload" href="/critical-font.woff2" as="font" crossorigin>
 
<!-- Inline critical CSS -->
<style>
  .hero { /* critical styles */ }
</style>
 
<!-- Defer non-critical CSS -->
<link rel="preload" href="/styles.css" as="style" onload="this.rel='stylesheet'">

What is the optimal resource loading order for LCP?

The optimal loading order prioritizes the critical rendering path. Start with inlined critical CSS in the head, then preload the LCP image and critical fonts. Defer non-critical CSS using the preload/onload pattern. Use defer for JavaScript to avoid blocking parsing.

<!-- Optimal resource loading order -->
<head>
  <!-- Critical CSS inlined -->
  <style>/* Above-the-fold styles */</style>
 
  <!-- Preload critical assets -->
  <link rel="preload" href="/critical.js" as="script">
 
  <!-- Defer non-critical CSS -->
  <link rel="preload" href="/styles.css" as="style"
        onload="this.rel='stylesheet'">
 
  <!-- Defer JavaScript -->
  <script src="/app.js" defer></script>
</head>

INP and Interactivity Questions

INP measures how responsive your page feels during actual use.

What is Interaction to Next Paint (INP)?

INP measures the delay between any user interaction (click, tap, keypress) and when the browser can paint the visual response. Unlike FID which only measured the first interaction, INP considers all interactions and reports the worst one (with some outlier exclusion).

The target is under 200ms. This means users should see visual feedback within 200ms of any click or tap. Pages exceeding 500ms are considered to have poor interactivity.

What causes poor INP scores?

Poor INP is almost always caused by JavaScript blocking the main thread. When JavaScript is executing, the browser cannot respond to user input. Long-running synchronous operations, too many event listeners, heavy re-renders on interaction, and synchronous operations during handlers all contribute to poor INP.

The main thread handles JavaScript, layout, paint, and user input. Block it with computation and your app feels frozen.

How do you improve INP?

Improving INP requires keeping the main thread free to respond to user input. Break up long-running tasks, move heavy computation to Web Workers, and provide immediate visual feedback before async operations complete.

// Bad: Blocking the main thread
button.addEventListener('click', () => {
  const result = heavyComputation(); // Blocks for 500ms
  updateUI(result);
});
 
// Good: Break up work with scheduler
button.addEventListener('click', async () => {
  // Show immediate feedback
  button.disabled = true;
 
  // Yield to browser between chunks
  const result = await yieldingComputation();
  updateUI(result);
});
 
// Using scheduler.yield() (modern browsers)
async function yieldingComputation() {
  let result = 0;
  for (let i = 0; i < 1000000; i++) {
    result += expensiveStep(i);
    if (i % 10000 === 0) {
      await scheduler.yield(); // Let browser handle events
    }
  }
  return result;
}

CLS and Visual Stability Questions

CLS measures the frustrating experience of content shifting unexpectedly.

What is Cumulative Layout Shift (CLS)?

CLS measures unexpected layout shifts—when visible elements move without user interaction. It quantifies how much content shifts during page load, with a target of under 0.1. Higher values indicate a page where elements jump around, potentially causing users to click the wrong thing.

Nothing frustrates users more than clicking a button that shifts right before they tap. CLS captures this experience as a measurable metric.

What causes poor CLS scores?

Poor CLS typically comes from images without dimensions, ads or embeds without reserved space, web fonts causing text reflow (FOIT/FOUT), and dynamically injected content above existing content.

When the browser doesn't know an element's size ahead of time, it renders with zero height, then shifts everything when the actual content loads.

How do you prevent layout shifts?

Preventing layout shifts requires reserving space for content before it loads. Always specify image dimensions, reserve space for dynamic content like ads, and handle font loading to minimize text reflow.

<!-- Always specify image dimensions -->
<img src="photo.jpg" width="800" height="600" alt="..." />
 
<!-- Or use aspect-ratio in CSS -->
<style>
  .video-container {
    aspect-ratio: 16 / 9;
    width: 100%;
  }
</style>
 
<!-- Reserve space for dynamic content -->
<div class="ad-slot" style="min-height: 250px;">
  <!-- Ad loads here -->
</div>

How do you load fonts without causing layout shifts?

Font loading can cause layout shifts when the fallback font has different metrics than the custom font. Use font-display: swap with size-adjust to match fallback font metrics, minimizing the shift when the custom font loads.

/* Use font-display: swap with size-adjust */
@font-face {
  font-family: 'CustomFont';
  src: url('/font.woff2') format('woff2');
  font-display: swap;
  size-adjust: 105%; /* Match fallback font metrics */
}

font-display values:

ValueBehavior
swapShow fallback immediately, swap when loaded
blockBrief invisible text, then show custom font
fallbackVery brief block, then fallback, late swap ignored
optionalBrowser decides based on connection speed

JavaScript Performance Questions

JavaScript is usually the biggest performance bottleneck in modern web apps.

What is code splitting and why does it matter?

Code splitting divides your JavaScript bundle into smaller chunks that load on demand. Instead of downloading the entire application upfront, users only download code for the current route or feature. This reduces initial load time, improves Time to Interactive, and saves bandwidth.

The key insight is that users rarely need all your code immediately. Route-based splitting ensures dashboard code doesn't load until users navigate to the dashboard.

// Route-based splitting (React)
import { lazy, Suspense } from 'react';
 
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
 
function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}
// Route-based splitting (Angular)
const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard.component')
      .then(m => m.DashboardComponent)
  }
];

What is tree shaking and how does it work?

Tree shaking is a build-time optimization that removes unused code from your bundle. It works by analyzing which exports are actually imported and eliminating the rest. Tree shaking requires ES modules syntax—it doesn't work with CommonJS require.

// Bad: Imports entire library
import _ from 'lodash';
_.debounce(fn, 300);
 
// Good: Imports only what's used (tree-shakeable)
import debounce from 'lodash/debounce';
debounce(fn, 300);
 
// Even better: Use native or lighter alternatives
function debounce(fn, ms) {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), ms);
  };
}

How do you analyze and reduce bundle size?

Start by visualizing your bundle to understand what's taking space. Use tools like webpack-bundle-analyzer, vite-bundle-visualizer, or Next.js's built-in analyzer. Look for duplicate dependencies, unused code, large libraries that could be replaced, and code that should be lazy-loaded.

# Webpack
npx webpack-bundle-analyzer stats.json
 
# Vite
npx vite-bundle-visualizer
 
# Next.js
ANALYZE=true npm run build

What are dynamic imports and when should you use them?

Dynamic imports load code when needed rather than upfront. Use them for features that aren't needed on initial load, large libraries used in specific scenarios, and conditional features based on user type or preferences.

// Load heavy library only when used
async function generatePDF() {
  const { jsPDF } = await import('jspdf');
  const doc = new jsPDF();
  // Generate PDF...
}
 
// Conditional feature loading
if (user.hasAdvancedFeatures) {
  const module = await import('./advanced-features');
  module.init();
}

How do you keep the main thread responsive?

The main thread handles JavaScript, layout, paint, and user input. Keeping it responsive requires breaking up long tasks, moving heavy computation off-thread, and prioritizing user-visible work.

// 1. Web Workers for heavy computation
const worker = new Worker('/compute-worker.js');
worker.postMessage(data);
worker.onmessage = (e) => updateUI(e.data);
 
// 2. requestIdleCallback for non-urgent work
requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0 && tasks.length > 0) {
    performTask(tasks.shift());
  }
});
 
// 3. setTimeout to break up synchronous work
function processLargeArray(items, callback) {
  const chunk = 100;
  let index = 0;
 
  function processChunk() {
    const end = Math.min(index + chunk, items.length);
    for (; index < end; index++) {
      processItem(items[index]);
    }
    if (index < items.length) {
      setTimeout(processChunk, 0); // Yield to browser
    } else {
      callback();
    }
  }
  processChunk();
}

Rendering Performance Questions

Understanding how browsers render helps you avoid performance pitfalls.

What is the critical rendering path?

The critical rendering path is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into pixels on screen. The browser parses HTML into the DOM, parses CSS into the CSSOM, combines them into the Render Tree, calculates layout, then paints pixels.

HTML → DOM
              ↘
                Render Tree → Layout → Paint → Composite
              ↗
CSS  → CSSOM

Optimizing the critical path means minimizing critical resources (inline critical CSS, defer non-critical), reducing critical bytes (minify, compress), and shortening path length (reduce round trips, use HTTP/2).

What is layout thrashing and how do you avoid it?

Layout thrashing happens when you read layout properties, then write, then read again—forcing the browser to recalculate layout repeatedly. Each read after a write triggers a synchronous layout calculation, which is expensive.

// Bad: Forces layout recalculation on each iteration
elements.forEach(el => {
  const height = el.offsetHeight; // Read (forces layout)
  el.style.height = height + 10 + 'px'; // Write (invalidates layout)
});
 
// Good: Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // All reads first
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px'; // All writes after
});
 
// Better: Use CSS where possible
elements.forEach(el => {
  el.style.height = 'calc(100% + 10px)';
});

What is the difference between reflow and repaint?

Reflow (layout) and repaint are different operations with different costs. Reflow recalculates element positions and sizes—triggered by size, position, or DOM structure changes. Repaint only updates visual properties like color and visibility. Composite operations like transform and opacity are cheapest because they run on the GPU.

OperationTriggered ByCost
Reflow (Layout)Size, position, or DOM structure changesHigh
RepaintColor, visibility, background changesMedium
Compositetransform, opacityLow

Which CSS properties should you use for animations?

For smooth animations, prefer compositor-only properties that run on the GPU: transform and opacity. Properties that trigger layout or paint cause the main thread to do work, which can cause jank if the main thread is busy.

/* Bad: Triggers reflow */
.animate {
  transition: left 0.3s, top 0.3s, width 0.3s;
}
 
/* Good: Compositor-only, runs on GPU */
.animate {
  transition: transform 0.3s, opacity 0.3s;
}
 
/* Move element without triggering layout */
.moved {
  transform: translateX(100px);
}

Image Optimization Questions

Images typically account for most of a page's bytes, so optimizing them has outsized impact.

What modern image formats should you use?

WebP and AVIF are modern formats that provide significantly better compression than JPEG and PNG. WebP is 25-35% smaller than JPEG with similar quality and has 97%+ browser support. AVIF offers even better compression but with 85% support. Use SVG for icons and illustrations.

FormatBest ForBrowser Support
WebPGeneral use, photos97%+
AVIFMaximum compression85%+
SVGIcons, illustrations100%
PNGTransparency needed, fallback100%
<!-- Serve modern formats with fallbacks -->
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Description" width="800" height="600" />
</picture>

How do responsive images work?

Responsive images let the browser choose the best image size for the current viewport and device pixel ratio. Use srcset to provide multiple sizes and sizes to tell the browser how wide the image will be displayed.

<!-- Different sizes for different viewports -->
<img
  src="photo-800.jpg"
  srcset="
    photo-400.jpg 400w,
    photo-800.jpg 800w,
    photo-1200.jpg 1200w"
  sizes="(max-width: 600px) 100vw, 800px"
  alt="Description"
 />

The browser parses sizes to determine display width, considers device pixel ratio (DPR), then selects the smallest image that covers the need. This saves bandwidth on mobile devices while serving high-resolution images on large displays.

How does lazy loading work?

Lazy loading defers loading images until they're needed—typically when they're about to enter the viewport. This reduces initial page load time and saves bandwidth for images the user never scrolls to.

<!-- Native lazy loading -->
<img src="photo.jpg" loading="lazy" alt="..." />

For more control, use Intersection Observer to detect when images approach the viewport:

// Intersection Observer for lazy loading
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
}, { rootMargin: '100px' }); // Load 100px before visible
 
document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

How do you optimize font loading?

Font optimization involves preloading critical fonts and controlling the loading behavior to minimize layout shifts. Use font-display to control what users see while fonts load, and preload fonts that are needed above the fold.

/* 1. Use font-display to control loading behavior */
@font-face {
  font-family: 'CustomFont';
  src: url('/font.woff2') format('woff2');
  font-display: swap; /* Show fallback, swap when loaded */
}
<!-- 2. Preload critical fonts -->
<link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin>

Caching Strategy Questions

Effective caching dramatically improves repeat visit performance.

What are the Cache-Control header options?

Cache-Control headers tell browsers and CDNs how to cache responses. Different content types need different caching strategies—static assets with versioned filenames can be cached forever, while HTML should always be revalidated.

# Static assets (versioned filenames)
Cache-Control: public, max-age=31536000, immutable

# HTML (always revalidate)
Cache-Control: no-cache

# API responses (short cache)
Cache-Control: private, max-age=60
DirectiveMeaning
publicCDN and browser can cache
privateOnly browser can cache (user-specific data)
max-age=NCache for N seconds
immutableNever revalidate (use with versioned URLs)
no-cacheCache but revalidate before use
no-storeDon't cache at all

What are the main service worker caching strategies?

Service workers enable powerful caching strategies that work offline and provide instant loading on repeat visits. The main strategies differ in whether they prioritize speed (cache first) or freshness (network first).

// sw.js - Cache-first strategy for static assets
self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image' ||
      event.request.destination === 'style' ||
      event.request.destination === 'script') {
    event.respondWith(
      caches.match(event.request).then(cached => {
        return cached || fetch(event.request).then(response => {
          const clone = response.clone();
          caches.open('static-v1').then(cache => {
            cache.put(event.request, clone);
          });
          return response;
        });
      })
    );
  }
});
 
// Network-first strategy for API calls
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          const clone = response.clone();
          caches.open('api-v1').then(cache => {
            cache.put(event.request, clone);
          });
          return response;
        })
        .catch(() => caches.match(event.request))
    );
  }
});

When should you use each caching pattern?

Different content types and use cases call for different caching strategies. Static assets benefit from cache-first for instant loading. API data usually needs network-first for freshness with cache fallback. Stale-while-revalidate balances speed and freshness.

PatternUse Case
Cache FirstStatic assets, fonts, images
Network FirstAPI data, frequently updated content
Stale While RevalidateBalance between freshness and speed
Cache OnlyOffline-first apps, installed assets
Network OnlyReal-time data, auth requests

Performance Measurement Questions

You can't optimize what you don't measure.

How do you use Lighthouse for performance auditing?

Lighthouse audits performance, accessibility, SEO, and best practices. It runs in Chrome DevTools, as a CLI, or as a Node module. Lighthouse provides both scores and specific recommendations for improvement.

# Run from command line
npx lighthouse https://example.com --output=html
 
# Or use Chrome DevTools > Lighthouse tab

Key Lighthouse metrics:

MetricWhat It Measures
FCPFirst Contentful Paint
LCPLargest Contentful Paint
TBTTotal Blocking Time (correlates with INP)
CLSCumulative Layout Shift
Speed IndexHow quickly content is visually populated

What should you look for in the Chrome DevTools Performance panel?

The Performance panel shows exactly what happens during page load or interaction. Record a session, then analyze the flame chart for long tasks (marked with red corners), layout thrashing (forced reflow warnings), and frames taking longer than 16ms (causing jank).

  1. Record page load or interaction
  2. Analyze the flame chart for long tasks
  3. Identify layout thrashing, forced reflows
  4. Check the Frames section for jank (frames > 16ms)

What is the difference between lab data and field data?

Lab data (Lighthouse) shows performance under controlled conditions—useful for debugging and development. Field data (Real User Monitoring) shows actual performance across real users, devices, and network conditions. Both are valuable for different purposes.

Lab data catches regressions during development. Field data reveals issues that only appear on specific devices or slow networks that you don't test with.

How do you implement Real User Monitoring (RUM)?

RUM captures Core Web Vitals from actual users using the web-vitals library. Send these metrics to your analytics endpoint to understand real-world performance across your user base.

// Capture Core Web Vitals with web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';
 
function sendToAnalytics({ name, value, id }) {
  // Send to your analytics endpoint
  navigator.sendBeacon('/analytics', JSON.stringify({
    metric: name,
    value: value,
    id: id
  }));
}
 
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

Google's field data sources:

  • Chrome User Experience Report (CrUX)
  • PageSpeed Insights (shows both lab and field data)
  • Search Console Core Web Vitals report

Framework Performance Questions

Each framework has its own performance patterns and optimizations.

How do you optimize React performance?

React performance optimization focuses on reducing unnecessary re-renders and efficiently handling large lists. Use React.memo for expensive components, useMemo for expensive calculations, useCallback for stable function references, and virtualization for long lists.

// 1. Memoize expensive components
const ExpensiveList = React.memo(({ items }) => {
  return items.map(item => <ListItem key={item.id} {...item} />);
});
 
// 2. useMemo for expensive calculations
const sortedItems = useMemo(() => {
  return items.sort((a, b) => a.date - b.date);
}, [items]);
 
// 3. useCallback for stable function references
const handleClick = useCallback((id) => {
  setSelected(id);
}, []);
 
// 4. Virtualize long lists
import { FixedSizeList } from 'react-window';
 
function VirtualList({ items }) {
  return (
    <FixedSizeList
      height={400}
      itemCount={items.length}
      itemSize={50}
    >
      {({ index, style }) => (
        <div style={style}>{items[index].name}</div>
      )}
    </FixedSizeList>
  );
}

How do you optimize Angular performance?

Angular optimization centers on change detection. Use OnPush change detection to check components only when inputs change. Use trackBy with ngFor to prevent unnecessary DOM recreation. Lazy load routes to reduce initial bundle size.

// 1. OnPush change detection
@Component({
  selector: 'app-list',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent {
  @Input() items: Item[];
}
 
// 2. trackBy for ngFor
<div *ngFor="let item of items; trackBy: trackById">
  {{ item.name }}
</div>
 
trackById(index: number, item: Item): number {
  return item.id;
}
 
// 3. Lazy load routes
const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module')
      .then(m => m.AdminModule)
  }
];
 
// 4. Use Signals for fine-grained reactivity (Angular 16+)
count = signal(0);
doubled = computed(() => this.count() * 2);

How do you optimize Next.js performance?

Next.js provides built-in optimizations. Use the Image component for automatic image optimization. Use dynamic imports for code splitting. Leverage Server Components (default in App Router) to reduce client bundle size. Use Suspense for streaming.

// 1. Use Image component for automatic optimization
import Image from 'next/image';
 
<Image
  src="/hero.jpg"
  width={1200}
  height={600}
  priority // Preload above-fold images
  placeholder="blur"
/>
 
// 2. Dynamic imports for code splitting
import dynamic from 'next/dynamic';
 
const DynamicChart = dynamic(() => import('./Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false // Don't render on server if client-only
});
 
// 3. Use React Server Components (default in App Router)
// Server Components don't add to client bundle
async function ProductList() {
  const products = await db.products.findMany();
  return products.map(p => <ProductCard key={p.id} product={p} />);
}
 
// 4. Streaming with Suspense
<Suspense fallback={<ProductsSkeleton />}>
  <ProductList />
</Suspense>

Quick Reference

Core Web Vitals targets:

  • LCP < 2.5s
  • INP < 200ms
  • CLS < 0.1

Bundle optimization:

  • Code split routes and heavy features
  • Tree shake with ES modules
  • Analyze bundle regularly
  • Replace heavy dependencies

Image optimization:

  • Use WebP/AVIF formats
  • Implement responsive images
  • Lazy load below-fold images
  • Always set width/height

Caching:

  • Immutable cache for versioned assets
  • No-cache for HTML
  • Service workers for offline/fast repeat loads

Measurement:

  • Lighthouse for lab data
  • RUM (web-vitals) for field data
  • DevTools Performance for debugging

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides