Frontend Interview โ The Complete Guide
Everything you need to crack frontend interviews at top companies. From HTML semantics to system design, organized by priority and depth.
Table of Contents
HTML โ Structure & Semantics
HTML is the skeleton of every web page. Interviewers test it less often than JS, but when they do, they expect you to know semantics and accessibility cold. Getting these right signals maturity.
๐ฅ Semantic Tags โ Must Know
Semantic HTML means using tags that describe their content: <header>, <nav>, <main>, <article>, <section>, <aside>, <footer>. Screen readers and search engines rely on these to understand page structure.
Why it matters in interviews
Interviewers check if you default to <div> for everything or use meaningful tags. Using semantic HTML shows you care about accessibility and SEO โ both are table stakes at top companies.
Accessibility Basics
- Always use
alton images (empty string for decorative) - Use
aria-labelfor icon-only buttons - Ensure keyboard navigation works (focus management)
- Use
roleattributes when semantic tags aren't enough - Color contrast ratio โฅ 4.5:1 for normal text
Forms & Input Types
Know the difference between type="text", email, number, tel, date. Mobile browsers show different keyboards based on type. Use <label> with htmlFor for accessibility. Controlled vs uncontrolled forms come up in React interviews.
SEO Basics
- One
<h1>per page, hierarchical headings - Meta tags:
title,description,og:image - Semantic structure helps crawlers understand content
- SSR/SSG improves SEO (covered in Next.js section)
๐ Quick Revision
Quick Revision Cheat Sheet
Semantic tags: header, nav, main, article, section, aside, footer โ use them instead of divs
Accessibility: alt text, aria-label, keyboard nav, focus management, color contrast
Forms: Use correct input types, label with htmlFor, validation attributes
SEO: One h1, meta tags, semantic structure, SSR for crawlability
Common Interview Questions
Q:What's the difference between <section> and <div>?
A: <section> is semantic โ it represents a thematic grouping of content with a heading. <div> is a generic container with no semantic meaning. Use <section> when the content forms a logical group; use <div> for styling/layout purposes only.
Q:How do you make a website accessible?
A: Use semantic HTML, add alt text to images, ensure keyboard navigation, manage focus for modals/dialogs, use ARIA attributes when needed, maintain sufficient color contrast, and test with screen readers.
Q:What is the purpose of the <meta> viewport tag?
A: It controls how the page scales on mobile devices. Without it, mobile browsers render the page at desktop width and zoom out. The standard value is width=device-width, initial-scale=1.
CSS โ Layout & Rendering
CSS questions test your understanding of layout mechanics. You won't be asked to style a pixel-perfect design, but you will be asked to explain how the box model works or when to use flexbox vs grid.
๐ฅ Box Model โ Must Know
Every element is a box: content โ padding โ border โ margin. The key gotcha: by default, width only sets the content width. Use box-sizing: border-box to include padding and border in the width calculation. Every modern CSS reset does this.
Flexbox vs Grid
| Feature | Flexbox | Grid |
|---|---|---|
| Dimension | 1D (row OR column) | 2D (rows AND columns) |
| Best for | Navbars, card rows, centering | Page layouts, dashboards, complex grids |
| Alignment | justify-content, align-items | justify-items, align-items, place-items |
| Sizing | flex-grow, flex-shrink, flex-basis | fr units, minmax(), auto-fill/auto-fit |
| When to pick | Content flows in one direction | You need precise row + column control |
Positioning
staticโ default, normal flowrelativeโ offset from normal position, still in flowabsoluteโ removed from flow, positioned relative to nearest positioned ancestorfixedโ removed from flow, positioned relative to viewportstickyโ hybrid: relative until a scroll threshold, then fixed
Specificity & Cascade
Specificity determines which CSS rule wins when multiple rules target the same element. The hierarchy: !important > inline styles > #id > .class > element. Equal specificity? Last rule wins. Understanding this prevents "why isn't my style applying?" debugging nightmares.
๐ก Pro Tip
In interviews, if asked "how would you center a div," give the flexbox answer first (display: flex; justify-content: center; align-items: center), then mention grid (display: grid; place-items: center) as an alternative. Shows you know both.
๐ Quick Revision
Quick Revision Cheat Sheet
Box model: content โ padding โ border โ margin. Use border-box.
Flexbox: 1D layout. justify-content for main axis, align-items for cross axis.
Grid: 2D layout. fr units, grid-template-columns/rows, gap.
Specificity: !important > inline > #id > .class > element. Last rule wins on tie.
Centering: Flexbox: justify-content + align-items. Grid: place-items: center.
Common Interview Questions
Q:Explain the CSS box model.
A: Every element is a rectangular box with four layers: content (the actual text/image), padding (space between content and border), border (the visible edge), and margin (space between this element and neighbors). box-sizing: border-box makes width/height include padding and border.
Q:When would you use Grid over Flexbox?
A: Use Grid when you need control over both rows and columns simultaneously โ dashboards, image galleries, page layouts. Use Flexbox for single-axis layouts โ navbars, card rows, centering content. They're complementary, not competing.
JavaScript โ Core (Most Important)
๐ฅ This is the highest-weight section
JavaScript fundamentals make or break frontend interviews. Expect 2-3 questions on closures, async behavior, or the event loop in every single interview. Master this section first.
๐ฅ Closures, Scope & this โ Must Know
A closure is a function that remembers the variables from its outer scope even after that scope has finished executing. Every function in JS creates a closure. This is how data privacy, callbacks, and module patterns work.
function createCounter() { let count = 0; // closed over by the returned function return { increment: () => ++count, getCount: () => count, }; } const counter = createCounter(); counter.increment(); // 1 counter.increment(); // 2 counter.getCount(); // 2 // 'count' is private โ can't be accessed directly
Scope chain: JS looks up variables from inner โ outer โ global. this depends on HOW a function is called, not where it's defined. Arrow functions inherit this from their enclosing scope (lexical binding).
๐ฅ Async JS & Event Loop โ VERY IMPORTANT
JavaScript is single-threaded but non-blocking. The Event Loop is the mechanism that allows JavaScript to perform non-blocking, asynchronous operations despite being a single-threaded language. It acts as a traffic coordinator, continuously monitoring the state of the execution environment to decide when to run specific pieces of code.
The event loop processes the call stack, then microtasks (Promises, queueMicrotask), then macrotasks (setTimeout, setInterval, I/O). Understanding this order is critical for predicting output in interview questions.
console.log("1 โ sync"); setTimeout(() => console.log("2 โ macrotask"), 0); Promise.resolve().then(() => console.log("3 โ microtask")); console.log("4 โ sync"); // Output: 1, 4, 3, 2 // Sync first, then microtasks, then macrotasks
Promises & Async/Await
Promises represent eventual values. In JavaScript, a Promise is a special object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value. It serves as a placeholder for a result that is not yet available but will be in the future, allowing you to handle asynchronous code in a cleaner way than traditional nested callbacks (often called "callback hell").
async/await is syntactic sugar over Promises that makes async code read like sync code. Key: await pauses the async function, not the entire thread. The event loop keeps running.
async function fetchUser(id: string) { try { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { console.error("Failed to fetch user:", err); return null; } } // Promise.all for parallel requests const [user, posts] = await Promise.all([ fetchUser("1"), fetch("/api/posts").then(r => r.json()), ]);
๐ฅ Debounce & Throttle โ Must Know
| Technique | What it does | Use case |
|---|---|---|
| Debounce | Waits until the user STOPS triggering for N ms, then fires once | Search input, resize handler, form validation |
| Throttle | Fires at most once every N ms, regardless of how often triggered | Scroll handler, mousemove, rate-limiting API calls |
function debounce<T extends (...args: unknown[]) => void>( fn: T, delay: number ): (...args: Parameters<T>) => void { let timer: ReturnType<typeof setTimeout>; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }
Deep vs Shallow Copy
Shallow copy duplicates the top-level properties but nested objects still share references (Object.assign, spread operator). Deep copy recursively clones everything (structuredClone, JSON.parse(JSON.stringify())).
Prototypes
Every JS object has a hidden [[Prototype]] link. When you access a property, JS walks the prototype chain until it finds it or hits null. This is how inheritance works in JS. Classes are syntactic sugar over prototypal inheritance.
๐ก Pro Tip โ How to identify JS questions in interviews
If the interviewer shows you a code snippet and asks "what's the output?" โ it's testing closures, this, or event loop order. Trace through the execution step by step: sync code first, then microtasks, then macrotasks.
๐ Quick Revision
Quick Revision Cheat Sheet
Closure: Function + its outer scope. Enables data privacy and callbacks.
this: Depends on call site. Arrow functions use lexical this.
Event loop: Call stack โ microtasks (Promises) โ macrotasks (setTimeout).
async/await: Sugar over Promises. await pauses the function, not the thread.
Debounce: Wait until user stops, then fire. For search inputs.
Throttle: Fire at most once per interval. For scroll handlers.
Deep copy: structuredClone() or JSON round-trip. Spread is shallow only.
Common Interview Questions
Q:What is a closure and why is it useful?
A: A closure is a function that retains access to variables from its enclosing scope even after that scope has returned. It's useful for data privacy (module pattern), callbacks, and creating factory functions. Every event handler in React is a closure.
Q:Explain the event loop. What's the output of this code? (setTimeout 0 vs Promise)
A: JS runs all synchronous code first (call stack), then drains the microtask queue (Promise callbacks, queueMicrotask), then picks one macrotask (setTimeout, setInterval). So Promise.then always runs before setTimeout(fn, 0).
Q:Implement debounce from scratch.
A: Return a wrapper function that clears the previous timer and sets a new one. The original function only fires after the user stops calling for `delay` ms. Key: use closure to hold the timer reference across calls.
React
React is the most tested framework in frontend interviews. You need to understand not just how to use it, but how it works under the hood โ rendering, reconciliation, and performance.
๐ฅ Hooks โ Must Know
useState for local state, useEffect for side effects, useRef for mutable refs that don't trigger re-renders, useMemo/useCallback for memoization. The most common interview question: explain the useEffect dependency array and cleanup function.
useEffect(() => { // Runs after every render (no deps array) }); useEffect(() => { // Runs once on mount (empty deps) return () => { // Cleanup on unmount }; }, []); useEffect(() => { // Runs when 'id' changes const controller = new AbortController(); fetch(`/api/user/${id}`, { signal: controller.signal }); return () => controller.abort(); // cancel on cleanup }, [id]);
๐ฅ Rendering & Reconciliation
React re-renders a component when its state or props change. It diffs the new virtual DOM against the previous one (reconciliation) and applies minimal DOM updates. Keys help React identify which items changed in lists.
Performance Optimization
React.memoโ skip re-render if props haven't changeduseMemoโ cache expensive computationsuseCallbackโ stable function references for child components- Virtualize long lists (react-window, react-virtuoso)
- Code-split with
React.lazy+Suspense
Controlled vs Uncontrolled Components
| Aspect | Controlled | Uncontrolled |
|---|---|---|
| State lives in | React state (useState) | The DOM (ref) |
| Value access | Via state variable | Via ref.current.value |
| When to use | Most forms โ validation, conditional logic | Simple forms, file inputs, third-party libs |
| Re-renders | On every keystroke | Only when you read the ref |
๐ก Pro Tip
When asked "how would you optimize a slow React app," start with: (1) identify unnecessary re-renders with React DevTools Profiler, (2) memoize with React.memo/useMemo, (3) virtualize long lists, (4) code-split heavy routes. This shows a systematic approach.
๐ Quick Revision
Quick Revision Cheat Sheet
useState: Local state. Triggers re-render on update.
useEffect: Side effects after render. Cleanup on unmount or before re-run.
useRef: Mutable ref that persists across renders without causing re-render.
Reconciliation: React diffs virtual DOM trees and applies minimal real DOM updates.
Keys: Help React identify list items. Use stable IDs, never array index.
React.memo: HOC that skips re-render if props are shallowly equal.
Common Interview Questions
Q:What happens when you call setState in React?
A: React schedules a re-render (it's async/batched). On the next render, the component function runs again with the new state, React diffs the new virtual DOM against the old one, and applies the minimal set of real DOM mutations.
Q:Why shouldn't you use array index as a key?
A: If items are reordered, inserted, or deleted, the index-based keys don't match the actual items anymore. React reuses DOM nodes incorrectly, causing bugs like input values appearing in the wrong row. Use stable unique IDs instead.
Q:Explain the useEffect cleanup function.
A: The function returned from useEffect runs before the effect re-runs (when deps change) and on unmount. Use it to cancel subscriptions, abort fetch requests, clear timers, or remove event listeners to prevent memory leaks.
TypeScript
TypeScript is expected at most top companies now. Interviews test whether you can type real-world patterns โ not just primitives.
Types vs Interfaces
| Feature | type | interface |
|---|---|---|
| Union types | โ type A = string | number | โ Not possible |
| Extends | โ type B = A & { extra: string } | โ interface B extends A { } |
| Declaration merging | โ | โ Same-name interfaces merge |
| When to use | Unions, mapped types, utility types | Object shapes, class contracts, API responses |
๐ฅ Generics โ Must Know
// Generic function โ works with any type function first<T>(arr: T[]): T | undefined { return arr[0]; } // Generic with constraint function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } // Generic React component interface ListProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; } function List<T>({ items, renderItem }: ListProps<T>) { return <ul>{items.map(renderItem)}</ul>; }
Utility Types
Partial<T>โ all properties optionalRequired<T>โ all properties requiredPick<T, K>โ select specific propertiesOmit<T, K>โ exclude specific propertiesRecord<K, V>โ object with keys K and values VReturnType<T>โ extract return type of a function
๐ Quick Revision
Quick Revision Cheat Sheet
type vs interface: type for unions/mapped types. interface for object shapes and merging.
Generics: Parameterize types. Use constraints with extends.
Utility types: Partial, Required, Pick, Omit, Record, ReturnType.
React typing: React.FC (avoid), Props interface, React.ReactNode for children.
Browser & Web Internals
๐ฅ Very High Value in Senior Interviews
Browser internals separate mid-level from senior candidates. Understanding the rendering pipeline, event loop, and storage APIs shows deep knowledge that interviewers love.
๐ฅ Event Loop & Call Stack
The call stack executes synchronous code. When it's empty, the event loop checks the microtask queue (Promises), drains it completely, then picks one macrotask (setTimeout). This cycle repeats. The browser can only paint between macrotasks.
In JavaScript, the Call Stack is a core mechanism that the engine uses to track and manage function execution. It acts as a "to-do list," keeping track of which function is currently running and where to return once that function finishes.
DOM vs Virtual DOM
| Aspect | Real DOM | Virtual DOM |
|---|---|---|
| What | Browser's actual document tree | Lightweight JS object representation |
| Updates | Direct manipulation is expensive | Diff two trees, apply minimal patches |
| Who uses it | Vanilla JS, jQuery | React, Vue (internally) |
| Performance | Slow for frequent updates | Batches updates, minimizes reflows |
๐ฅ Rendering Pipeline
HTML โ DOM, CSS โ CSSOM โ Render Tree โ Layout โ Paint โ Composite. Blocking resources (CSS, sync JS) delay first paint. Understanding this pipeline is key to performance optimization.
What Happens When You Type a URL
DNS lookup โ TCP handshake โ TLS handshake (HTTPS) โ HTTP request โ Server response โ HTML parsing โ DOM construction โ CSSOM โ Render tree โ Layout โ Paint. This is a classic interview question that tests breadth of knowledge.
Storage APIs
| Storage | Size | Persistence | Use case |
|---|---|---|---|
| localStorage | ~5MB | Until cleared | User preferences, theme, tokens |
| sessionStorage | ~5MB | Until tab closes | Form drafts, temp state |
| Cookies | ~4KB | Configurable expiry | Auth tokens (httpOnly), tracking |
| IndexedDB | Large (100MB+) | Until cleared | Offline data, caching large datasets |
๐ Quick Revision
Quick Revision Cheat Sheet
Event loop: Stack โ microtasks (all) โ one macrotask โ paint โ repeat.
Rendering: HTMLโDOM, CSSโCSSOM, mergeโRender Tree, Layout, Paint, Composite.
Virtual DOM: JS object tree. React diffs old vs new, patches real DOM minimally.
URL flow: DNS โ TCP โ TLS โ HTTP โ Parse โ DOM โ CSSOM โ Render โ Paint.
Storage: localStorage (persistent), sessionStorage (tab), cookies (server), IndexedDB (large).
Performance & Optimization
Performance questions test whether you can diagnose and fix slow apps. Know the tools (Lighthouse, DevTools) and the techniques.
๐ฅ Core Web Vitals โ Must Know
| Metric | What it measures | Good threshold |
|---|---|---|
| LCP (Largest Contentful Paint) | When the main content is visible | โค 2.5s |
| INP (Interaction to Next Paint) | Responsiveness to user input | โค 200ms |
| CLS (Cumulative Layout Shift) | Visual stability (unexpected shifts) | โค 0.1 |
Key Techniques โ Explained
๐ฅ Lazy Loading
Lazy loading defers loading resources until they're actually needed. Instead of downloading every image and component upfront (which blocks the initial page load), you load them on demand โ typically when they enter the viewport or when a user navigates to a route.
For images: add loading="lazy" to <img> tags. The browser handles the rest โ it only fetches the image when it's about to scroll into view. Next.js <Image> does this by default.
For components: use React.lazy() + <Suspense>. The component's code is split into a separate bundle chunk and only downloaded when the component is first rendered.
import { lazy, Suspense } from "react"; // Component is NOT loaded until <HeavyChart /> is rendered const HeavyChart = lazy(() => import("./HeavyChart")); function Dashboard() { return ( <div> <h1>Dashboard</h1> {/* Suspense shows fallback while the chunk downloads */} <Suspense fallback={<div>Loading chart...</div>}> <HeavyChart /> </Suspense> </div> ); } // Image lazy loading โ native browser support <img src="/hero.jpg" alt="Hero" loading="lazy" />
When to use lazy loading
Use it for below-the-fold images, heavy third-party components (charts, editors, maps), and routes the user may never visit. Don't lazy-load above-the-fold content โ that hurts LCP.
๐ฅ Code Splitting
Code splitting breaks your JavaScript bundle into smaller chunks that load on demand. Without it, users download your entire app upfront โ even pages they'll never visit. With it, each route gets its own chunk, and shared code is extracted into common chunks.
Route-based splitting is the most common approach. Next.js does this automatically โ each page.tsx becomes its own chunk. In plain React, use React.lazy with React Router.
Component-based splitting is for heavy components within a page. Use dynamic import() to load a chart library, rich text editor, or PDF viewer only when needed.
// Route-based splitting with React Router + lazy const Home = lazy(() => import("./pages/Home")); const Settings = lazy(() => import("./pages/Settings")); // Next.js does this automatically for every page.tsx // Component-based splitting with next/dynamic import dynamic from "next/dynamic"; const RichEditor = dynamic(() => import("./RichEditor"), { loading: () => <p>Loading editor...</p>, ssr: false, // skip server-side rendering for browser-only libs });
Caching Strategies
| Layer | How it works | What to cache |
|---|---|---|
| Browser cache | HTTP headers (Cache-Control, ETag) tell the browser to reuse responses | Static assets (JS, CSS, images), API responses |
| CDN edge cache | Content is cached at servers close to the user geographically | Static files, SSG pages, public API responses |
| Service Worker | JS that intercepts network requests and serves cached responses | Offline support, app shell, API fallbacks |
| In-memory (React) | useMemo, React Query cache, state that avoids refetching | Computed values, API data with stale-while-revalidate |
Minimizing Re-renders
- State colocation โ keep state as close to where it's used as possible. If only one component needs it, don't put it in a parent.
- React.memo โ wrap components that receive the same props often. Skips re-render if props are shallowly equal.
- useMemo / useCallback โ stabilize expensive computations and function references so child components don't re-render unnecessarily.
- Avoid object/array literals in JSX โ
style={{ color: "red" }}creates a new object every render, breaking memo.
Image Optimization
- Use modern formats: WebP (30% smaller than JPEG) or AVIF (50% smaller)
- Serve responsive images with
srcsetandsizesโ don't send a 2000px image to a 400px phone screen - Always set
widthandheighton images to prevent layout shift (CLS) - Use Next.js
<Image>โ it handles lazy loading, responsive sizing, and format conversion automatically
Tree Shaking
Tree shaking removes unused code from your final bundle at build time. It works with ES modules (import/export) because they're statically analyzable. CommonJS (require) can't be tree-shaken. This is why you should always use named imports: import { debounce } from "lodash-es" instead of import _ from "lodash".
๐ก Pro Tip
In interviews, always mention measurement before optimization. "I'd first profile with DevTools/Lighthouse to identify the bottleneck, then apply the appropriate fix." This shows you don't optimize blindly.
๐ Quick Revision
Quick Revision Cheat Sheet
LCP: Largest Contentful Paint โค 2.5s. Optimize images, fonts, server response.
INP: Interaction to Next Paint โค 200ms. Reduce JS execution, defer non-critical work.
CLS: Cumulative Layout Shift โค 0.1. Set dimensions on images/ads, avoid dynamic injection.
Lazy loading: loading='lazy' for images, React.lazy + Suspense for components.
Code splitting: Split by route. Dynamic import() for heavy modules.
Networking & APIs
Frontend devs need to understand HTTP fundamentals. You'll be asked about methods, status codes, CORS, and how to handle API errors gracefully.
HTTP Methods
| Method | Purpose | Idempotent? |
|---|---|---|
| GET | Retrieve data | Yes |
| POST | Create a resource | No |
| PUT | Replace a resource entirely | Yes |
| PATCH | Partially update a resource | No |
| DELETE | Remove a resource | Yes |
Status Codes to Know
| Code | Meaning | When you see it |
|---|---|---|
| 200 | OK | Successful GET/PUT |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 301 | Moved Permanently | URL redirect (cached) |
| 304 | Not Modified | Cached response is still valid |
| 400 | Bad Request | Invalid input/params |
| 401 | Unauthorized | Missing or invalid auth token |
| 403 | Forbidden | Valid auth but insufficient permissions |
| 404 | Not Found | Resource doesn't exist |
| 429 | Too Many Requests | Rate limited |
| 500 | Internal Server Error | Server bug |
๐ฅ CORS โ Must Know
Cross-Origin Resource Sharing. Browsers block requests to different origins by default. The server must include Access-Control-Allow-Origin headers. Preflight requests (OPTIONS) happen for non-simple requests (custom headers, PUT/DELETE). This is a very common source of bugs and interview questions.
๐ Quick Revision
Quick Revision Cheat Sheet
GET: Read data. Idempotent. Cacheable.
POST: Create data. Not idempotent. Body required.
CORS: Browser security. Server must whitelist origins. Preflight for complex requests.
401 vs 403: 401 = not authenticated. 403 = authenticated but not authorized.
fetch vs axios: fetch is native (no deps). axios has interceptors, auto JSON, cancel tokens.
Next.js
Next.js is the default React framework at many companies. Know the rendering strategies and when to use each.
๐ฅ Rendering Strategies โ Must Know
This is the single most asked Next.js interview question. You need to explain not just what each strategy does, but when to pick one over another and the trade-offs involved.
CSR โ Client-Side Rendering
The server sends a minimal HTML shell with a <script> tag. JavaScript downloads, executes, and renders the page in the browser. The user sees a blank screen (or spinner) until JS finishes.
- Pros: simple deployment (static hosting), fast subsequent navigation (SPA), good for auth-gated content
- Cons: slow first paint (blank screen), poor SEO (crawlers see empty HTML), large JS bundle
- Use when: dashboards, admin panels, apps behind login where SEO doesn't matter
SSR โ Server-Side Rendering
The server generates full HTML on every request and sends it to the browser. The user sees content immediately, then JS hydrates to make it interactive. Each request hits the server.
- Pros: fast first paint, great SEO, always fresh data
- Cons: higher server cost (every request runs server code), slower TTFB than static, can't cache easily
- Use when: personalized pages (user profile, feed), content that changes per request, SEO-critical dynamic pages
SSG โ Static Site Generation
HTML is generated at build time and served as static files. No server computation at request time โ the CDN serves pre-built HTML instantly. Fastest possible TTFB.
- Pros: fastest performance (CDN-served), cheapest (no server), excellent SEO
- Cons: content is stale until next build, build time grows with page count, can't personalize
- Use when: blogs, documentation, marketing pages, any content that rarely changes
ISR โ Incremental Static Regeneration
The best of SSG and SSR. Pages are statically generated at build time, but can be revalidated in the background after a configurable interval. The first visitor after the interval gets the stale page (fast), and the next visitor gets the freshly regenerated page.
- Pros: static performance + fresh data, no full rebuild needed, scales to millions of pages
- Cons: first visitor after revalidation sees stale data, more complex mental model
- Use when: e-commerce product pages, news articles, content that updates periodically (not real-time)
SSR vs SSG
In modern Next.js (App Router), the default behavior is server-side rendering using React Server Components. However, Next.js automatically chooses between SSG and SSR based on data fetching, and developers can opt into client-side rendering using "use client".
// SSG โ generated at build time (default for static pages) export default function BlogPost({ post }) { return <article>{post.content}</article>; } // SSR โ generated on every request export const dynamic = "force-dynamic"; // App Router // or use generateMetadata / fetch with { cache: "no-store" } // ISR โ static + revalidate every 60 seconds export const revalidate = 60; // App Router // Next request after 60s triggers background regeneration // CSR โ client-only component "use client"; import { useEffect, useState } from "react"; export default function Dashboard() { const [data, setData] = useState(null); useEffect(() => { fetch("/api/stats").then(r => r.json()).then(setData); }, []); return data ? <Chart data={data} /> : <Spinner />; }
| Strategy | When HTML is generated | Best for | SEO | Data freshness |
|---|---|---|---|---|
| CSR | In the browser at runtime | Dashboards, auth-gated pages | โ Poor | Always fresh (client fetch) |
| SSR | On each request on the server | Personalized, dynamic pages | โ Good | Always fresh (server fetch) |
| SSG | At build time | Blogs, docs, marketing | โ Great | Stale until rebuild |
| ISR | Build time + background revalidation | E-commerce, news, semi-dynamic | โ Great | Fresh within revalidation window |
Routing & Middleware
App Router uses file-system routing with page.tsx, layout.tsx, loading.tsx, error.tsx. Middleware runs before every request โ use it for auth checks, redirects, and A/B testing. Server Components are the default; add "use client" only when you need browser APIs or state.
๐ก Pro Tip
When asked "SSR vs CSR," don't just list differences. Explain the trade-off: SSR gives better SEO and faster first paint but higher server cost. CSR gives faster navigation after initial load but worse SEO. Then mention ISR as the middle ground.
๐ Quick Revision
Quick Revision Cheat Sheet
CSR: Browser renders. Fast nav, poor SEO. Use for dashboards.
SSR: Server renders per request. Good SEO, higher server cost.
SSG: Build-time HTML. Best perf, stale until rebuild.
ISR: SSG + revalidation. Best of both worlds for semi-dynamic content.
Server Components: Default in App Router. No client JS. Add 'use client' for interactivity.
State Management
State management questions test whether you know when to use local state vs global state, and which tool fits which problem.
When to Use What
| Approach | Scope | Best for |
|---|---|---|
| useState | Single component | Form inputs, toggles, local UI state |
| Lifting state up | Parent + children | Shared state between siblings |
| Context API | Subtree | Theme, auth, locale โ infrequently changing data |
| Redux / Zustand | Global | Complex state with many consumers, frequent updates |
| React Query / SWR | Server state | API data with caching, revalidation, optimistic updates |
๐ฅ Key Interview Insight
The most common mistake is reaching for Redux/Context too early. Start with local state. Lift up only when siblings need it. Use Context for truly global, infrequently-changing data (theme, auth). Use a server-state library for API data. Redux is for complex client-side state that many components read and write.
๐ Quick Revision
Quick Revision Cheat Sheet
Local first: useState for component-scoped state. Don't over-engineer.
Lift up: Move state to the nearest common parent when siblings need it.
Context: For theme/auth/locale. Avoid for frequently changing data (causes re-renders).
Redux/Zustand: Global client state with many consumers. Zustand is simpler.
React Query: Server state. Handles caching, refetching, loading/error states.
Testing
Testing questions are less common in coding rounds but come up in system design and behavioral rounds. Know the concepts and when to apply each type.
Unit vs Integration vs E2E
| Type | What it tests | Tools | Speed |
|---|---|---|---|
| Unit | Single function/component in isolation | Jest, Vitest | Fast |
| Integration | Multiple components working together | React Testing Library | Medium |
| E2E | Full user flows in a real browser | Playwright, Cypress | Slow |
React Testing Library Philosophy
Test behavior, not implementation. Query by role, label, or text โ not by class name or test ID. If a user can't see it, don't test it. This approach makes tests resilient to refactors.
import { render, screen, fireEvent } from "@testing-library/react"; import Counter from "./Counter"; test("increments count on click", () => { render(<Counter />); const button = screen.getByRole("button", { name: /increment/i }); fireEvent.click(button); expect(screen.getByText("Count: 1")).toBeInTheDocument(); });
๐ Quick Revision
Quick Revision Cheat Sheet
Unit tests: Test one thing in isolation. Fast. Use Jest/Vitest.
Integration: Test components together. Use React Testing Library.
E2E: Test full user flows. Use Playwright/Cypress. Slow but high confidence.
RTL philosophy: Test behavior, not implementation. Query by role/text, not class.
Testing pyramid: Many unit tests, fewer integration, fewest E2E.
Frontend System Design
๐ฅ High-value for senior roles
Frontend system design rounds are increasingly common at FAANG. You're expected to design component architectures, handle scalability, and make trade-off decisions. Many of these topics have dedicated deep-dive pages โ use them.
Component Design
- Separate logic (smart/container) from presentation (dumb/presentational)
- Keep components small and focused โ single responsibility
- Use composition over inheritance
- Design for reusability: props for configuration, children for content
Folder Structure
Feature-based structure groups files by feature (auth/, dashboard/, settings/) rather than by type (components/, hooks/, utils/). This scales better for large teams because each feature is self-contained.
Scalability Patterns
- Virtualization for large lists (10K+ items)
- Pagination / infinite scroll for large datasets
- Debounced search for real-time filtering
- Optimistic updates for perceived performance
- Code splitting per route for bundle size
Handling Large Forms
- Use a form library (React Hook Form, Formik) for complex forms
- Validate on blur, not on every keystroke
- Split large forms into steps (wizard pattern)
- Use uncontrolled inputs with refs for performance-critical forms
Security
๐ก Pro Tip โ System Design Interview Framework
Use this structure: (1) Clarify requirements, (2) Define the component tree, (3) Identify state and data flow, (4) Handle edge cases (loading, error, empty), (5) Discuss performance and scalability, (6) Mention testing strategy. This framework works for any frontend system design question.
๐ Quick Revision
Quick Revision Cheat Sheet
Component design: Smart/dumb split. Single responsibility. Composition over inheritance.
Folder structure: Feature-based for scale. Group by domain, not file type.
Large lists: Virtualize with react-window. Paginate or infinite scroll.
Large forms: React Hook Form. Validate on blur. Wizard pattern for multi-step.
Security: Sanitize inputs (XSS). CSRF tokens. httpOnly cookies for auth.
Interview framework: Requirements โ Components โ State โ Edge cases โ Perf โ Testing.