Interview Handbook
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.
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.
RADIO is a popular mnemonic to help you not miss any steps. Proceed sequentially but be flexible to revisit steps when needed.
| Step | Goal | Suggested duration |
|---|---|---|
| R — Requirements | Understand the problem: functionality, users, constraints, devices. | ~5 minutes |
| A — Architecture | Draw the major components & data flow between them | ~12 minutes |
| D — Data Model | State definition: server data vs. client/UI state | ~8 minutes |
| I — Interface/API | The contract between client and server, and between components (props). | ~8 minutes |
| O — Optimizations | Performance, a11y, network, UX, security — deep dive. | ~12 minutes |
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.
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.
What is the core difference between frontend system design and backend system design?
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.
What does the product do? Users can: create posts, like, comment, infinite scroll, upload images...
How well does the product perform? Load speed, smoothness, offline capability, a11y, SEO.
Device (mobile/desktop), supported browsers, network (3G?), multilingual support, framework.
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.
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.
Which of the following requirements has the MOST DIRECT and SIGNIFICANT impact on choosing a rendering pattern (CSR/SSR/SSG)?
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.
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.
Knowledge of data & logic. Calling APIs, managing state, handling events. Not concerned with specific display.
Only receives props and renders UI. No business state. Easy to test, easy to reuse, easy to build Storybook.
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).
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.
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.
Core question: Where and when is HTML generated? This choice affects SEO, rendering speed, server cost, and complexity.
| Pattern | When is HTML created? | SEO | FCP speed | Good for |
|---|---|---|---|---|
| Client-Side Rendering (CSR) | In the browser, after JS runs | Weak (requires workaround) | First-time slow | Dashboard, post-login app, minimal SEO needed. |
| SSR (Server-Side Rendering) | For each request, on the server | Good. | Fast (TTFB depends on server) | Personalized page, data changes continuously, requires SEO. |
| SSG (Static Site Generation) | During build, before deployment | Best. | Fastest (CDN) | Blog, documentation, marketing, landing page |
| ISR (Incremental Static Regeneration) | Pre-built + periodic regeneration | Good. | Fast | E-commerce, many pages but updates are not continuous. |
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:
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.
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?
Performance isn't just about being "fast"—it's about being fast at the moments users perceive. Google measures this through Core Web Vitals.
Largest Contentful Paint. The time it takes for the largest element to appear. Good: < 2.5s
Interaction to Next Paint. Interaction response delay (replaces FID). Good: < 200ms
Cumulative Layout Shift. Layout shift level. Good: < 0.1
Instead of sending all JavaScript at once, only load the necessary parts. This is one of the most effective optimizations for FCP/LCP.
// Split bundle by route — load only when needed
const Dashboard = React.lazy(() => import('./Dashboard'));
function App() {
return (
<Suspense fallback={<Skeleton />}>
<Dashboard />
</Suspense>
);
}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.
Popular libraries: react-window, react-virtuoso, @tanstack/virtual.
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.
A page displaying a list of 50,000 products lags heavily when scrolling. What is the most optimal solution?
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 | GraphQL | |
|---|---|---|
| Fetch data | Multiple fixed endpoints | 1 endpoint, client declares the required fields themselves. |
| Over/Under-fetching | Prone to (over-fetching/under-fetching) | Minimize as much as possible. |
| Caching | Simple (HTTP cache by URL) | More complex (requires normalization) |
| Versioning | Commonly /v1, /v2 | Schema evolution, deprecate field. |
| "Matches" | Simple API, cache by URL | Complex UI with multiple data relationships. |
?page=2&limit=20. Easy to understand, supports page navigation. Drawback: data inconsistency when new items are inserted.
Stable with changing data, suitable for infinite scroll and feeds. Difficult to jump to arbitrary pages.
UI loads more when scrolling to the end (IntersectionObserver), typically using a cursor below.
"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.
When a user types "rea" quickly → "react", the request for "rea" may arrive AFTER "react" and overwrite the correct result. Solution:
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.
In an autocomplete field, how do you ensure the displayed results always match the latest keyword the user has typed?
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.
| Type | Description | The right tool |
|---|---|---|
| Server State | List of articles, user profiles (requires caching, sync, refetch) | React Query, SWR, RTK Query, Apollo |
| Local State | Input value, open/close dropdown | useState, useReducer |
| Global UI State | Theme, login, cart, global modal | Context, Zustand, Redux, Jotai |
| URL State | Selected tab, filters, search keywords | Router (query parameters) |
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 data flow makes debugging easier: every state change goes through action → reducer, so we always know "why the state changed."
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?
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.
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;
}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.
Allows users to compose child components flexibly, sharing state implicitly via Context.
<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>**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.
When designing an Autocomplete component for an interview, which of the following factors is OFTEN FORGOTTEN but highly valued?
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."
| The mechanism | Capacity | Characteristics | Used for |
|---|---|---|---|
| **Memory (JS)** | RAM | Lost when reloading | Cache runtime, state |
| localStorage | 5–10 MB | Synchronous, string only, long-lived. | Theme, small token, prefs |
| sessionStorage | ~5MB | Lost when closing the tab | Session-based temporary data |
| IndexedDB | Large (hundreds of MB+) | Asynchronous, store object | Offline data, big data |
| Cookies | ~4KB | Attach to every request | Session ID (HttpOnly), auth |
| Cache API | Large | Through Service Worker | Cache response, PWA offline |
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.
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.
You need to store several hundred MB of data for the app to work offline. Which mechanism is suitable?
Chat, notifications, stock prices, "typing..." — when to use polling, SSE, or WebSocket? Each has its own trade-offs in latency, data direction, and complexity.
| The mechanism | Afternoon | Complexity | Good for |
|---|---|---|---|
| Short Polling | The client asked: | Low | No rush to update, prototype. |
| Long Polling | Client asks (keep connection) | Average | Fallback when WebSocket is unavailable |
| SSE | Server → Client (one-way) | Low–Medium | Notification, feed, log stream |
| WebSocket | two-way | Cao | Chat, collaboration, gaming, trading |
Server push only? → SSE (lightweight, auto-reconnect, over HTTP). Need high-frequency bidirectional? → WebSocket. Not urgent/simple? → Polling. Always consider reconnection, heartbeat, and fallback.
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.
You are building a 1-on-1 chat feature with "typing..." indicators and instant two-way messaging. What is the most suitable mechanism?
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.
All operations can be performed using the keyboard. Focus is clearly visible, tab order is logical, and there is no focus trap.
Use correct semantic tags (button, nav, main) before considering ARIA. Proper HTML equals free accessibility.
aria-label, role, aria-live for dynamic content. Only use ARIA when HTML is insufficient.
Color contrast meets WCAG (4.5:1), respects prefers-reduced-motion.
"No ARIA is better than bad ARIA." Prioritize semantic HTML. A real `
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.
| Attack | The essence | Prevention |
|---|---|---|
| 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 |
| Clickjacking | Embed a transparent iframe site to trick clicks. | X-Frame-Options and frame-ancestors in CSP |
| Secret leak | API key/token embedded in client bundle | Never expose server-side secrets in client code; use a proxy instead. |