#The frontend system design round
AddedIn a classic system design round the system is a fleet of servers and the question is how it survives a million users. In a frontend system design round the system is the app running in one user's browser, and the question is how it stays fast, correct and usable on a device you don't control, over a network you don't control, through every state the data can be in. You still draw boxes, but they are components, stores, caches and network calls; you still give numbers, but they are kilobytes, milliseconds on a slow phone and rows on a screen rather than queries per second.
The interviewer is looking for the same signals as in the backend round (scoping, trade-offs, communication, depth) applied to the client: Can you split a UI into components with clear ownership? Do you know where each piece of state should live? Can you design the API the client needs, not just consume one? Do you think about loading, empty, error, offline and slow states without being asked? Do you know what makes a page slow, inaccessible or insecure, and what you'd do about it?
A good mental model is that you are designing for ten thousand machines you will never see. Each one is a different phone with a different CPU, screen, network and assistive technology, running your code at the same time. A backend architect controls the hardware and scales it; you can't. Every byte you ship and every millisecond of main-thread work is paid for on the slowest of those machines, and every state you forget to design is a blank screen for someone.
A widely used structure for the round is RADIO: Requirements, Architecture, Data model, Interface, Optimisations. It's the frontend version of the backend framework (requirements → estimates → API → data model → design → deep dives), and it gives the interviewer a map of where you are.
The time split matters as much as the order. Candidates who fail this round usually spend twenty minutes on the component tree and never reach the part where they would have shown depth. Put a budget on it, the same way you'd estimate QPS:
// RADIO time budget for a 45-minute frontend design round (5 min intro, 5 min questions at the end).
const total = 45;
const reserved = { intro: 5, "your questions": 5 };
const design = total - Object.values(reserved).reduce((a, b) => a + b, 0);
// Share of the design time per RADIO step (a rule of thumb, not a law).
const radio = [
["R Requirements", 0.15],
["A Architecture", 0.2],
["D Data model", 0.15],
["I Interface (API)", 0.15],
["O Optimisations", 0.35],
];
let clock = reserved.intro;
for (const [step, share] of radio) {
const minutes = Math.round(design * share);
console.log(`${step.padEnd(20)} ${String(minutes).padStart(2)} min (${clock}–${clock + minutes})`);
clock += minutes;
}
console.log(`design time used: ${clock - reserved.intro} of ${design} min`);R Requirements 5 min (5–10)
A Architecture 7 min (10–17)
D Data model 5 min (17–22)
I Interface (API) 5 min (22–27)
O Optimisations 12 min (27–39)
design time used: 34 of 35 min- A 45-minute slot isn't 45 minutes of design: about 5 go on introductions and 5 on your questions at the end, so
designis45 − 10 = 35minutes. If your slot is 60 minutes, changetotaland the split scales with it. - Each RADIO step gets a share of those 35 minutes. Requirements, data model and interface get 15% each (
35 × 0.15 = 5.25, rounded to 5), architecture 20% (7 minutes) and optimisations 35% (12.25→ 12). - The
clockvariable turns the shares into wall-clock checkpoints: requirements end at minute 10, architecture at 17, data model at 22, interface at 27, optimisations at 39. Glance at the clock at those points; if you're at minute 25 and still drawing components, cut to the interface. - Rounding leaves one of the 35 minutes unassigned (
34 of 35), which is slack you will use: interviewers interrupt with questions. - The biggest share goes to optimisations on purpose. The first four steps are the skeleton any competent candidate draws; performance, accessibility, offline behaviour and security are where the interviewer learns your level. The shares are a rule of thumb, not a law: an interviewer who wants to go deep on the data model early is giving you a hint, so follow it.
What each step produces, using a news feed as the running example (the full design is in Design a news feed UI):
| Step | You produce | For a news feed |
|---|---|---|
| Requirements | Functional list, non-functional targets, what's out of scope | Read feed, like, comment, new-post banner; mobile web first; LCP under 2.5 s on a mid-range phone; no offline posting |
| Architecture | Component tree, where data enters, rendering strategy | FeedPage → FeedList (virtualised) → FeedItem → LikeButton; SSR the first page, CSR after |
| Data model | Client entities and stores, who owns each piece of state | Normalised posts, users, comments by id; feed = list of post ids plus a cursor |
| Interface | Server API the client needs, component props and events | GET /feed?cursor=…&limit=20, POST /posts/:id/like; FeedItem({ postId }), onLike |
| Optimisations | The deep dives, in order of impact | Virtualisation, image loading, optimistic likes, real-time banner, accessibility of the feed, XSS in user content |
Requirements deserve their own checklist, because the frontend has questions the backend round doesn't:
- Who and where: which devices (mobile web, desktop, both?), which browsers must work, which countries and languages, right-to-left scripts?
- What the network is like: fast office Wi-Fi or a phone on a train? Must it work offline, or only survive a flaky connection?
- SEO and sharing: does the page need to be indexed or produce link previews? That decides rendering more than anything else.
- Data shape and scale: how long can the list get (50 items or 50,000)? How big are items? How often does the data change, and must changes appear live?
- Accessibility and compliance: which WCAG level, which assistive technologies? Is there content regulation (cookie consent, data residency)?
- Who else is on the page: third-party scripts (ads, analytics), other teams' code, a design system you must use?
The backend round estimates QPS and storage. The frontend equivalent is bytes and round trips on a slow connection, because that is what decides whether you meet a loading target. Lighthouse's default mobile profile is a good stand-in for a mid-range phone: 150 ms round-trip time and 1.6 Mbps down, which its documentation describes as roughly the bottom quarter of 4G connections (Lighthouse docs/throttling.md, checked October 2026).
// First load of a news-feed page on Lighthouse's mobile profile (150 ms RTT, 1.6 Mbps down).
const RTT = 0.15; // seconds
const MBPS = 1.6;
const seconds = (kb) => (kb * 1024 * 8) / (MBPS * 1_000_000);
const posts = 20; // first page of the feed
const critical = {
html: 25, // KB, compressed
css: 30,
js: 180,
"feed JSON": posts * 1.2, // 1.2 KB per post
"3 images above the fold": 3 * 60,
};
const later = { "17 images below the fold": 17 * 60, "15 avatars": 15 * 4 };
const sum = (o) => Object.values(o).reduce((a, b) => a + b, 0);
for (const [name, kb] of Object.entries({ ...critical, ...later })) {
console.log(`${name.padEnd(25)} ${kb.toFixed(0).padStart(5)} KB ${seconds(kb).toFixed(2)} s`);
}
const criticalKb = sum(critical);
const connect = 3 * RTT; // DNS + TCP + TLS 1.3, roughly one round trip each
const requestRounds = 3 * RTT; // HTML, then CSS/JS, then feed JSON and images: three dependent round trips
console.log(`critical bytes: ${criticalKb.toFixed(0)} KB -> ${(connect + requestRounds + seconds(criticalKb)).toFixed(2)} s on the wire`);
console.log(`everything: ${(criticalKb + sum(later)).toFixed(0)} KB -> ${seconds(criticalKb + sum(later)).toFixed(2)} s of transfer`);html 25 KB 0.13 s
css 30 KB 0.15 s
js 180 KB 0.92 s
feed JSON 24 KB 0.12 s
3 images above the fold 180 KB 0.92 s
17 images below the fold 1020 KB 5.22 s
15 avatars 60 KB 0.31 s
critical bytes: 439 KB -> 3.15 s on the wire
everything: 1519 KB -> 7.78 s of transferseconds(kb)converts kilobytes to transfer time at 1.6 Mbps:kb × 1024 × 8bits divided by 1,600,000 bits per second. 180 KB of JavaScript is1,474,560 / 1,600,000 = 0.92 sof pure transfer, before the phone spends any time parsing it.criticalis everything the user needs to see the first screen: HTML, CSS, JavaScript, the first page of feed data (20 posts × 1.2 KB = 24 KB) and the three images above the fold (180 KB). Together that's 439 KB.- Bytes aren't the only cost. Before any byte flows, DNS, TCP and TLS cost about three round trips (
connect = 0.45 s), and the resources depend on each other: the HTML must arrive before the browser finds the CSS and JS, and the JS (or the HTML) before it asks for the feed and images. Three dependent rounds is another 0.45 s. Total:0.9 + 2.25 = 3.15 son the wire. - 3.15 s already misses a 2.5 s LCP target, so this estimate tells you what to optimise before you've drawn a component: the images and the JavaScript dominate the critical bytes. Server-rendering the first page (feed data in the HTML, one round trip fewer), smaller image formats and less JavaScript are the levers.
- The 17 images below the fold are 1,020 KB, more than double the critical path. If they loaded eagerly they would compete for the same 1.6 Mbps with the first screen; lazy loading them (
loading="lazy") is what keeps the critical path at 439 KB. Total transfer for everything would be 7.78 s.
Using the feed estimate above: which two changes to the critical bytes bring the first screen under 2.5 s, and by how much?
Show answer
Encode the three above-the-fold images as AVIF at about half the size (60 → 30 KB each) and cut the JavaScript from 180 to 120 KB. That saves 150 KB and lands at 2.38 s.
const RTT = 0.15, MBPS = 1.6;
const seconds = (kb) => (kb * 1024 * 8) / (MBPS * 1_000_000);
const overhead = 6 * RTT; // connect (3) + three dependent rounds (3)
const target = 2.5;
const before = 439; // KB of critical bytes from feed-estimate.mjs
const mustSave = Math.ceil(((overhead + seconds(before) - target) * MBPS * 1_000_000) / 8 / 1024);
const after = before - 3 * (60 - 30) - (180 - 120); // smaller hero images, less JS
console.log(`must save at least ${mustSave} KB`);
console.log(`after: ${after} KB -> ${(overhead + seconds(after)).toFixed(2)} s`);must save at least 127 KB
after: 289 KB -> 2.38 s- The fixed cost is six round trips at 150 ms: 0.9 s. That leaves
2.5 − 0.9 = 1.6 sfor transfer, which at 1.6 Mbps is 312.5 KB. The page has 439 KB, so it must lose at least 127 KB (mustSaverounds up so the result really fits). - The images: three at 60 KB → three at 30 KB saves 90 KB. Modern formats commonly halve JPEG sizes at similar quality, but measure on your own images; 50% is an assumption here.
- The JavaScript: 180 → 120 KB saves 60 KB, from route-level code splitting (see Loading strategy) or dropping a dependency.
439 − 90 − 60 = 289 KB, which is289 × 8192 / 1.6e6 = 1.48 sof transfer plus 0.9 s of round trips: 2.38 s. Removing a round trip (SSR the first feed page) would give another 0.15 s of headroom, which is the next thing to say.