Frontend CoachResourcesQuestionsSystem Design
Sign inGet started

Contents

Platform

  • Introduction & RADIO
  • Clarify the requirements.
  • Overall architecture

Core Topics

  • Rendering Patterns
  • Performance
  • Networking & API
  • State Management
  • Component Design

Advanced

  • Caching & Storage
  • Real-time Updates
  • Accessibility & i18n
  • Security

Deep foundation

  • Browser Rendering
  • Event Loop & Async
  • Network Protocols
  • JS Engine (V8)
  • HTTP Caching & CORS

Deep technical techniques

  • Design System
  • Testing Strategy
  • Monitoring
  • Build & Deploy
  • Bundler comparison
  • Advanced Testing
  • Storybook & Docs
  • Module Federation
  • Distributed Tracing

Expand

  • Asset Optimization
  • PWA & Offline
  • Scalability

Practice

  • Case Study: News Feed
  • Case Study: Chat
  • Case Study: Autocomplete
  • Case Study: Photo Gallery
  • Case Study: Infinite Table
  • Case Study: Video Player
  • Case Study: Google Docs
  • Interview Checklist
  • Question Bank
Cheatsheet

Interview Handbook

Designing a Frontend System from A to Z

A visual, comprehensive, and easy-to-remember guide — covering requirement clarification, architecture, rendering, performance, real-time, and security. Includes diagrams, comparison tables, flashcards, and a question bank for self-assessment.

33Core Topic
54+Diagrams & Tables
70+Technical Interview Review Questions
7Full case study
01 — FOUNDATION

What is Frontend System Design & the RADIO thinking framework?

Unlike backend system design (which focuses on scaling servers, databases, and throughput), frontend system design focuses on user experience: client architecture, data flow, rendering, perceived performance, and codebase maintainability.

In an interview, you typically have 45–60 minutes to design a client-side product (e.g., News Feed, Autocomplete, Chat, Photo Gallery). The interviewer evaluates how systematically you think, not how many APIs you remember.

The RADIO framework — the backbone of every answer.

RADIO is a popular mnemonic to help you not miss any steps. Proceed sequentially but be flexible to revisit steps when needed. The RADIO framework is referenced from greatfrontend.com.

R Requirements Clarify the requirements. A Architecture Architecture & Components D Data Model Data model I Interface (API) API contract/props O Optimizations Optimization & Deep-Dive
The RADIO process
StepGoalSuggested duration
R — RequirementsUnderstand the problem: functionality, users, constraints, devices.~5 minutes
A — ArchitectureDraw the major components & data flow between them~12 minutes
D — Data ModelState definition: server data vs. client/UI state~8 minutes
I — Interface/APIThe contract between client and server, and between components (props).~8 minutes
O — OptimizationsPerformance, a11y, network, UX, security — deep dive.~12 minutes
Interview Tips
Think out loud and continuously confirm with the interviewer: "I'll assume X, does that sound okay to you?" They evaluate the communication process and trade-offs more than a single "correct" answer.
Key points to remember
FE System Design = Intentional Trade-offs. There is no perfect solution; every choice comes with a cost. Your job is to pick the right one for the right context and explain why.

Self-assessment quick check

🧩Quick Check

What is the core difference between frontend system design and backend system design?

02 — FOUNDATION

Requirements Clarification

This is the most important step, yet the most overlooked. Jumping straight into a solution without understanding the problem is the biggest trap in interviews.

Classification of requirements

⚙️

Functional

What does the product do? Users can: create posts, like, comment, infinite scroll, upload images...

📐

Non-functional

How well does the product perform? Load speed, smoothness, offline capability, a11y, SEO.

📱

Constraints

Device (mobile/desktop), supported browsers, network (3G?), multilingual support, framework.

Essential questions to ask.

  • Users & Scale: Who uses it? How many concurrent users? Primary device types?
  • Core features: Which features are mandatory for MVP? Which are "nice-to-have"?
  • Data: Does the data change frequently? Is real-time required?
  • Performance: Are there specific targets (e.g., LCP < 2.5s)?
  • SEO: Does the page need to be indexed by Google? (Significantly impacts rendering pattern)
  • Offline: Does it need to work when there is no network connection?
  • Cross-platform: Web-only or need to share logic with mobile app?
  • i18n / a11y: Do you need to support multiple languages, RTL, and screen readers?
Common mistakes
Spending too little (or too much) time on this step. The goal is to finalize the scope within ~5 minutes to establish a clear "contract" for the remainder of the session.
Practical Tips
Write the finalized list of requirements in a corner of the board (or a separate cell). When diving deeper, you can always point to it to explain why a feature was chosen or omitted.
🧩Quick Check

Which of the following requirements has the MOST DIRECT and SIGNIFICANT impact on choosing a rendering pattern (CSR/SSR/SSG)?

03 — FOUNDATION

Overall architecture & component separation

After understanding the problem, you outline the major "blocks" of the client system and how they communicate. Goal: separation of concerns — each part has a clear responsibility.

Common client architecture (layered)

Server / API / CDN REST · GraphQL · WebSocket CLIENT APPLICATION Data / Network Layer API client · caching · retry · auth tokens · normalize State / Store Layer Server cache · global UI state · derived/selector View Model / Controller Hooks, container components, event handlers, business logic View / UI Components Presentational, "dumb" components — only render from props
The layered architecture of a Single Page Application (SPA)

Idea: data flows from Server → Network → Store → View Model → View, while events flow in the opposite direction. Each layer can be tested and replaced independently.

Container / Presentational pattern

🧠

Container ("smart")

Knowledge of data & logic. Calling APIs, managing state, handling events. Not concerned with specific display.

🎨

Presentational ("dumb")

Only receives props and renders UI. No business state. Easy to test, easy to reuse, easy to build Storybook.

Why separate it like that?
Reusability (UI reused in multiple places), easy to test (logic separated from DOM), and easy to maintain (changing API does not affect UI and vice versa).

When to use Micro-frontends?

Micro-frontend splits a large application into multiple independent parts, each owned by a team (potentially using different frameworks). It is effective for **large organizations with many teams**, but increases complexity in build processes, duplicate bundles, and UX consistency.

Independent team deployment- Split a large codebaseDuplicate bundles- Integration Complexity- UX is most prone to inconsistency
🧩Quick Check

The main benefit of separating Container and Presentational components is improved separation of concerns: Container components handle logic, state, and data fetching, while Presentational components focus solely on rendering UI. This makes code more reusable, testable, and easier to maintain.

04 — CORE

Rendering Patterns: CSR · SSR · SSG · ISR

Core question: Where and when is HTML generated? This choice affects SEO, rendering speed, server cost, and complexity.

Visually compare the timeline.

CSR Empty HTML Load JS bundle Fetch data Render UI ✓ The user sees the content late. SSR Server renders full HTML. The user sees the content ✓ Hydration (attaching JS) → interaction Content appears first, interaction comes later. SSG Pre-built HTML at deployment CDN responds immediately ✓ Fastest, but data is static at build time.
When is HTML ready — CSR vs SSR
PatternWhen is HTML created?SEOFCP speedGood for
Client-Side Rendering (CSR)In the browser, after JS runsWeak (requires workaround)First-time slowDashboard, post-login app, minimal SEO needed.
SSR (Server-Side Rendering)For each request, on the serverGood.Fast (TTFB depends on server)Personalized page, data changes continuously, requires SEO.
SSG (Static Site Generation)During build, before deploymentBest.Fastest (CDN)Blog, documentation, marketing, landing page
ISR (Incremental Static Regeneration)Pre-built + periodic regenerationGood.FastE-commerce, many pages but updates are not continuous.

Hydration & modern variants

Hydration is the process of attaching JavaScript event handlers to server-generated HTML, turning a "static" page into an "interactive" one. The issue: it can be heavy and delay Time to Interactive (TTI). Techniques to reduce cost:

  • Streaming SSR: the server sends HTML in chunks, and the browser renders progressively instead of waiting for the entire page.
  • Progressive / Selective Hydration: hydrate critical parts first (e.g., the portion the user is currently viewing).
  • Islands Architecture: most of the page is static HTML, only a few interactive "islands" require JavaScript (Astro, Qwik).
  • RSC (React Server Components): components run on the server, no JavaScript is sent to the client for that part.
How to answer intelligently
Don't say "always use SSR." Instead, say: "Since the page needs SEO and content varies by user, I choose SSR; but for the post-login dashboard, CSR is sufficient because it doesn't need indexing." → This demonstrates context-driven thinking.

Flashcards: quick review of concepts

Flashcards3
🧩Quick Check

You are building a news blog that requires good SEO and extremely fast loading, with content updated several times a day. Which pattern is the most suitable?

05 — CORE

Performance & Core Web Vitals

Performance isn't just about being "fast"—it's about being fast at the moments users perceive. Google measures this through Core Web Vitals.

Core Web Vitals — 3 metrics to remember.

🎨

LCP

Largest Contentful Paint. The time it takes for the largest element to appear. Good: < 2.5s

👆

INP

Interaction to Next Paint. Interaction response delay (replaces FID). Good: < 200ms

📏

CLS (Cumulative Layout Shift)

Cumulative Layout Shift. Layout shift level. Good: < 0.1

Optimal levers — categorized by type.

Performance 4 optimization axes 🌐 Network Code splitting · lazy load Compression · HTTP/2 · CDN · preload 🖼️ Assets WebP/AVIF images · responsive Font subset · tree-shaking · minify ⚙️ Render / JS Virtualize list · memo Debounce · web worker · avoid reflow ✨ Perceived Skeleton · optimistic UI Prefetch · streaming · placeholder
Performance optimization map

Code splitting and lazy loading are techniques used to optimize the loading performance of web applications. Code splitting allows you to break your application into smaller chunks, which can be loaded on demand rather than all at once. This reduces the initial load time and improves the user experience. Lazy loading is a strategy that delays the loading of non-essential resources until they are needed. This means that components or modules are only loaded when they are required, further enhancing performance and reducing the amount of data that needs to be transferred during the initial load. Together, these techniques help in creating more efficient and responsive applications.

Instead of sending all JavaScript at once, only load the necessary parts. This is one of the most effective optimizations for FCP/LCP.

React lazy
// Split bundle by route — load only when needed
const Dashboard = React.lazy(() => import('./Dashboard'));

function App() {
  return (
    <Suspense fallback={<Skeleton />}>
      <Dashboard />
    </Suspense>
  );
}

List Virtualization — a key technique for handling long lists.

Rendering 10,000 rows at once will kill performance. Virtualization only renders items within the viewport (plus a small buffer), reusing DOM elements when scrolling.

Real list: 10,000 items viewport Only render. Actual DOM: ~10–15 items Lightweight DOM Smooth scrolling. Low RAM
Windowing / virtualization mechanism

Popular libraries: react-window, react-virtuoso, @tanstack/virtual.

Don't optimize prematurely
Always measure first, optimize later. Use Lighthouse, Performance tab, and React Profiler to identify real bottlenecks instead of guessing. Optimizing the wrong place wastes time and complicates the code.

Flashcards Performance

Flashcards3
🧩Quick Check

A page displaying a list of 50,000 products lags heavily when scrolling. What is the most optimal solution?

06 — CORE

Networking & Client-side API Design

Client-server communication significantly impacts UX. You need to know when to use REST, GraphQL, and how to handle pagination, retry, and race conditions.

REST vs GraphQL REST (Representational State Transfer) and GraphQL are two different approaches to building APIs. REST is an architectural style that uses standard HTTP methods (GET, POST, PUT, DELETE) to interact with resources. Each resource is identified by a unique URL, and the server responds with the entire resource representation. This can lead to over-fetching or under-fetching of data, as clients may receive more or less information than needed. GraphQL, on the other hand, is a query language for APIs that allows clients to request exactly the data they need. It provides a single endpoint and enables clients to specify the structure of the response. This reduces over-fetching and under-fetching issues, as clients can tailor their requests to their specific requirements. In summary, REST is resource-oriented and can lead to inefficiencies in data retrieval, while GraphQL is query-oriented and offers more flexibility in data fetching.

RESTGraphQL
Fetch dataMultiple fixed endpoints1 endpoint, client declares the required fields themselves.
Over/Under-fetchingProne to (over-fetching/under-fetching)Minimize as much as possible.
CachingSimple (HTTP cache by URL)More complex (requires normalization)
VersioningCommonly /v1, /v2Schema evolution, deprecate field.
"Matches"Simple API, cache by URLComplex UI with multiple data relationships.

Pagination: 3 strategies

🔢

Offset / Page

?page=2&limit=20. Easy to understand, supports page navigation. Drawback: data inconsistency when new items are inserted.

🔖

Cursor-based

Stable with changing data, suitable for infinite scroll and feeds. Difficult to jump to arbitrary pages.

♾️

Infinite Scroll

UI loads more when scrolling to the end (IntersectionObserver), typically using a cursor below.

This is a commonly asked question.
"Which pagination should social media feeds use?" → Cursor-based. Because feeds continuously have new posts inserted at the top; offset would cause duplicate/missed items when scrolling.

Handling Race Condition in search/autocomplete.

When a user types "rea" quickly → "react", the request for "rea" may arrive AFTER "react" and overwrite the correct result. Solution:

AbortController
let controller;
async function search(query) {
  controller?.abort();           // Cancel the old request.
  controller = new AbortController();
  const res = await fetch(`/api?q=${query}`, {
    signal: controller.signal
  });
  return res.json();
}

Combine with debounce (~300ms) to reduce the number of requests, and check "whether this request is still the latest" before calling setState.

Resilience: retry, timeout, backoff

  • Timeout: do not leave requests hanging indefinitely — cancel after N seconds.
  • Retry with exponential backoff: retry at 1s → 2s → 4s to avoid overwhelming the server.
  • Idempotency: For POST requests that create data, use an idempotency key to retry without creating duplicates.
  • Graceful degradation: serve stale cache / display user-friendly error messages instead of a white screen.
🧩Quick Check

In an autocomplete field, how do you ensure the displayed results always match the latest keyword the user has typed?

07 — CORE

State Management

Incorrect state management is the root cause of most frontend bugs. The key is to classify state types and choose the right tool for each — don't put everything into a single global store.

State classification — decision diagram

Where does this state come from? Server hay Client? from the server Server State DS articles, profiles, products... → React Query / SWR / RTK Query from the client/UI Used in multiple places? No. Local State input, toggle, hover... → useState / useReducer Yes. Global UI State theme, auth, modal, cart → Context / Zustand / Redux URL State filter, tab, search query → store in URL for sharing and correct back/forward behavior.
Decision tree: where to place state?

Four types of state to distinguish:

TypeDescriptionThe right tool
Server StateList of articles, user profiles (requires caching, sync, refetch)React Query, SWR, RTK Query, Apollo
Local StateInput value, open/close dropdownuseState, useReducer
Global UI StateTheme, login, cart, global modalContext, Zustand, Redux, Jotai
URL StateSelected tab, filters, search keywordsRouter (query parameters)
Common Misunderstandings
Many people stuff server data into Redux and write their own cache/loading/error logic. In reality, server state is very different from client state: it's asynchronous, can become "stale", and requires refetching & deduplication. Let React Query/SWR handle that.

One-way Flux / Redux

View Action Reducer Store Store updates → view re-renders
Unidirectional data flow

One-way data flow makes debugging easier: every state change goes through action → reducer, so we always know "why the state changed."

🧩Quick Check

A list of blog posts is fetched from the API and needs caching, auto-refetch when stale, and request deduplication. Which state type and tool should you use?

08 — CORE

Component Design & API Design

A good component is like a good function: easy to use correctly, hard to misuse. In interviews, designing a "props API" for a component (such as Autocomplete, Modal) is a very common question.

Principles of Component API Design

  • Single Responsibility: each component does one thing well.
  • Composition over configuration: prioritize composing smaller components rather than a single massive component with 30 props.
  • Controlled vs Uncontrolled: allow both when appropriate (value + onChange, or defaultValue).
  • Sensible defaults: works immediately with minimal props.
  • Default accessibility: ARIA roles and focus management are built-in.

Example: design an API for the Autocomplete component.

Props API
interface AutocompleteProps<T> {
  // Data & How to Retrieve It
  fetchOptions: (query: string) => Promise<T[]>;
  // Custom display
  renderOption?: (item: T) => ReactNode;
  getOptionLabel: (item: T) => string;
  // Behavior
  debounceMs?: number;        // default 300
  minChars?: number;          // default 1
  // Controlled
  value?: T | null;
  onChange?: (item: T | null) => void;
  // State
  loading?: boolean;
  emptyMessage?: string;
}
Advantages during the interview
When designing Autocomplete, proactively mention: debounce, race condition (AbortController), caching results, keyboard navigation (↑↓ Enter Esc), ARIA (role="combobox", aria-activedescendant), and virtualization for long lists. This is the checklist interviewers expect.

The Compound Components pattern is a design pattern in React that allows you to create components that work together as a single unit. This pattern enables better composition and flexibility by allowing child components to communicate with their parent component without needing to pass props explicitly. It promotes a more intuitive API and enhances the reusability of components.

Allows users to compose child components flexibly, sharing state implicitly via Context.

Compound API
<Tabs defaultValue="profile">
  <Tabs.List>
    <Tabs.Trigger value="profile">Profile</Tabs.Trigger>
    <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="profile">...</Tabs.Panel>
  <Tabs.Panel value="settings">...</Tabs.Panel>
</Tabs>
Design thinking
Question: "What customizations will users of this component need?" Too few props → rigid. Too many props → hard to use. Compound components and render props/children help balance flexibility without exploding props.
🧩Quick Check

When designing an Autocomplete component for an interview, which of the following factors is OFTEN FORGOTTEN but highly valued?

09 — ADVANCED

Caching & Client Storage

Proper caching helps apps run faster and remain functional even on weak networks. However, "there are only two hard problems in computer science: naming things and cache invalidation."

Client-side cache layers

App / Memory React Query cache Storage localStorage / IDB Service Worker Cache API (offline) HTTP cache CDN / browser Server The closer to the app (left), the faster but the more prone to "stale" data → requires an invalidation strategy.
The journey of a request and potential caching points.

Choose client-side storage.

The mechanismCapacityCharacteristicsUsed for
**Memory (JS)**RAMLost when reloadingCache runtime, state
localStorage5–10 MBSynchronous, string only, long-lived.Theme, small token, prefs
sessionStorage~5MBLost when closing the tabSession-based temporary data
IndexedDBLarge (hundreds of MB+)Asynchronous, store objectOffline data, big data
Cookies~4KBAttach to every requestSession ID (HttpOnly), auth
Cache APILargeThrough Service WorkerCache response, PWA offline
Token security
Don't store sensitive tokens in localStorage if you're concerned about XSS — it's accessible via JavaScript. Cookies with HttpOnly + Secure + SameSite flags are safer for sessions, since JavaScript cannot read them.

Cache invalidation strategy

TTL (time-to-live)stale-while-revalidateCache-then-networkInvalidate on mutationETag / Last-Modified

Stale-while-revalidate (core of SWR/React Query): immediately returns stale data to the UI for smoothness, while fetching in the background to update → users see fast results and always have fresh data.

🧩Quick Check

You need to store several hundred MB of data for the app to work offline. Which mechanism is suitable?

10 — ADVANCED

Real-time Updates

Chat, notifications, stock prices, "typing..." — when to use polling, SSE, or WebSocket? Each has its own trade-offs in latency, data direction, and complexity.

Short Polling Client Server Polling — simple, costly in requests, delayed. SSE (Server-Sent Events) Open once. Server continuously pushes one-way — feed/notification aggregation WebSocket Two-way, full-duplex channel — chat, gaming, collaboration
Compare real-time mechanisms
The mechanismAfternoonComplexityGood for
Short PollingThe client asked:LowNo rush to update, prototype.
Long PollingClient asks (keep connection)AverageFallback when WebSocket is unavailable
SSEServer → Client (one-way)Low–MediumNotification, feed, log stream
WebSockettwo-wayCaoChat, collaboration, gaming, trading
How to quickly select
Server push only? → SSE (lightweight, auto-reconnect, over HTTP). Need high-frequency bidirectional? → WebSocket. Not urgent/simple? → Polling. Always consider reconnection, heartbeat, and fallback.

Optimistic UI for a real-time feel.

When liking a post, update the UI immediately before the server responds, then rollback if an error occurs. The user experiences instant feedback even with network latency.

🧩Quick Check

You are building a 1-on-1 chat feature with "typing..." indicators and instant two-way messaging. What is the most suitable mechanism?

11 — ADVANCED

Accessibility (a11y) and Internationalization (i18n)

A good system serves all users — including those using screen readers, keyboards, and people from different languages/cultures. This is what distinguishes an average candidate from an excellent one.

Core Accessibility Checklist

⌨️

Keyboard

All operations can be performed using the keyboard. Focus is clearly visible, tab order is logical, and there is no focus trap.

🏷️

Semantic HTML

Use correct semantic tags (button, nav, main) before considering ARIA. Proper HTML equals free accessibility.

🔊

ARIA & screen reader

aria-label, role, aria-live for dynamic content. Only use ARIA when HTML is insufficient.

🌗

Contrast & Motion

Color contrast meets WCAG (4.5:1), respects prefers-reduced-motion.

The first rule of ARIA
"No ARIA is better than bad ARIA." Prioritize semantic HTML. A real <button> already has built-in focus, keyboard, and role — no need for <div onClick> then patching with ARIA.

Internationalization (i18n)

  • String externalization: do not hardcode text; use translation keys.
  • Plural & gender: "1 item" vs "2 items" — use ICU MessageFormat.
  • Format according to locale: dates, numbers, and currency via the Intl API.
  • RTL: right-to-left support (Arabic, Hebrew) — use logical CSS properties (margin-inline-start).
  • Lazy-load language packs: only load the current locale to reduce bundle size.
Intl API
new Intl.NumberFormat('vi-VN', {
  style: 'currency', currency: 'VND'
}).format(1500000); // "1.500.000 ₫"
🧩Quick Check

What is the best way to create an accessible button?

12 — ADVANCED

Frontend Security

Frontend cannot fully protect itself (everything on the client side can be spoofed), but there are vulnerabilities that FE engineers must guard against: XSS, CSRF, and sensitive information exposure.

Common threats

AttackThe essencePrevention
Cross-Site Scripting (XSS)Inject malicious script into the page via unescaped input.Escape output, avoid dangerouslySetInnerHTML, use CSP, sanitize (DOMPurify).
CSRF (Cross-Site Request Forgery)Trick the browser into sending a request with cookies to another site.SameSite cookie, CSRF token, Origin check
ClickjackingEmbed a transparent iframe site to trick clicks.X-Frame-Options and frame-ancestors in CSP
Secret leakAPI key/token embedded in client bundleNever expose server-side secrets in client code; use a proxy instead.

XSS — the number one enemy of Frontend.

Attacker Inject App render If you do NOT escape Victim The script runs and loses the cookie. 🛡️ Block here: escape/sanitize + CSP + HttpOnly cookie
How XSS works & blocking points
React helps you (partially)
React automatically escapes values in JSX, so {userInput} is safe against XSS. Danger only arises when you use dangerouslySetInnerHTML — in that case, you MUST sanitize with DOMPurify first.
The golden rule
Never trust data from the client. All frontend validation is only for good UX; real validation must happen on the server. The frontend can be opened via DevTools and anything can be modified.
🧩Quick Check

When must you **mandatorily** sanitize HTML (e.g., using DOMPurify) in React?

13 — Deep Foundation

How does the browser render a page?

Understanding the render pipeline is key to optimizing performance in the right places. Every technique—such as avoiding layout thrashing, using transform instead of top/left, or leveraging content-visibility—stems from this foundation.

Critical Rendering Path — 6 Steps

HTML → DOM tree CSS → CSSOM tree Render Tree DOM + CSSOM Layout calculate size/position Paint Draw pixels Composite GPU layer fusion Fix geometry (width, top) → re-run from Layout (most expensive). Fix transform/opacity → Only Composite (cheap, runs on GPU).
From bytes to pixels on the screen

Reflow vs Repaint — why is it important?

TypeWhen does it occur?Cost
Reflow (Layout)Resizing, repositioning, adding/removing DOM elements, reading offsetHeight...Expensive — must recalculate the geometry of the entire tree.
RepaintChange color, visibility, box-shadow (no layout change)Average — pixel repaint
Composite-onlyChange transform, opacityCheapest — GPU compositing layers, skipping Layout & Paint
Layout Thrashing
Reading layout properties (offsetTop, getBoundingClientRect) immediately after writing to the DOM forces the browser to perform synchronous reflows repeatedly. To avoid this: batch all reads together before writing (read-then-write batching), or use requestAnimationFrame.
A high-scoring answer.
"For smooth 60fps animation, I only animate transform and opacity because they run on the compositor thread, do not block the main thread, and do not cause reflow." → demonstrates your understanding of the pipeline.
🧩Quick Check

Which CSS property, when animated, will NOT cause reflow or repaint, only composite?

14 — DEEP FOUNDATION

Event Loop & Asynchronous in JS

JavaScript runs on a single thread but can handle thousands of tasks thanks to the event loop. Understanding the order of microtasks vs macrotasks helps you debug those "hard-to-understand" bugs related to execution order.

Mechanism: Call Stack, Queue & Loop

Call Stack main() Synchronous execution LIFO Event Loop Microtask Queue Promise.then, queueMicrotask HIGH PRIORITY Macrotask Queue setTimeout, event, I/O Run one per loop iteration Each round: run through the entire stack → CLEAN UP microtasks → get 1 macrotask → render → repeat.
How does the event loop coordinate?

Execution order — classic example

JS
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// Output: 1, 4, 3, 2
// Synchronous (1,4) runs first → microtask (3) → macrotask (2)
**The Unforgettable Rule**
After each macrotask, the event loop clears the ENTIRE microtask queue before rendering and fetching the next macrotask. Therefore, Promise.then always runs before setTimeout, even if setTimeout is set to 0ms.
Trap: infinite microtask
If a microtask continuously creates new microtasks, rendering will suffer from starvation — the page freezes because the event loop never reaches the paint step. For repeated heavy tasks, use setTimeout/requestIdleCallback to yield the thread.
🧩Quick Check

The output of: setTimeout(()=>log('A'),0); Promise.resolve().then(()=>log('B')); log('C') is: C, B, A

15 — DEEP FOUNDATION

Network protocol: HTTP/1.1 → HTTP/3

Each HTTP version addresses a specific bottleneck. Understanding them helps you explain why bundling was once important, and why with HTTP/2, "file concatenation" is no longer necessary.

Compare the versions.

VersionKey featuresThe remaining issue
HTTP/1.11 request/response per connection at a time; must open multiple parallel connections.Head-of-line blocking; limit of ~6 connections per domain → requires bundling, sprites.
HTTP/2Multiplexing: multiple streams over a single connection; header compression; server push.Still HOL blocking at the TCP layer (losing one packet blocks the entire connection).
HTTP/3Runs on QUIC (UDP); each stream is independent; faster handshake.New, some older networks block UDP.

TLS handshake and why HTTPS is "slow the first time."

DNS IP TCP 3-way handshake TLS key exchange Request send & wait TTFB first byte Each step costs one round-trip. Use connection reuse, preconnect, HTTP/3 to cut down
The lifecycle of an HTTPS request
Practical Optimization
Use <link rel="preconnect"> for critical third-party domains (CDN, API), and dns-prefetch for less important domains. This completes DNS/TCP/TLS early, shaving off a few hundred milliseconds when the actual request occurs.
Common interview questions
"With HTTP/2, should you still bundle all JS into one file?" → No. Multiplexing eliminates the penalty for multiple small files, and actually benefits caching (fixing one module won't invalidate the cache for the entire bundle).
🧩Quick Check

HTTP/2 reduces the need to bundle multiple JS files into one large bundle because it supports multiplexing, allowing multiple requests to be sent concurrently over a single TCP connection. This eliminates the overhead of multiple connections and reduces latency, making it efficient to load many small files individually rather than combining them into a single large file.

16 — DEEP FOUNDATION

JavaScript Engine: How V8 Works Internally

Understanding the engine helps you write "engine-friendly" code — faster without blind micro-optimization. V8 (Chrome, Node) is a prime example with its intelligent JIT compilation pipeline.

V8's compilation pipeline

Parser source → AST Ignition interpreter → bytecode Profiler Detect "hot" code (run multiple times) TurboFan JIT → machine code highly optimized Deoptimization: if an assumption is wrong (type change) → revert to bytecode
From source code to machine execution

Hidden Classes & Inline Caching

V8 does not know your object's "shape" in advance. It creates a hidden class (shape) for each object structure. Objects with the same shape share a hidden class → property access is extremely fast. Changing the shape (adding/deleting properties, adding in a different order) breaks this optimization.

JS
// GOOD: same shape, V8 can optimize
function Point(x, y) { this.x = x; this.y = y; }
const a = new Point(1, 2);
const b = new Point(3, 4); // The same hidden class as A.

// BAD: Adding a property later, different order → different shape
a.z = 5;            // Now you have a different shape.
delete a.x;         // Deleting properties breaks hidden classes → slow, avoid using it.

Garbage Collection — Generational

RegionWhat does it contain?GC (Garbage Collection)
Young generationMost new objects "die young".Scavenge — fast, frequent
Old generationObject survives multiple GC cyclesMark-Sweep-Compact — slower, less frequent
Common memory leaks in frontend development
Listener not removed, setInterval not cleared, closure holding large references, or detached DOM nodes still held by JavaScript. In React: forgetting cleanup in useEffect. Use DevTools → Memory to take heap snapshots and find leaks.
Don't blindly micro-optimize.
The engine is extremely intelligent; writing clear code with consistent shape and avoiding deletions in hot paths is sufficient. Measure with a profiler before optimizing — "premature optimization is the root of all evil."
🧩Quick Check

Why should all properties of an object be initialized in the same order, within the constructor?

17 — DEEP FOUNDATION

Detailed HTTP Caching & CORS

Proper caching is a "free" way to boost performance and reduce server load. CORS is something every frontend developer has struggled with — understanding it helps you debug quickly instead of "trial and error."

Important caching headers

HeaderEffect
Cache-Control: max-age=31536000, immutableCache for 1 year, no revalidation needed — for assets with content hash.
Cache-Control: no-cacheMust revalidate with the server before using (NOT "no cache")
Cache-Control: no-storeAbsolutely do not store — for sensitive data.
ETag + If-None-MatchContent fingerprinting; server returns 304 Not Modified if unchanged → saves bandwidth.
stale-while-revalidateUse the old version immediately, update silently — fast + gradually new
Classic caching strategy
Files with content hashes (e.g., app.a3f9.js) are cached immutably for 1 year because changing content changes the filename. The index.html file uses no-cache (always revalidate) so users receive the latest version. This is the foundation of safe deployment.

CORS — why is the request blocked?

Browser Server (API) 1. OPTIONS (preflight): "I want to send a PUT request, is header X allowed?" 2. Access-Control-Allow-Origin/Methods/Headers 3. Actual Request (PUT) — if preflight allows
Preflight request (OPTIONS) for "non-simple" requests
CORS is a browser mechanism.
CORS does not protect the server (requests still reach the server). It protects the user: the browser blocks JavaScript from reading responses from a different origin unless the server allows it. Therefore, CORS errors must be fixed on the server (by adding headers), not on the client.
Trick question
"Does no-cache mean no caching?" → No! no-cache = still stores but must revalidate (ETag) before using. "Don't store anything" is no-store. Confusing these two is a very common mistake.
🧩Quick Check

A CORS error appears in the console. Where is the correct place to fix it?

18 — ADVANCED TECHNIQUES

Design System & Component Library

As the product and team grow, UI consistency becomes a technical challenge. A design system serves as the "single source of truth," enabling dozens of developers to create consistent interfaces without stepping on each other's toes.

The layers of a Design System.

Design Tokens Color, spacing, font, radius — primitive variables. Primitives / Base components Button, Input, Text — use tokens, no business logic Patterns / Composite Form, Modal, DataTable — composed from primitives The real product
From token to product

Design Tokens — the heart of theming.

CSS
/* Token semantic points to the original token → changing the theme is easy. */
:root {
  --blue-500: #3b82f6;        /* primitive */
  --color-primary: var(--blue-500); /* semantic */
}
[data-theme="dark"] {
  --color-primary: var(--blue-300);
}
/* The component only uses semantic tokens, no hardcoded colors. */
.btn { background: var(--color-primary); }
Versioning & Breaking Changes
Component libraries should use semantic versioning. Changing a required prop constitutes a major version. Provide codemods for automatic migration, and issue deprecation warnings at least one version before removal. This maintains trust with the teams using the library.
Notable trade-off
"Flexible" components (many props) vs. "opinionated" components (fewer choices, high consistency). Good libraries often use compound components and slots to achieve both flexibility and consistency, rather than cramming in 30 boolean props.
🧩Quick Check

Why should "semantic tokens" (--color-primary) be separated from "primitive tokens" (--blue-500)?

19 — ADVANCED TECHNIQUES

Testing Strategy for Frontend

Tests are not about achieving 100% coverage, but about having confidence to refactor and deploy. The core question: what to test, at which layer, to catch the most bugs with the lowest maintenance cost.

Testing Pyramid (and the variant "Trophy")

E2E Integration Unit Few · Slow · Expensive The entire user flow. Sweet spot Multiple components working together Many · fast · cheap pure function/logic
Distribute tests by layer

What type of tests catch which bugs?

TypeWhat to testPopular tools
UnitPure function, util, reducer, custom hookJest, Vitest
Component / IntegrationComponent renders correctly, user interaction, callback invocationReact Testing Library
E2EComplete flow: login → purchase → paymentPlaywright, Cypress
Visual regressionDoes the UI break visually?Chromatic, Percy
The philosophy of Testing Library
"Test like how users interact" — find elements by role/label/text instead of class or internal test-id. Such tests are less brittle during refactoring and catch real bugs. Avoid testing implementation details (internal state, function names).
Trap: flaky tests
E2E tests become "flaky" due to timing issues (waiting for animations, network). Mitigation: wait for conditions (await expect(...).toBeVisible()) instead of sleep(2000), mock network when needed, and isolate tests (no dependency on execution order).
🧩Quick Check

According to the philosophy of React Testing Library, which element-finding method is most recommended?

20 — ADVANCED TECHNIQUES

Monitoring and Observability

"You cannot improve what you don't measure." After deployment, how do you know if real users encounter errors or if the page is slow? This is what distinguishes junior from senior engineers.

Three pillars to monitor.

🐞

Error Tracking

Catch JS runtime errors, promise rejections, and API errors. Group them by category, include stack trace and breadcrumbs. Tools: Sentry, Rollbar.

📊

RUM — Real User Monitoring

Measure Core Web Vitals (LCP, INP, CLS) from real users, segmented by device/network/region. This differs from lab tests (Lighthouse).

📈

Analytics & Funnel

At which step did the user drop off, and which feature was used? Connect performance to user behavior and revenue.

Performance Budget — set limits to avoid "drifting."

JSON
// Warn/block the build if it exceeds the threshold.
{
  "budgets": [
    { "resourceType": "script", "maximumError": "170kb" },
    { "metric": "LCP", "maximumWarning": "2500ms" }
  ]
}
Lab data vs Field data
Lab (Lighthouse) = simulated environment, stable, good for debugging. Field/RUM = real users, many device/network variations — these are the numbers that matter for SEO (Google uses CrUX). Must look at both.
Source maps for production
Upload private source maps (not public) to Sentry so that stack traces from minified code become readable. Do not deploy source maps publicly to avoid source code exposure.
🧩Quick Check

Google uses field data from the Chrome User Experience Report (CrUX) to evaluate Core Web Vitals for search rankings.

21 — ADVANCED TECHNIQUES

Build, Bundling & Deploy

The code you write doesn't run directly in the browser. Understanding bundlers helps you reduce bundle size, improve load speed, and deploy safely with feature flags and rollback.

What does a bundler do?

TechniqueEffect
Tree-shakingRemove dead code — requires ES modules for static analysis.
Code splittingSplit bundles by route/component, load on demand (dynamic import()).
MinificationMinify variable names, remove whitespace, dead code
Content hashingFile name with hash (app.a3f9.js) → permanent cache, content change = name change
Module FederationShare the runtime module between apps (micro-frontend platform).

CI/CD Pipeline & Deployment Strategy

Commit push PR CI lint·test·build Preview Temporary deployment Canary 5% of users Production 100% + built-in rollback Canary/blue-green Helps detect errors early on a small number of users, easy to rollback if errors occur.
From commit to production safely
Feature flags = decoupling deployment from release
Deploy code to production but disable the feature behind a flag. Gradually enable it from 1% → 50% → 100%. If an error occurs, turn off the flag immediately (no need to rollback the deployment). Also used for A/B testing.
A valuable question
How to deploy without breaking the experience for users on an old tab? → Use versioned assets with content hashes, keep old files for a period of time, and handle "chunk load failed" errors by reloading the page when a new version is detected.
🧩Quick Check

The main benefit of feature flags compared to using deploy/rollback is that they allow you to toggle features on or off at runtime without redeploying, enabling safer, more granular control over releases, faster rollback, and easier A/B testing or gradual rollouts.

22 — ADVANCED TECHNIQUES

Bundler comparison: Webpack · Vite · esbuild · Rollup

Each build tool is created to solve a different problem. Understanding them helps you choose the right tool and explains why Vite is "lightning fast" during development.

Overview comparison

ToolStrong pointsTechnical specifications
WebpackComplex app, massive loader/plugin ecosystemBundle everything; extensive configuration; module federation
ViteExtremely fast dev experience.Dev: Native ESM + esbuild, no bundling; Prod: Rollup
esbuildRaw build speed (written in Go)10–100× faster than JS tools; fewer advanced features
RollupBuild a libraryExcellent tree-shaking, clean output (ESM/CJS/UMD).

Why is Vite fast during development?

Webpack (dev) module A module B module C… Bundle COMPLETELYThen run it. Large project = long wait every time you start it. Vite (dev) BrowserModule requirements Vite servesESM on demand Instant startup, only build the module being viewed.
Webpack bundles everything upfront, while Vite serves ESM on demand.
The core idea of Vite
Modern browsers support native ES modules. During development, Vite does not bundle but lets the browser import each module individually; Vite only transforms the modules that are requested (using esbuild for extremely fast processing). As a result, server startup and HMR are nearly instantaneous, regardless of project size. For production builds, Vite bundles using Rollup for optimization.
How to choose a tool
For building libraries → Rollup (clean output, good tree-shaking). For building new apps → Vite (excellent DX). For legacy/complex projects requiring specific plugins → Webpack. For raw speed in the pipeline → esbuild.
🧩Quick Check

Why does Vite start the dev server almost instantly even with large projects?

23 — ADVANCED TECHNIQUES

Advanced Testing: Mock, Async & Contract

After mastering the testing pyramid, here are the techniques that distinguish developers who write "runnable" tests from those who write "reliable" tests. Testing incorrectly is worse than not testing at all, as it creates a false sense of security.

Mock vs Stub vs Spy — don't get confused.

ConceptWhat is it?Used when
StubReplace the function with a fixed return value.Need a dependency that returns predefined data (mock API returns an object).
SpyTrack how the ACTUAL function is called (number of times, parameters).To confirm whether the callback/analytics is called correctly.
MockA mock object has expectations about how it is called.Check interaction between modules, fail if called incorrectly.
FakeSimple in-memory database implementation.Replace the heavy system with a lightweight version.
Don't over-mock.
Mocking everything makes tests only verify "the code calls the mocked function correctly" — it doesn't reflect real behavior. Prioritize mocking at system boundaries (network, time, storage), and keep the logic within the test running as real code.

Testing asynchronous code & timers

JS
// Use fake timers to test debounce WITHOUT waiting in real time.
vi.useFakeTimers();
const spy = vi.fn();
const debounced = debounce(spy, 300);
debounced(); debounced(); // Call 2 times consecutively.
vi.advanceTimersByTime(300); // "tua" 300ms
expect(spy).toHaveBeenCalledTimes(1); // runs only once
Mock the network with MSW
Mock Service Worker (MSW) intercepts requests at the network layer, so components use real fetch without needing code changes. The same mock works for both testing and development — preventing tests from "knowing too much" about how the API is called.

Contract Testing — preventing "API changes without anyone knowing"

When frontend and backend develop independently, contract tests ensure both sides still "understand each other": the frontend declares what response format it expects, and the backend confirms it provides that exact format. If the backend changes a field, the contract test fails immediately — catching the error before production.

A high-scoring answer
I test based on user behavior, mock at the network boundary using MSW, use fake timers for debounce/throttle, and apply contract tests for the FE-BE boundary. → This shows you understand testing at the correct layers.
🧩Quick Check

The main difference between "spy" and "stub" is that a spy records information about how a function is called (e.g., number of calls, arguments), while a stub replaces a function with a predefined behavior or return value, typically to isolate the code under test.

24 — ADVANCED TECHNIQUES

Storybook & Component Documentation

Isolating components from the app enables efficient development, testing, and documentation. Storybook is the standard tool for building a living library for a design system.

Why develop isolated components?

🔬

Isolated development

Build each component state (loading, error, empty, full) without running the entire app or creating complex data.

📖

Living document

Each "story" is a live example. Designers and developers can view all variants, props, and usage patterns.

👁️

Visual regression

Take screenshots of each story, compare when changes occur → catch UI "breakage" that unit tests cannot detect.

♿

A11y audit

Addon to check accessibility directly in the story: contrast, ARIA, focus order.

Structure of a "story"

JS
// Button.stories.tsx — each export is one state
export default { title: 'UI/Button', component: Button };

export const Primary = { args: { variant: 'primary', children: 'Save' } };
export const Loading = { args: { loading: true } };
export const Disabled = { args: { disabled: true } };
Story = test + docs + dev simultaneously
A good story serves as documentation (for others to view), a playground (for developers to experiment), and input for visual regression tests. "Write once, use for three purposes" — this is why Storybook is worth investing in for teams with a design system.
Systems thinking
Mentioning Storybook when asked about design systems shows you're thinking about maintainability and collaboration, not just making code work. That's the mindset of someone building platform/infra for a team.
🧩Quick Check

What is the "3-in-1" benefit of a story in Storybook?

25 — ADVANCED TECHNIQUES

Module Federation — executing micro-frontends

This is the most common technical mechanism for implementing micro-frontends: allowing multiple apps to build and deploy independently while sharing code and rendering each other at runtime.

Host & Remote

Host (Shell App) General layout, router, remote loading. Remote: Cart Team A deploys separately. Remote: Search Team B deploys separately. Remote: Profile Team C deploys separately.
The host loads the module from a remote source at runtime.

Issues to address

ChallengeHow to handle
Duplicate dependencyDeclare shared resources (React, common libraries) to load once, avoiding multiple versions being downloaded.
Version mismatchSet singleton: true for React → prevents two React instances from conflicting.
Remote errors/slow performanceError boundary + fallback UI; don't let one remote crash the entire app.
UI consistencyShare the design system via a shared package.
The cost to pay
Module Federation adds significant complexity: managing shared dependencies, runtime versioning, difficulty debugging when remotes fail, and often worse performance than a monolith due to loading multiple runtimes. Only use it when the organizational benefits (independent teams) outweigh this cost.
Alternative
Not every micro-frontend requires Module Federation runtime. For lighter requirements: build-time integration (each team exports a package), iframe (strong isolation but limited UX), or web components. Choose based on the actual level of independence needed.
🧩Quick Check

Why is it necessary to declare React as a shared singleton in Module Federation?

26 — ADVANCED TECHNIQUES

Distributed Tracing & Alert Strategy

When a request flows through FE → API gateway → multiple services, how do you identify which step is slow? Tracing connects the pieces into a single picture. And alert properly to avoid being "ignored" due to too many alerts.

Trace, Span & Context Propagation

trace-id: abc123 FE — page load (1200ms) span: API gateway /feed (820ms) span: auth-service (110ms) span: db query (560ms) Looking at the waterfall, I can tell immediately. db query 560ms It’s a bottleneck — don’t guess blindly.
A trace consists of multiple spans that are sequential/nested.
The role of Frontend
The frontend initializes a trace-id and sends it as a header (e.g., traceparent per W3C Trace Context standard) with every API call. This links user-facing errors/latency to backend logs — making it feasible to debug "why user X experienced slowness."

Three Pillars of Observability

📝

Logs

Discrete events have timestamps. Good for investigating a specific case in detail.

📊

Metrics

Time-series aggregated metrics (p50/p95/p99 latency, error rate). Suitable for dashboards and alerts.

🔗

Traces

The journey of a request through services. Useful for identifying bottlenecks in a distributed system.

Properly using alerts — combating "alert fatigue"

The principleMeaning
Alert on symptoms, not on root causes.Report "error rate > 2%" (affects users) instead of "high CPU" (may be harmless).
Use percentile, not average.p95/p99 reflects the worst-case experience; averages hide the long tail.
ActionableEach alert must have a clear action; if nothing can be done, do not alert.
Reasonable thresholdToo sensitive → false alarms (cry wolf); too loose → missed real incidents.
Why p95 is more important than the average
"Average 200ms" sounds fine, but if p99 is 5 seconds, then 1% of users are suffering — often the most important ones (heavy data users). Seniors always look at tail latency, not just the average.
🧩Quick Check

Why should alerts be based on p95/p99 latency instead of average latency?

27 — EXTENSION

Optimize Images & Resources

Images often account for 50%+ of page weight. Proper optimization is the fastest way to improve LCP. This is a "low-hanging fruit" that many overlook.

Image formats — choose the right one.

FormatUsed whenNotes
AVIFScreenshots, background — top priority.Best compression, increasingly broad support.
WebPFallback for AVIFSmaller than JPEG/PNG by ~30%
SVGIcon, logo, vector imageInfinite resolution, small, can be styled with CSS.
JPEG/PNGThe final fallbackPNG for images requiring transparency.

Responsive images and lazy loading are techniques used to optimize web performance and improve user experience. Responsive images allow different image sizes to be served based on the user's device and screen resolution, ensuring that images are displayed at the appropriate size without unnecessary loading of large files on smaller screens. Lazy loading defers the loading of images until they are needed, typically when they come into the viewport. This reduces initial page load time and saves bandwidth, as images that are not immediately visible are not loaded until the user scrolls down to them. Implementing these techniques can significantly enhance the performance of a website, especially on mobile devices.

HTML
<!-- The browser automatically selects the best source + appropriate size. -->
<picture>
  <source type="image/avif" srcset="hero.avif">
  <source type="image/webp" srcset="hero.webp">
  <img src="hero.jpg" width="1200" height="630"
       loading="lazy" decoding="async" alt="...">
</picture>
Always set width & height
Declare width/height (or aspect-ratio) so the browser reserves space in advance — preventing layout shifts when images finish loading (improving CLS). Images without dimensions are the most common cause of CLS.
LCP images should NOT be lazy-loaded.
Images "above the fold" (typically LCP images) should use loading="eager" + fetchpriority="high", and even preload. Lazy-loading them will worsen LCP. Only lazy-load images below the fold.
🧩Quick Check

Which images should have `loading="lazy"` applied?

28 — EXTENSION

PWA, Service Worker & Offline-first

A Service Worker is a background proxy between the app and the network, enabling the app to work offline, load instantly on subsequent visits, and receive push notifications. It is the foundation of an "app-like" experience.

Where does the Service Worker stand?

App (page) fetch() Service Worker Block the request. Decide between cache/network Cache Storage Saved resources Network When new data is needed
SW is a proxy between the app and the network.

Cache strategy

StrategyHow it works"Matches"
Cache FirstFetch from cache first, only go to network if not available.Static resources (CSS, JS, fonts, logos)
Network FirstGo to network first, use cache on error.Data needs to be fresh (feed, price)
Stale-While-RevalidateReturn cache immediately, while updating in the background.Avatar, content changes infrequently — fast + gradual updates.
Trap: SW "stuck" on an old version
Service Workers have their own lifecycle (install → waiting → activate). If updates are not handled properly, users may get stuck on an old version. A strategy of skip-waiting combined with a "New version available, reload?" notification is needed for smooth updates.
Requirements of PWA
To be installable as an app, you need HTTPS, a Web App Manifest (name, icon, theme), and a Service Worker. When these are in place, the browser shows an "Add to Home Screen" prompt.
🧩Quick Check

The most suitable caching strategy for feed data that must always be fresh but still needs a fallback when offline is **stale-while-revalidate**. This approach serves cached data immediately (stale) while fetching fresh data in the background, ensuring the user sees content even without a network connection. For critical freshness, combine it with a short max-age and a network-first strategy, falling back to cache only when the network fails.

29 — EXTENSION

Frontend in a large team scale.

When a codebase has hundreds of developers, the issue is no longer individual technical skill but how to organize so that many people can work in parallel without conflicts or slowing each other down.

Monorepo vs Polyrepo

CriteriaMonorepoPolyrepo
Share the code.Easy — shared repo, simultaneous updates.Difficult — via npm package, versioning
Synchronize changesA PR that fixes both the app and the library.Multiple PRs, multiple release steps
Build/CINeed smart tools (Nx, Turborepo) to only build the changed parts.Simple, independent per repository.
Team boundariesNeed clear CODEOWNERSNaturally split by repo.

Micro-frontends — when to use them.

Don't use it just because it "sounds cool."
Micro-frontends solve organizational problems (multiple teams deploying independently), not technical problems. The cost: complexity in shared dependencies, UI consistency, and performance (loading multiple runtimes). Only worthwhile when teams are large enough to block each other.
Senior mindset
"I'll start with a modular monolith with clear module boundaries. Only split into micro-frontends when there's evidence that teams are blocking each other during deployment." → shows you prioritize simpler solutions first.
🧩Quick Check

The most reasonable reason to adopt a micro-frontend architecture is to enable independent development, deployment, and scaling of frontend features by different teams, reducing coordination overhead and improving maintainability for large-scale applications.

30 — PRACTICE

Case Study: Chat Application (like Messenger)

A classic interview question. A chat app combines nearly every topic: real-time, optimistic UI, virtualized list, offline, and state synchronization. We follow the RADIO framework.

R — Requirements

⚙️

Functional

Send/receive text messages in real-time, view conversation list, show delivery/read receipts (sent → delivered → read), typing indicators, media attachments (images/files), message history with infinite scroll upward.

📐

Non-Functional

Message latency < 500ms (P99), 99.9% delivery guarantee, supports 10k concurrent users, smooth scroll through thousands of messages, works on 3G networks.

📱

Constraints

Mobile-first (iOS/Android web), messages must queue offline and send on reconnect, support one-to-one and group chats up to 256 members.

  • Key clarification questions: One-to-one only or group chat? Max group size?
  • File/image attachments or text only? File size limit?
  • Push notifications when app is backgrounded?
  • End-to-end encryption required?

A — Architecture

UI (Chat View) Virtualized message list + composer State Store Normalize messages by ID. WebSocket Layer Real-time send/receive + reconnect Local Queue offline-safe message queue IndexedDB Offline history
Client architecture of chat

Key design decision: WebSocket (not SSE) because chat is bidirectional — the user both sends and receives over the same persistent connection. The Local Queue stores outgoing messages while offline, draining them in order when the WebSocket reconnects.

D — Data Model

TypeScript — Normalized state
interface Message {
  id: string;              // server ID (replaces tempId after ack)
  tempId?: string;         // client-generated, used for optimistic send
  conversationId: string;
  senderId: string;
  text: string;
  attachments: Attachment[];
  status: 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
  sentAt: string;          // ISO 8601
}

// Normalized state: O(1) lookup, easy status updates
interface ChatState {
  messages: {
    byId: Record<string, Message>;
    byConversation: Record<string, string[]>;  // conversationId → messageIds
  };
  conversations: { byId: Record<string, Conversation> };
  ui: {
    typingUsers: Record<string, string[]>;     // conversationId → userIds
    localQueue: Message[];                     // pending offline messages
  };
}

I — Interface (API & WebSocket Events)

API contract
// REST: initial data load
GET  /conversations
  → [{ id, participants, lastMessage, unreadCount }]

GET  /conversations/:id/messages?before=<cursor>&limit=30
  → { messages: Message[], prevCursor: string | null }

POST /media/upload-url
  → { uploadUrl: string; attachmentId: string }

// WebSocket JSON frames
// Client → Server
{ type: 'message', conversationId, tempId, text, attachmentIds }
{ type: 'read',    conversationId, upToMessageId }
{ type: 'typing',  conversationId, isTyping: true }

// Server → Client
{ type: 'message',  message: Message }          // new incoming message
{ type: 'ack',      tempId, serverId, status }   // delivery confirmation
{ type: 'typing',   conversationId, userId, isTyping }
{ type: 'read',     conversationId, userId, upToMessageId }

O — Optimizations

⚡

Optimistic send

Display message immediately with 'sending' status and tempId. On server ack, swap tempId → serverId. On error, mark 'failed' and show a retry button.

📜

Virtual list

Only render messages within the viewport. Load more on reverse scroll (infinite scroll upward). Maintain scroll anchor when prepending history — CSS overflow-anchor or manual scrollTop adjustment.

🔌

Reconnect & Sync

WebSocket drops → exponential backoff (1s → 2s → 4s). On reconnection, fetch missed messages using the last known cursor, then drain the local queue in order.

⌨️

Typing & Presence

Debounce typing events; emit at most once every 3s. Clear typing indicator after 5s of silence. Presence via heartbeat ping every 30s.

  • A11y: role="log" + aria-live="polite" on message list — screen readers announce new messages without interrupting the user.
  • Security: sanitize message content before rendering (prevent XSS via DOMPurify), validate file types server-side, use Content-Security-Policy headers.
  • Message ordering: always sort by server timestamp on display — client clocks drift, so client timestamps are only for optimistic ordering.
Ensure no data loss
Each message has a client-generated idempotency key (tempId). When resending due to network loss, the same ID is used — the server deduplicates. Combine a local queue with IndexedDB so messages composed offline are sent when connectivity is restored.
What the interviewer is listening for
WebSocket vs SSE justification (chat needs two-way → WebSocket), how new messages are handled while the user is scrolled up through history (don't auto-scroll — show a 'scroll to bottom' badge), and how message order conflicts are resolved (always sort by server timestamp).
🧩Quick Check

Why should you use a client-generated id (idempotency key) for each message?

31 — PRACTICE

Case Study: Autocomplete / Typeahead

A small but extremely common problem because it tests: debounce, race condition, caching, and keyboard accessibility — all in one component.

R — Requirements

⚙️

Functional

Show dropdown suggestions as user types, navigate suggestions with keyboard (↑↓ Enter Esc), select suggestion to fill input, support async remote data source, show loading/empty states.

📐

Non-Functional

Perceived response < 100ms for cached queries, < 300ms for fresh API requests. No stale/wrong results shown (race-condition-free). Accessible to keyboard and screen reader users.

📱

Constraints

Works with mobile virtual keyboards, generic API (supports any data type via props), must be composable — caller provides data-fetching logic.

  • Key clarification questions: Local static list or remote API? (changes architecture significantly)
  • Maximum number of suggestions shown at once? (affects virtualization need)
  • Should partial matches highlight the matching substring?
  • Multi-select or single-select?

A — Architecture & Data Flow

Type the key onChange Debounce ~300ms Cache? If available, reuse it. Fetch + AbortController Render dropdown a11y Cancel the old request when a new key is pressed → avoid race condition (old result overwrites new)
Flow from key press to result

D — Data Model

TypeScript — state shape
interface AutocompleteState {
  query: string;
  results: Suggestion[];
  selectedIndex: number;          // -1 = none, 0..n = highlighted item
  isOpen: boolean;
  isLoading: boolean;
  error: string | null;
  cache: Map<string, Suggestion[]>; // query → results, cleared on unmount
}

interface Suggestion {
  id: string;
  label: string;                  // displayed text
  metadata?: Record<string, unknown>; // caller-defined extra data
}

I — Interface (Props API)

Props API + server contract
interface AutocompleteProps<T extends Suggestion> {
  // Data source — caller owns the fetching logic
  fetchSuggestions: (query: string) => Promise<T[]>;

  // Display customization
  renderOption?: (item: T, isHighlighted: boolean) => ReactNode;
  getOptionLabel: (item: T) => string;

  // Behavior tuning
  debounceMs?: number;    // default 300
  minChars?: number;      // default 1 — no fetch until N chars typed
  maxResults?: number;    // default 10

  // Controlled mode
  value?: T | null;
  onChange?: (item: T | null) => void;

  // Slots
  placeholder?: string;
  emptyMessage?: string;
  loadingMessage?: string;
}

// Server endpoint
GET /search?q={query}&limit=10
  → { suggestions: [{ id, label, ...metadata }] }

O — Key Optimizations

Handling Race Conditions

Race condition + cache
useEffect(() => {
  if (query.length < minChars) return;

  // Check cache first
  if (cache.has(query)) {
    setResults(cache.get(query)!);
    return;
  }

  const controller = new AbortController();
  setIsLoading(true);

  fetchSuggestions(query, { signal: controller.signal })
    .then(results => {
      cache.set(query, results);   // store in session cache
      setResults(results);
    })
    .catch(e => { if (e.name !== 'AbortError') setError(e.message); })
    .finally(() => setIsLoading(false));

  // Cancel the in-flight request when query changes or unmount
  return () => controller.abort();
}, [query]);
Accessibility — the point many candidates miss
Use the ARIA combobox pattern: role="combobox" on the input with aria-expanded, aria-autocomplete="list", aria-controls pointing to the listbox. Set aria-activedescendant to the ID of the highlighted option. Keyboard: ↑↓ to navigate, Enter to select, Esc to close and restore original value, Tab to close.
Query caching
Cache results by query in a Map. When the user deletes and retypes the same word, return immediately from cache — no API call. Combine with 300ms debounce to reduce requests by ~80%. For very long lists (100+ suggestions), add virtualization to the dropdown.
🧩Quick Check

In autocomplete, a race condition occurs when the response from an earlier request arrives after the response from a later request, causing the UI to display outdated results. The correct way to handle this is to use a cancellation mechanism, such as an AbortController in fetch, or by tracking the latest request with a counter or timestamp to ignore stale responses.

32 — PRACTICE

Case Study: Photo Gallery (like Google Photos)

The problem of smoothly displaying thousands of images. Check: virtualization for the grid, lazy loading, responsive images, and lightbox. The core principle is "only work with what the user sees."

R — Requirements

⚙️

Functional

Browse photos in a responsive masonry/justified grid, infinite scroll to load more, click photo to open fullscreen lightbox with prev/next navigation, search/filter by date or album.

📐

Non-Functional

Above-the-fold images visible in < 1s, smooth 60fps scroll with 10,000+ photos, no CLS when images load, lightbox opens in < 200ms.

📱

Constraints

Various image aspect ratios (portrait, landscape, square), mobile-first with pinch-to-zoom in lightbox, bandwidth-conscious (serve thumbnails; full res only on demand).

  • Key clarification questions: Fixed grid (equal tiles) or justified layout (variable width, consistent row height)?
  • User's own photos only or shared albums?
  • Upload feature required or read-only gallery?
  • Max photos per gallery? (affects pagination strategy)

A — Architecture & Key Techniques

Viewport (actual rendering) Images outside the viewport: Do NOT render (keep only) height for correct scrolling Lightweight DOM There are ten thousand images.
Virtualized grid: only render images within the viewport.

Key design decisions: Server must return width and height for each photo so the browser can reserve the correct space before the image loads — essential for a CLS-free justified layout. Virtual DOM recycling keeps memory flat regardless of gallery size.

🖼️

Responsive thumbnails

Server generates multiple sizes (200px, 400px, 800px). srcset + sizes attributes ensure each device loads the correct resolution. Full-res loaded only on lightbox open.

⏳

LQIP blur-up

Display average color or blur-up placeholder while loading → creates a fast feel and prevents layout shifts. Server returns blurDataUrl alongside each photo.

♻️

DOM node recycling

When scrolling, reuse virtual nodes that have left the screen instead of creating new ones → reduces GC pressure and keeps memory stable.

🔍

Lightbox preload

When opening an image, preload the adjacent prev/next images to enable smooth navigation without latency.

D — Data Model

TypeScript — state shape
interface Photo {
  id: string;
  url: string;           // full-resolution URL (lazy-loaded in lightbox)
  thumbnailUrl: string;  // 400px thumbnail URL
  blurDataUrl: string;   // tiny base64 LQIP placeholder
  width: number;         // original pixel width — required for layout calc
  height: number;        // original pixel height
  takenAt: string;       // ISO 8601
  alt: string;
}

interface GalleryState {
  photos: {
    byId: Record<string, Photo>;
    displayOrder: string[];     // ordered photo IDs
    nextCursor: string | null;
  };
  layout: PhotoLayout[];       // pre-calculated positions (top, left, w, h)
  lightbox: {
    isOpen: boolean;
    currentId: string | null;
    isTransitioning: boolean;
  };
}

I — Interface (API)

API contract
// Paginated photo list
GET /photos?cursor=<c>&limit=30&albumId=<id>
  → {
      photos: [{ id, thumbnailUrl, blurDataUrl, width, height, takenAt, alt }],
      nextCursor: string | null
    }

// Full-res on demand (only fetched when lightbox opens)
GET /photos/:id
  → { ...Photo, url: string }   // includes full-resolution URL

// Upload (if required)
POST /photos/upload-url
  → { uploadUrl: string; photoId: string }
// Client PUTs binary directly to CDN via uploadUrl

O — Optimizations

  • Justified layout math: server returns width/height per photo. Client runs a layout algorithm (e.g., Google's bin-packing) to compute row heights and positions. Cache layout to avoid recalculation on every scroll.
  • Scroll anchor on load more: when fetching older photos above, record scrollTop before appending and restore it after to prevent the viewport jumping.
  • IntersectionObserver on bottom sentinel → fetch next batch when within 300px of fold.
  • Lightbox keyboard a11y: trap focus inside lightbox (Tab/Shift+Tab), ArrowLeft/ArrowRight navigate, Escape closes, role="dialog" + aria-label.
  • Pinch-to-zoom: use native CSS touch-action: manipulation + a library like react-zoom-pan-pinch for mobile lightbox.
What the interviewer pays attention to
How to calculate justified grid layout when images have different aspect ratios (server must return dimensions), how to maintain scroll position when loading more images, and what happens when a user opens a lightbox for a photo not yet loaded at full-res (show blur-up, load in background, fade in).
🧩Quick Check

Why is virtualization important for a gallery of 10,000 images?

33 — PRACTICE

Case Study: Infinite Data Table

A massive data table (like Airtable, financial spreadsheets) with millions of rows and many columns. Check: bidirectional virtualization, server-side sort/filter, sticky header/column, and inline editing.

R — Requirements

⚙️

Functional

Display millions of rows with 50+ columns, sort by any column, multi-column filter, inline cell editing, sticky first column + header row, column resize/reorder, row selection, CSV export.

📐

Non-Functional

< 16ms frame budget during scroll (60fps), < 500ms for sort/filter operations, support 1M rows × 50 columns without browser freeze, optimistic inline edits.

📱

Constraints

Desktop-first (financial users), rows may have variable height (text wrap), must not load all data into memory, real-time updates from other users editing.

  • Key clarification questions: Fixed or variable row height? (variable height dramatically increases virtualization complexity)
  • Real-time collaborative editing or single-user?
  • How many columns max? Are columns user-configurable?
  • Export format requirements? (CSV, Excel, PDF)

A — Architecture

Sticky header (always visible when scrolling vertically) Sticky col The actual render area. ~ rows × columns currently visible Millions of cells, but only a few dozen are created in the DOM at any given time.
Only render cells within the visible area (both rows & columns).

Key design decision: virtualize both rows and columns — a table with 50 columns and 1M rows could generate 50M DOM nodes without virtualization. The render window is typically ~20 rows × visible columns, with a small buffer. Libraries like @tanstack/virtual or ag-grid handle this well.

D — Data Model

TypeScript — state shape
interface Column {
  id: string;
  label: string;
  type: 'text' | 'number' | 'date' | 'select';
  width: number;         // pixel width — resizable
  pinned?: 'left';       // sticky column
}

interface Row {
  id: string;
  cells: Record<string, CellValue>; // columnId → value
  _version: number;                 // optimistic concurrency
}

interface TableState {
  columns: Column[];
  rows: { byId: Record<string, Row>; order: string[] };
  sort: { columnId: string; direction: 'asc' | 'desc' } | null;
  filters: Record<string, FilterValue>;
  cursor: string | null;   // for server-side pagination
  editingCell: { rowId: string; columnId: string } | null;
}

I — Interface (API)

API contract
// Never load all rows — server handles sort/filter/paginate
GET /rows
  ?sort=price:desc
  &filter={"status":"active","price":{"gte":100}}
  &cursor=<c>
  &limit=100
  → { rows: Row[], totalCount: number, nextCursor: string | null }

// Inline cell edit — optimistic with version check
PATCH /rows/:id
  body: { cells: { [columnId]: newValue }, _version: number }
  → { row: Row }           // 409 Conflict if version mismatch

// Real-time updates (SSE or WebSocket)
GET /rows/changes  (EventSource)
  ← data: { type: 'update', rowId, cells, _version }

O — Optimizations

Why use sort/filter on the server side?
Sorting 1 million rows on the client will block the main thread. The server (with indexed database queries) handles this in milliseconds and only returns the page of data the client needs. Client-side sort/filter is only appropriate when the entire dataset is already loaded (< ~5,000 rows).
  • Optimistic cell edit: update cell in local state immediately, send PATCH in background, rollback on 409 conflict (another user edited same row).
  • Debounce column resize: update column width in local state on every mouse move, but only persist to server on mouseup.
  • Web Worker for large exports: CSV generation for 100k rows blocks the main thread. Offload to a Web Worker or stream the download from the server.
  • Row selection: track selected row IDs in a Set, not booleans on each row — avoids re-rendering unselected rows on selection change.
  • A11y: use role="grid", role="row", role="gridcell", arrow key navigation between cells, announce sort direction changes with aria-sort.
Trap: dynamic row height
If each row has different height (text wrap), virtualization becomes much harder — you need to measure and cache actual heights after render, or use estimation with adjustment. Fixed row height is dramatically simpler. Confirm with requirements.
🧩Quick Check

With a table of 1 million rows, where should sorting/filtering be done?

34 — PRACTICE

Case Study: Video Player (like YouTube)

Smooth video playback at all network speeds. Tests understanding of adaptive bitrate streaming (HLS/DASH), buffering, and building custom controls with accessibility.

R — Requirements

⚙️

Functional

Play/pause/seek, volume + mute, playback speed (0.5×–2×), quality selection (auto + manual), fullscreen, captions/subtitles, chapter markers, watch progress saved server-side.

📐

Non-Functional

Playback starts within 2s, no rebuffering on stable 4G+, quality auto-adapts to bandwidth, supports 4K/1080p/720p/480p, works on iOS Safari (HLS native) and desktop (DASH via MSE).

📱

Constraints

Must work on iOS Safari (which requires HLS, not DASH), custom player UI (native controls are not brandable), no autoplay with audio (browser policy + a11y).

  • Key clarification questions: Live stream or VOD (Video on Demand)?
  • DRM (Digital Rights Management) required?
  • Captions mandatory (legal requirement in some regions)?
  • Picture-in-picture support required?

A — Architecture

In the same video, multiple levels. 1080p · chunk 2-10s 720p · chunk 480p · chunk ABR Player bandwidth measurement + buffer Select level Fast network → high, slow network → low The manifest (.m3u8 / .mpd) lists the available levels and chunks. The player loads the next chunk at the appropriate level. in real time. Network drops mid-stream → subsequent chunks load at a lower quality, no need to pause the video.
The video is divided into chunks of multiple qualities, and the player selects based on network conditions.

Key design decision: use an established ABR library (hls.js for HLS, dash.js for DASH) rather than implementing ABR from scratch. The player UI is a custom React layer on top of a native <video> element — native controls are hidden, custom ones overlay it.

D — Data Model

TypeScript — player state
interface PlayerState {
  // Playback
  isPlaying: boolean;
  currentTime: number;        // seconds
  duration: number;
  buffered: [number, number][]; // array of [start, end] buffered ranges

  // Quality
  quality: 'auto' | '1080p' | '720p' | '480p' | '360p';
  currentBandwidth: number;   // estimated kbps

  // Controls
  volume: number;             // 0..1
  isMuted: boolean;
  playbackRate: number;       // 0.5 | 1 | 1.25 | 1.5 | 2
  isFullscreen: boolean;

  // Captions
  captionsEnabled: boolean;
  activeCaptionTrack: string | null; // e.g. 'en', 'fr'

  // Persistence
  savedPosition: number;      // loaded from server on init
}

I — Interface (API)

API contract
// Video metadata + stream manifest
GET /videos/:id
  → { id, title, duration, thumbnailUrl,
      manifestUrl: string,   // .m3u8 (HLS) or .mpd (DASH)
      captions: [{ lang, label, url }],
      chapters: [{ startTime, title }] }

// Player saves progress every 15s (or on pause/unload)
POST /videos/:id/progress
  body: { position: number }  // seconds
  → 204 No Content

// Captions fetched on demand
GET <captionUrl>  → WebVTT file

// HLS manifest from CDN
GET <manifestUrl>  → m3u8 playlist
GET <chunkUrl>     → .ts segment (fetched by hls.js internally)

O — Optimizations

Buffer & States

ConceptMeaning
HLS / DASHTwo streaming standards divide video into chunks + manifest. HLS is popular on Apple, DASH is an open standard.
Buffer aheadPreload several chunks to prevent lag when the network fluctuates.
Startup timeStart at a low level to play fast, then gradually increase as the network is measured.
MSE (Media Source Extensions)The browser API that allows JavaScript to "load" chunks into a video element — the foundation of an ABR player.
The core trade-off of ABR
High quality vs. rebuffering risk. ABR always prioritizes uninterrupted playback over maximum quality — a single rebuffer stall kills user experience more than watching at 480p. Starting at low quality and ramping up is the standard strategy.
  • Preload next segment: buffer several chunks ahead of playhead to absorb bandwidth spikes. Tune buffer goal (e.g., 15s) for VOD; use smaller buffer for live streams to reduce latency.
  • Thumbnail scrubbing: pre-generate a sprite sheet of thumbnails at 1-per-second intervals. When user hovers the scrubber, show the sprite at that timestamp without seeking.
  • Save position on unload: use visibilitychange + pagehide events (not beforeunload — unreliable on mobile) to POST progress to server.
  • Reduced motion: respect prefers-reduced-motion — disable animated transitions in controls.
  • Performance: hide custom controls overlay during playback (opacity:0, pointer-events:none) — a transparent overlay intercepts events but adds no paint cost.
A11y for player — the full checklist
Space = play/pause, ←/→ = seek ±5s, ↑/↓ = volume, F = fullscreen, C = toggle captions. All controls need aria-label (e.g., aria-label="Play"). Progress bar = role="slider" with aria-valuenow, aria-valuemin, aria-valuemax. Never autoplay with audio — browser policy blocks it and it violates WCAG 1.4.2.
🧩Quick Check

When the user's network drops mid-stream, Adaptive Bitrate Streaming (ABR) works by detecting the decrease in available bandwidth through client-side monitoring. The player then automatically switches to a lower-bitrate video segment, reducing quality to prevent buffering. This process is seamless, as the video is divided into small chunks encoded at multiple bitrates, and the player requests the next chunk at the appropriate level based on real-time network conditions.

35 — PRACTICE

Case Study: Collaborative Editing (like Google Docs)

The hardest and most "valuable" problem: multiple users editing the same document in real-time without overwriting each other. Tests understanding of OT vs CRDT, presence, and conflict resolution.

R — Requirements

⚙️

Functional

Real-time collaborative text editing, see other users' cursors and selections, rich text formatting (bold, italic, headings, lists), comments/suggestions, version history with restore, offline editing (changes sync on reconnect).

📐

Non-Functional

Local keystrokes feel instantaneous (< 50ms), changes from other users appear within 500ms, convergence guaranteed — all users see the same document after edits settle, no data loss on network drop.

📱

Constraints

Support 50+ concurrent editors on the same document, works offline and syncs on reconnect, document must be stored persistently (not just in memory), must handle browser crashes gracefully.

  • Key clarification questions: Rich text (Google Docs) or plain text/code (like Replit)?
  • Max concurrent editors per document?
  • Is offline editing required? (fundamentally changes the architecture)
  • Comment threads and suggestions, or just live editing?

A — Architecture & Core Challenge

Core issue: concurrent editing

User A Insert "X" at position 5. User B Delete the character at position 3. OT / CRDT transform/merge operations Convergence Everyone = same result
Two people editing simultaneously — how to avoid overwriting?

Key design decision: use an established CRDT library (Yjs with y-websocket provider) rather than implementing OT/CRDT from scratch. Yjs handles conflict resolution, offline sync, and presence out of the box. Custom OT implementations are notoriously difficult to get correct.

D — Data Model

TypeScript — Yjs-based state
// Using Yjs CRDT
import * as Y from 'yjs';

const ydoc = new Y.Doc();
const ytext = ydoc.getText('content');  // shared text with CRDT semantics

// Client state
interface CollabState {
  syncStatus: 'synced' | 'pending' | 'offline';
  localVersion: number;
  collaborators: {
    [userId: string]: {
      name: string;
      color: string;         // unique color per user
      cursor: number | null; // character offset in document
      selection: { from: number; to: number } | null;
    };
  };
  comments: Comment[];
  pendingOps: Y.UpdateType[]; // buffered while offline
}

// Version history snapshot
interface Snapshot {
  id: string;
  createdAt: string;
  createdBy: string;
  description: string;
  stateVector: Uint8Array;   // Yjs state vector for this point in time
}

I — Interface (WebSocket Protocol)

WebSocket + REST API
// WebSocket (y-websocket protocol)
// Initial sync
Client → Server: { type: 'sync-step-1', stateVector: Uint8Array }
Server → Client: { type: 'sync-step-2', update: Uint8Array }  // missing ops

// Incremental updates
Client → Server: { type: 'update', update: Uint8Array }        // local edit
Server → Client: { type: 'update', update: Uint8Array }        // remote edit

// Presence (cursor/selection — ephemeral, not persisted)
Client → Server: { type: 'awareness', state: { cursor, selection, user } }
Server → Client: { type: 'awareness', changes: { [clientId]: state } }

// REST: document management
GET  /docs/:id          → { id, title, createdAt, collaborators }
GET  /docs/:id/history  → { snapshots: Snapshot[] }
POST /docs/:id/snapshot → create a named checkpoint
GET  /docs/:id/snapshot/:snapshotId → { content: Uint8Array }

OT vs CRDT — two approaches.

CriteriaOT (Operational Transformation)CRDT
The ideaTransform operations concurrently to maintain consistency.Self-merging data structure, no transformation needed.
Do we need a central server?Often requires (coordinating the order)Optional — suitable for P2P/offline
ComplexityComplex logic transformations are difficult to get right.Simpler in theory, but requires more metadata.
Used byGoogle Docs (traditional)Figma, many new apps, Yjs/Automerge
👥

Presence

Other users' cursor & selection sent via private channel (no persistence needed), throttle to reduce traffic.

📴

Offline editing

Save local operations when offline; when back online, merge using CRDT/OT — no data loss.

🕐

Version history

Save snapshot + operation sequence to rewind history and restore.

⚡

Local-first

Apply changes locally immediately (optimistic), sync in the background → no typing lag.

Why not use "last write wins"?
The simplest approach — last writer wins — overwrites the other person's text. OT/CRDT ensures both concurrent operations are preserved and all clients converge to the same state. This is the difference between a "toy" and a real product.

O — Optimizations

  • Local-first rendering: apply keystrokes to local Yjs document immediately (no round-trip) → zero typing lag. Sync to server in background.
  • Presence throttling: send cursor position updates at most 50ms intervals (throttle) — presence is ephemeral and lossy delivery is acceptable.
  • Offline buffer: store pending Yjs updates in IndexedDB. On reconnect, replay in order — Yjs merges them correctly with server state.
  • Large document performance: use a ProseMirror or CodeMirror editor that supports incremental rendering — never re-render the entire document on each change.
  • A11y: announce collaborator joins/leaves via aria-live="polite", expose cursor positions in a non-obtrusive way, ensure the editor remains keyboard-navigable.
  • Security: validate all operations server-side (reject ops from users without write permission), use CRDT for conflict resolution but authorize each op independently.
How to make an impression in the O part
State that this is a 'convergence in distributed systems' problem. Compare OT vs CRDT with clear trade-offs. Mention local-first + offline editing. Bring up presence throttling and the ephemeral vs. persistent data split. No need to code transform algorithms — understanding the principles and trade-offs is sufficient for the FE round.
🧩Quick Check

Why does collaborative editing NOT use the "last write wins" strategy?

36 — PRACTICE

Case Study: Designing a News Feed

Applying RADIO to a classic problem: designing a Facebook/Twitter-style feed. This is a template you can reuse for most feed-based questions.

R — Requirements

⚙️

Functional

View personalized feed (infinite scroll), create posts (text + images/video), like/comment/share, see new-posts notification pill, real-time update when new content arrives.

📐

Non-Functional

LCP < 2.5s on mobile 3G, 60fps smooth scroll, WCAG AA accessibility, works on intermittent connectivity, supports 1M+ posts in history.

📱

Constraints

60%+ mobile users (Android Chrome 90+, Safari 14+), post-login only so SEO is not a concern, internationalized UI (LTR + RTL languages).

  • Key clarification questions: Personalized algorithmic feed or strictly chronological?
  • Who can post — all users or verified accounts only?
  • Video support? (affects CDN strategy and player complexity)
  • Real-time updates required, or eventual consistency (5s poll) acceptable?
  • Offline read access required?

A — Architecture

App Shell + Router FeedContainer (smart) fetch + cache + pagination state PostComposer Create a post + optimistic VirtualizedList windowing + IntersectionObserver NewPostsPill "New X articles" (SSE/poll) PostCard (dumb) Header · Media · Actions like/comment (optimistic)
News Feed Architecture

Key design decisions: FeedContainer is ‘smart’ (owns all data fetching + pagination state); PostCard is ‘dumb’ (pure rendering from props) — trivially testable and reusable. VirtualizedList is critical: without it, 1,000+ posts render all DOM nodes at once and lag on mobile. Rendering pattern: CSR + React Query — feed is post-login and personalized, so SSR adds server cost with no SEO benefit.

D — Data Model

TypeScript — Normalized state
interface Post {
  id: string;
  authorId: string;
  text: string;
  mediaUrls: string[];       // CDN URLs for images/video thumbnails
  likeCount: number;
  commentCount: number;
  likedByMe: boolean;
  createdAt: string;         // ISO 8601
}

// Normalized client state — avoids duplication across feed + profile views
interface FeedState {
  posts: {
    byId: Record<string, Post>;
    feedOrder: string[];           // ordered post IDs
    nextCursor: string | null;
  };
  users: { byId: Record<string, User> };
  ui: {
    composerDraft: string;
    newPostCount: number;          // from SSE — drives the "N new posts" pill
  };
}
Why normalize?
Without normalization, updating a user’s name requires patching every post that references them. With normalization, a single users.byId[userId].name update propagates everywhere automatically — critical for large feeds where the same user may appear in hundreds of posts.

I — Interface (API)

API contract
// REST endpoints
GET  /feed?cursor=<c>&limit=10
  → { posts: Post[], users: User[], nextCursor: string | null }

POST /posts
  body: { text: string; mediaIds?: string[] }
  → { post: Post }

POST   /posts/:id/like    → { likeCount: number; likedByMe: true }
DELETE /posts/:id/like    → { likeCount: number; likedByMe: false }

// Two-step media upload — keeps API server lean
POST /media/upload-url
  → { uploadUrl: string; mediaId: string }  // pre-signed S3 URL
// Client PUTs binary directly to CDN via uploadUrl

// Server-Sent Events for real-time new post count
GET  /feed/updates  (EventSource)
  ← data: { type: ‘new_posts’, count: 3 }   // drives the "3 new posts" pill
Component API
// PostCard — dumb component, no internal async state
interface PostCardProps {
  post: Post;
  author: User;
  onLike: (postId: string) => void;
  onComment: (postId: string) => void;
  onShare: (postId: string) => void;
}

// FeedContainer — smart component, owns data fetching
const { data, fetchNextPage } = useInfiniteQuery({
  queryKey: [‘feed’],
  queryFn: ({ pageParam }) => fetchFeed({ cursor: pageParam }),
  staleTime: 30_000,   // 30s before background refetch
});

O — Optimizations

  • Virtualization: windowing with react-virtuoso — DOM stays at ~10–15 nodes regardless of feed length.
  • IntersectionObserver on sentinel element → auto-triggers next page fetch.
  • Images: LQIP blur-up placeholder + srcset for responsive sizes + explicit width/height → zero CLS.
  • Optimistic UI: like/comment update immediately, rollback on API error.
  • New posts pill: SSE pushes count → show pill, never auto-insert (would shift user’s reading position).
  • Resilience: stale cache shown offline, exponential backoff retry on reconnect.
  • Code splitting: PostComposer and media viewer lazily loaded — excluded from critical path bundle.
  • A11y: aria-live="polite" region for new-post count, role="article" on PostCard, keyboard-focusable like/comment buttons.
Optimistic UI — React Query
// Optimistic like — instant feedback, rollback on error
function handleLike(postId: string) {
  const prevData = queryClient.getQueryData([‘post’, postId]);

  queryClient.setQueryData([‘post’, postId], (old: Post) => ({
    ...old,
    likedByMe: !old.likedByMe,
    likeCount: old.likedByMe ? old.likeCount - 1 : old.likeCount + 1,
  }));

  likeMutation.mutate(postId, {
    onError: () => queryClient.setQueryData([‘post’, postId], prevData), // rollback
  });
}
CLS-free images
// CLS-free image with LQIP blur-up placeholder
// Server returns blurDataUrl: a tiny ~20×20 base64-encoded image
<img
  src={post.blurDataUrl}      // renders immediately — reserves space
  data-src={post.imageUrl}    // swapped in by IntersectionObserver
  width={post.imageWidth}
  height={post.imageHeight}   // correct aspect ratio → zero CLS
  loading="lazy"
  style={{
    filter: isLoaded ? ‘none’ : ‘blur(px)’,
    transition: ‘filter 0.s ease’,
  }}
  alt={post.imageAlt}
/>
How to "score points" in the O part
Don’t list everything. Pick 2–3 optimizations most relevant to the finalized requirements (e.g., because smoothness on mobile is needed → virtualization + responsive images) and dive deep into them. Depth over breadth.
🧩Quick Check

In the News Feed, why should we show a "X new posts" pill instead of automatically inserting new posts at the top of the list?

37 — PRACTICE

Interview Checklist — Print and Review Quickly

A set of "reflexes" to ensure you don't miss anything in the interview. Practice until it becomes second nature.

60-Minute Agenda

R · 5' A · 12' D · 8' I · 8' O · 12' + Q&A
Suggested time allocation

Always mention "trade-off" questions.

⚖️

Performance vs Complexity

Which optimizations are worth the effort? Measure before optimizing.

⚖️

CSR vs SSR

Balance between SEO/front-end performance and server cost/complexity.

⚖️

Real-time vs Simple

WebSocket is powerful but complex; polling is sufficient for many cases.

⚖️

Cache vs Freshness

Stale-while-revalidate balances both.

Signs of a strong candidate
(1) Ask clarifying questions before solving. (2) Draw clear diagrams. (3) Always mention trade-offs, never say "always use X". (4) Proactively address a11y, error handling, and performance. (5) Manage time wisely, know when to dive deep.

Comprehensive flashcard set

Flashcards6
38 — PRACTICE

Question bank summary

Mix multiple topics to test overall knowledge. Answer first, then view the explanation. Try to explain "why" yourself before clicking.

🧩Quick Check

The biggest issue with pure CSR for a marketing website is what?

🧩Quick Check

Which technique is most effective at reducing initial JS load time for large apps?

🧩Quick Check

GraphQL best solves the problem of over-fetching and under-fetching data in REST APIs.

🧩Quick Check

Where should the selected filter and tab be stored so that sharing the link and the back/forward buttons work correctly?

🧩Quick Check

The lightest and most suitable mechanism for displaying push notifications from the server is?

🧩Quick Check

Which attribute is used to announce dynamic content (e.g., "added to cart") to screen readers?

🧩Quick Check

The most secure way to store session tokens against XSS is?

🧩Quick Check

Reading `element.offsetHeight` immediately after changing the styles of multiple elements will cause what?

🧩Quick Check

Between Promise.then and setTimeout(fn, 0), which runs first?

🧩Quick Check

According to the testing pyramid, unit tests should make up the largest number of tests.

🧩Quick Check

The core difference between "lab data" and "field data" is what?

🧩Quick Check

Tree-shaking requires ES module syntax (import/export) to work effectively, as it relies on static analysis to determine which exports are used and which can be removed.

🧩Quick Check

The most common cause of CLS (Cumulative Layout Shift) related to images is missing explicit width and height attributes on image elements.

🧩Quick Check

What is the role of a Service Worker?

🧩Quick Check

Why should chat use WebSocket instead of SSE?

In conclusion
FE System Design has no single correct answer. Practice the mindset of: clarify → structure → trade-offs → deep dive. Revisit the diagrams and flashcards here regularly; spaced repetition is the best way to retain knowledge. Wishing you a successful interview! 🚀