Web performance interviews test whether you can connect user-visible symptoms to evidence. Core Web Vitals are useful outcomes, but a good answer also distinguishes lab diagnostics from real-user data, treats accessibility and correctness as constraints, and verifies every optimization after deployment.
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
- Core Web Vitals Overview Questions
- LCP Optimization Questions
- INP and Interactivity Questions
- CLS and Visual Stability Questions
- JavaScript Performance Questions
- Rendering Performance Questions
- Image Optimization Questions
- Caching Strategy Questions
- Performance Measurement Questions
- Framework Performance Questions
Core Web Vitals Overview Questions
Core Web Vitals are a shared vocabulary for three user-experience outcomes. They do not cover every performance or accessibility problem.
What are Core Web Vitals?
Core Web Vitals are three stable metrics intended to reflect real-user loading, responsiveness, and visual stability. Google evaluates a page or origin as “good” when the 75th percentile of visits meets the good threshold for every metric, segmented by device class in its field datasets.
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.
| Metric | Measures | Good | Needs Work | Poor |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Loading speed | ≤ 2.5s | > 2.5s and ≤ 4s | > 4s |
| INP (Interaction to Next Paint) | Responsiveness | ≤ 200ms | > 200ms and ≤ 500ms | > 500ms |
| CLS (Cumulative Layout Shift) | Visual stability | ≤ 0.1 | > 0.1 and ≤ 0.25 | > 0.25 |
Why do Core Web Vitals matter for SEO?
Google's core ranking systems use Core Web Vitals among signals aligned with page experience. There is no single page-experience signal, good scores do not guarantee a high position, and relevant, helpful content remains primary. Treat performance as a user outcome first; SEO is one possible downstream benefit.
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 the time from navigation until the largest eligible image or text block in the viewport is rendered. The candidate can change as larger content appears, and the metric stops updating after the first qualifying user interaction. It is a proxy for when the main content likely becomes visible, not proof that the whole page is ready.
The good threshold is 2.5 seconds or less at the 75th percentile of page visits; more than 4 seconds is poor. Diagnose the experience in field data rather than describing an individual slow load as an automatic search penalty.
What causes poor LCP scores?
Break LCP into time to first byte, resource-load delay, resource-load duration, and element-render delay. The dominant part tells you whether to focus on origin/cache latency, discovery and priority, bytes/network, or main-thread/rendering work.
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, evaluate redirects, CDN placement, caching, and backend work. Make the LCP resource discoverable in initial HTML; do not lazy-load it. Use fetchpriority="high" for a likely LCP image, or preload a resource that the parser cannot otherwise discover early, but verify priorities and waterfall changes because hints can also create contention.
<!-- Preload critical resources -->
<link rel="preload" href="/hero-image.webp" as="image" fetchpriority="high">
<!-- Inline critical CSS -->
<style>
.hero { /* critical styles */ }
</style>
<!-- Load the stylesheet normally unless measurement justifies a different path -->
<link rel="stylesheet" href="/styles.css">What is the optimal resource loading order for LCP?
There is no universal “optimal order.” Keep the HTML response fast, make critical CSS and the LCP resource discoverable, avoid redundant preloads, and use script semantics (defer, modules, or async) that match dependency and execution requirements. Inspect the network waterfall and priority column before changing hints.
<!-- Optimal resource loading order -->
<head>
<!-- Critical CSS inlined -->
<style>/* Above-the-fold styles */</style>
<!-- Prioritize the likely LCP image; do not lazy-load it -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<link rel="stylesheet" href="/styles.css">
<!-- 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 observes click, tap, and keyboard interactions across the page visit. Each interaction includes input delay, event-handler processing, and presentation delay until the next paint. The final value approximates the longest interaction, with one high interaction ignored per 50 interactions to reduce sensitivity to occasional outliers.
The good threshold is 200 milliseconds or less at the 75th percentile of page visits; more than 500 milliseconds is poor. A field INP is a page-visit summary, not a guarantee that every interaction completed under the threshold.
What causes poor INP scores?
Poor INP commonly comes from main-thread contention: long JavaScript tasks, expensive event handlers, style/layout work, rendering large DOM updates, or third-party scripts. Diagnose all three INP phases before assuming the handler alone is responsible.
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);
});
// scheduler.yield() where supported; provide a fallback for your browser matrix
async function yieldingComputation() {
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += expensiveStep(i);
if (i % 10000 === 0) {
await scheduler.yield();
}
}
return result;
}Yielding lets higher-priority work run, but it does not make total computation cheaper. Move CPU-heavy, DOM-independent work to a worker, remove unnecessary work, and re-measure field interactions. Give immediate feedback only if it accurately represents the operation's state.
CLS and Visual Stability Questions
CLS measures the frustrating experience of content shifting unexpectedly.
What is Cumulative Layout Shift (CLS)?
CLS is a unitless score built from clusters of unexpected layout shifts over the page lifetime, not only during initial load. Shifts close to a recent user input are generally excluded. The good threshold is 0.1 or less at the 75th percentile of visits.
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 fallback and web-font metrics differ. font-display controls the block and swap periods; metric overrides can make a dedicated fallback face more closely match the web font. Values must be derived from the actual fonts rather than guessed.
@font-face {
font-family: 'CustomFont';
src: url('/font.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'CustomFont Fallback';
src: local('Arial');
size-adjust: 102%; /* Example only: calculate for the chosen font pair */
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}font-display values:
| Value | Behavior |
|---|---|
swap | Show fallback immediately, swap when loaded |
block | Brief invisible text, then show custom font |
fallback | Very brief block, then fallback, late swap ignored |
optional | Browser 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 creates chunks that can load on demand. It can reduce initial transfer, parse, compile, and execution work when a deferred route or feature is genuinely unnecessary at startup. It can also regress performance through request waterfalls, duplicated modules, or many tiny chunks, so confirm the result on representative devices and networks.
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 attempts to remove exports proven unreachable. Static ES-module syntax makes this analysis reliable, but the result also depends on side-effect annotations, package structure, transpilation, and bundler configuration. Some tools optimize limited CommonJS patterns, so “CommonJS never tree-shakes” is too absolute.
// Bad: Imports entire library
import _ from 'lodash';
_.debounce(fn, 300);
// More explicit: imports the required entry point
import debounce from 'lodash/debounce';
debounce(fn, 300);
// A local implementation can be smaller, but must match required semantics
function debounce(fn, ms) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), ms);
};
}How do you analyze and reduce bundle size?
Start with a production build and inspect chunk composition, duplicate versions, source-map attribution, and code coverage. Transfer size alone is incomplete: measure parse, compile, and execution time on a representative lower-end device. Use the analyzer supported by your build tool rather than copying a package command without checking its version and configuration.
The best reduction is deletion: remove unused dependencies and polyfills, avoid shipping server-only modules, and narrow client boundaries. Then split code at user journeys that are not required for the first interaction and check for new waterfalls.
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 optional work where supported
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();
}requestIdleCallback() is not a deadline guarantee and needs a compatibility strategy. setTimeout(0) yields to a later task but does not guarantee immediate execution. Workers cannot manipulate the DOM and message serialization/transfer has a cost; use transferable objects when appropriate.
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
Optimize the measured path: reduce TTFB and redirects, remove blocking resource waterfalls, keep critical bytes small, and avoid main-thread work that delays rendering. HTTP/2 or HTTP/3 can help connection use, but protocol choice does not erase dependency chains or server latency.
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?
Layout and paint are different stages. Layout calculates geometry; paint records visual output; compositing assembles layers. A change can affect one element or a large subtree, and browser optimizations vary, so a fixed high/medium/low cost table is only a heuristic. transform and opacity often avoid layout, but they do not guarantee a free GPU-only operation and extra layers consume memory.
| Operation | Triggered By | Cost |
|---|---|---|
| Reflow (Layout) | Size, position, or DOM structure changes | High |
| Repaint | Color, visibility, background changes | Medium |
| Composite | Often transform or opacity | Often lower, but measure |
Which CSS properties should you use for animations?
For smooth animations, prefer transform and opacity when they produce the intended design because browsers can often update them without layout. Confirm layer behavior and frame timing in DevTools; will-change and layer promotion are not free, and accessibility settings such as prefers-reduced-motion still apply.
/* Bad: Triggers reflow */
.animate {
transition: left 0.3s, top 0.3s, width 0.3s;
}
/* Often cheaper because it avoids layout */
.animate {
transition: transform 0.3s, opacity 0.3s;
}
/* Move element without triggering layout */
.moved {
transform: translateX(100px);
}Image Optimization Questions
Images are often a large share of transferred bytes, but confirm this in your own traffic before prioritizing them over JavaScript, fonts, or server latency.
What modern image formats should you use?
WebP and AVIF support modern lossy and lossless workflows, but neither is universally smallest for every image or encoder setting. Compare visual quality, encoded size, encode/decode cost, animation, transparency, HDR, and your supported browser matrix. SVG is appropriate for many vector graphics—not arbitrary photos or untrusted markup.
| Format | Best For | Browser Support |
|---|---|---|
| WebP | General raster images | Verify required clients |
| AVIF | High-compression raster candidates | Verify required clients and decode cost |
| SVG | Trusted vector graphics | Not for photographic raster content |
| JPEG/PNG | Compatibility or workload-specific fallback | Choose by content and measurement |
<!-- 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 eligible offscreen images until they approach the viewport. It can reduce initial contention and unused transfer, but applying it to an in-viewport or LCP image delays discovery and harms LCP. Keep width and height attributes so deferred content still reserves space.
<!-- Native lazy loading -->
<img src="photo.jpg" loading="lazy" width="800" height="600" 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?
Subset and self-host fonts when licensing permits, choose only required weights/styles, and set an intentional font-display policy. Preload only fonts proven critical and used by initial content: every preload is an unconditional fetch that competes with other resources. Match fallback metrics to reduce shifts and measure both repeat and cold visits.
/* 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 tells private and shared caches when a response may be stored and reused. Choose policy from the content's freshness, personalization, invalidation, and security requirements. Content-hashed assets are strong candidates for a long freshness lifetime; HTML policy depends on whether it is personalized and how quickly it must change.
# Static assets (versioned filenames)
Cache-Control: public, max-age=31536000, immutable
# HTML that may be stored but must be revalidated before reuse
Cache-Control: no-cache
# API responses (short cache)
Cache-Control: private, max-age=60
| Directive | Meaning |
|---|---|
public | Response may be stored by shared caches |
private | Response is intended for a private cache, not a shared cache |
max-age=N | Cache for N seconds |
immutable | While fresh, clients should not revalidate after a reload |
no-cache | Cache but revalidate before use |
no-store | Don't cache at all |
What are the main service worker caching strategies?
Service workers can intercept requests and implement application-controlled caching, including offline behavior. They also create an extra correctness and security boundary: scope only eligible GET requests, version and expire caches, avoid storing personalized or authorization-protected responses by default, and define update and offline semantics.
// sw.js - Cache-first strategy for static assets
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (event.request.method === 'GET' &&
url.origin === self.location.origin &&
(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();
if (response.ok) {
caches.open('static-v1').then(cache => cache.put(event.request, clone));
}
return response;
});
})
);
}
});
// Network-first for explicitly allowlisted public API data
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (event.request.method === 'GET' &&
url.origin === self.location.origin &&
url.pathname.startsWith('/api/public/')) {
event.respondWith(
fetch(event.request)
.then(response => {
const clone = response.clone();
if (response.ok) {
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.
| Pattern | Use Case |
|---|---|
| Cache First | Static assets, fonts, images |
| Network First | API data, frequently updated content |
| Stale While Revalidate | Balance between freshness and speed |
| Cache Only | Offline-first apps, installed assets |
| Network Only | Mutations, sensitive/authenticated data, or strict freshness |
Treat this table as a starting hypothesis. Cache keys, Vary, credentials, quota, opaque responses, offline error UX, and invalidation can change the correct choice.
Performance Measurement Questions
You can't optimize what you don't measure.
How do you use Lighthouse for performance auditing?
Lighthouse is a lab diagnostic that audits performance, accessibility, SEO, and best practices under a declared test environment. Run comparable configurations repeatedly or in CI and inspect metric traces and opportunities; a single score is noisy and is not field evidence.
# Run from command line
npx lighthouse https://example.com --output=html
# Or use Chrome DevTools > Lighthouse tabKey Lighthouse metrics:
| Metric | What It Measures |
|---|---|
| FCP | First Contentful Paint |
| LCP | Largest Contentful Paint |
| TBT | Total Blocking Time; a lab diagnostic, not a measured field INP |
| CLS | Cumulative Layout Shift |
| Speed Index | How quickly content is visually populated |
What should you look for in the Chrome DevTools Performance panel?
The Performance panel records one environment and interaction. Use it to inspect long tasks, interaction timing, third-party work, style/layout, paint, and network dependencies. A 60 Hz display has about 16.7 ms per frame before browser overhead, but refresh rates vary, so avoid treating 16 ms as a universal pass/fail line.
- Record page load or interaction
- Analyze the flame chart for long tasks
- Identify layout thrashing, forced reflows
- Check frame pacing against the tested display and interaction
What is the difference between lab data and field data?
Lab data is repeatable enough for diagnosis and regression checks under a defined setup. Field data records actual visits across real devices, networks, caches, consent states, and user journeys. CrUX reports eligible Chrome populations in rolling windows and may aggregate at page or origin level; first-party RUM can add business context and broader coverage.
Use field data to find affected segments and prioritize outcomes, then reproduce with lab traces and verify the deployed change back in the field. Report distributions—especially the 75th percentile—not just averages.
How do you implement Real User Monitoring (RUM)?
RUM can capture Core Web Vitals with the web-vitals library. Record metric ID, navigation type, page/version, device context, and useful attribution without collecting unnecessary personal data. Sample deliberately, handle consent and ad blockers, and build percentile distributions rather than averaging scores.
// Capture Core Web Vitals with web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';
function sendToAnalytics({ name, value, id }) {
const body = JSON.stringify({
metric: name,
value,
id,
page: location.pathname
});
navigator.sendBeacon('/analytics', body);
}
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?
Start with React DevTools Profiler and browser traces: the bottleneck may be network, JavaScript, layout, or an expensive commit rather than “too many renders.” React Compiler can apply memoization automatically. In codebases without it, use memo, useMemo, or useCallback only where profiling shows that stable identity or cached computation removes meaningful work; these APIs have their own cost and are not correctness tools.
// Memoize only after profiling shows this boundary is expensive
const ExpensiveList = React.memo(({ items }) => {
return items.map(item => <ListItem key={item.id} {...item} />);
});
// Never mutate props while deriving data
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.date - b.date);
}, [items]);
// Stable identity matters only when a consumer benefits from it
const handleClick = useCallback((id) => {
setSelected(id);
}, []);
// Virtualize long lists when DOM/render cost is measured and semantics remain usable
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?
Profile with Angular DevTools and the browser's Angular performance track before selecting an optimization. In Angular 22, OnPush is the default strategy; it skips eligible subtrees based on notifications, not simply “inputs changed by reference.” Angular 21+ is zoneless by default. Stable track identity, lazy routes, @defer, signals, SSR/hydration, and smaller template computations address different bottlenecks.
// Angular 22: OnPush is the default; an explicit setting can document intent
@Component({
selector: 'app-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
}
`
})
export class ListComponent {
items = input.required<readonly Item[]>();
}
// Lazy-load a standalone route component
const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin/admin.component')
.then(m => m.AdminComponent)
}
];
// Signals express reactive dependencies
count = signal(0);
doubled = computed(() => this.count() * 2);How do you optimize Next.js performance?
In the Next.js 16 App Router, pages and layouts are Server Components by default. Keep 'use client' boundaries narrow, because their imports join the client module graph. Use next/image with a constrained remote-source policy, lazy-load Client Components or libraries that are not initially needed, and stream independent slow regions. Verify caching and rendering choices per route rather than assuming every Server Component is cached.
// app/page.jsx - Server Component
import Image from 'next/image';
import { Suspense } from 'react';
<Image
src="/hero.jpg"
width={1200}
height={600}
preload // Next.js 16; use selectively for the likely LCP image
alt="Product overview"
/>
// Server Component: its own module code is not shipped for hydration
async function ProductList() {
const products = await db.products.findMany();
return products.map(p => <ProductCard key={p.id} product={p} />);
}
// 4. Stream an independent slow region with Suspense
<Suspense fallback={<ProductsSkeleton />}>
<ProductList />
</Suspense>// components/AnalyticsPanel.jsx - Client Component
'use client';
import dynamic from 'next/dynamic';
const DynamicChart = dynamic(() => import('./Chart'), {
loading: () => <ChartSkeleton />
});
export function AnalyticsPanel() {
return <DynamicChart />;
}Quick Reference
Core Web Vitals targets:
- LCP ≤ 2.5s
- INP ≤ 200ms
- CLS ≤ 0.1
- Evaluate the 75th percentile of page visits
Bundle optimization:
- Code split routes and heavy features
- Tree shake with ES modules
- Analyze bundle regularly
- Remove or replace dependencies when measurement justifies it
Image optimization:
- Compare WebP/AVIF and fallbacks at equivalent visual quality
- Implement responsive images
- Lazy load offscreen images, never the LCP image
- Always set width/height
Caching:
- Long immutable freshness for content-hashed assets
- Choose HTML policy from freshness and personalization needs
- Service workers for offline/fast repeat loads
Measurement:
- Lighthouse for repeatable lab diagnostics
- RUM (web-vitals) for field data
- DevTools Performance for debugging
Frequently Asked Questions
What are Core Web Vitals and why do they matter?
Core Web Vitals are Google's field-oriented metrics for loading, responsiveness, and visual stability: LCP, INP, and CLS. Google's ranking systems use them as part of broader page-experience evaluation, but good scores neither guarantee rankings nor replace relevant content. They are most useful as measurable user-experience outcomes.
What is the difference between LCP, INP, and CLS?
LCP measures when the largest eligible viewport content renders; good is 2.5 seconds or less. INP measures the latency of interactions across the page visit; good is 200 milliseconds or less. CLS measures unexpected layout-shift clusters over the page lifetime; good is 0.1 or less. Assess each at the 75th percentile of visits.
How do you reduce JavaScript bundle size?
Measure the shipped and executed JavaScript first, then remove unused dependencies and polyfills, use production builds, preserve ES-module boundaries for tree shaking, and split at real route or feature boundaries. Dynamic imports can defer code, but too many chunks add overhead. Re-measure transfer, parse, compile, and execution cost on representative devices.
What is code splitting and how does it improve performance?
Code splitting creates separately loadable chunks, often at route or feature boundaries. A dynamic import returns a promise and gives compatible bundlers a split point. It can reduce initial transfer and main-thread work, but only when deferred code is not immediately required and chunk-request overhead does not outweigh the saving.
What is the critical rendering path?
The critical rendering path covers the work needed to turn the initial HTML and CSS into rendered pixels: discovery and loading of critical resources, DOM and CSSOM construction, style calculation, layout, paint, and compositing. JavaScript can block or invalidate parts of that work. Optimize the measured bottleneck rather than blindly inlining or preloading assets.
How do you optimize images for web performance?
Choose and measure the smallest acceptable encoding, offer responsive candidates with srcset and accurate sizes, preserve intrinsic dimensions, and lazy-load only offscreen images. Keep the LCP image discoverable in initial HTML and do not lazy-load it; fetchpriority high can help when measurement confirms priority is the bottleneck.
Sources
- Google Search Central: Page experience in Google Search
- web.dev: How Core Web Vitals thresholds are defined
- web.dev: Optimize Largest Contentful Paint
- web.dev: Optimize Interaction to Next Paint
- web.dev: Optimize Cumulative Layout Shift
- web.dev: Field measurement best practices
- MDN: HTTP caching
- MDN: Service Worker API
- React: React Compiler
- Angular: Performance
- Next.js 16: Image
- Next.js: Lazy loading
Related Articles
- Complete Frontend Developer Interview Guide - Full guide to frontend interviews
- Next.js Interview Guide - Next.js optimization and performance
- React Advanced Interview Guide - React performance patterns
- Angular Change Detection Interview Guide - Angular performance optimization
