> cs·fundamentals
interview 0% 24m read

Browser networking & app architecture

CORS and same-origin policy, SPA vs MPA vs SSR vs SSG, component architecture + virtual DOM, bundlers, and web-perf/a11y basics.

This chapter covers how the browser talks to servers under the same-origin security model, the four rendering architectures you’ll choose between, and the tooling — components, virtual DOM, bundlers — that modern frontends are built on. It closes with the performance and accessibility baselines every page should hit.

Same-origin policy and CORS

The same-origin policy is the browser’s foundational security boundary: JavaScript running on https://app.com may send a request to https://api.other.com, but it cannot read the response unless the other server explicitly allows it. Without this, any site you visited could read your logged-in Gmail or bank pages using your cookies. Origin = scheme + host + port; change any one and it’s cross-origin.

CORS is the server’s mechanism to opt in. When the browser makes a cross-origin request, it checks the response for headers like Access-Control-Allow-Origin. For “non-simple” requests (custom headers, methods like PUT/DELETE, JSON bodies), the browser first sends a preflight OPTIONS request asking permission; only if the server’s headers approve does the real request go through.

A browser and a cross-origin server exchanging two round-trips: first an OPTIONS preflight with Access-Control-Request headers and an approving response, then the real PUT request and its response.Browserapp.comServerapi.other.com1. OPTIONS (preflight) + Access-Control-Request-*2. 204 + Access-Control-Allow-* (approve)3. real PUT request4. 200 + data (now readable by JS)if step 2 doesn’t approve, step 3 never happens
FIG 1 · the CORS preflight For a non-simple request the browser sends an OPTIONS probe first. Only if the server's Allow-* headers approve does the real request leave the browser.

Cookies add a wrinkle: cross-origin fetch doesn’t send credentials unless you set credentials: "include" and the server returns Access-Control-Allow-Credentials: true with a specific (non-wildcard) origin. fetch is the modern promise-based API that replaced the callback-based XMLHttpRequest (XHR).

A credentialed cross-origin fetch — both sides must agree
// CLIENT — on https://app.com, calling a different origin
const res = await fetch("https://api.other.com/me", {
  credentials: "include",          // send cookies cross-origin
  headers: { "X-Token": "abc" },   // a custom header → triggers a preflight OPTIONS
});
# SERVER must answer the preflight AND the real request with matching headers:
Access-Control-Allow-Origin: https://app.com     # exact origin, NOT * when credentialed
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: X-Token

If any header is missing or Allow-Origin is * while credentials are included, the browser throws a CORS error and your await rejects — even though the server already ran the handler.

SPA vs MPA vs SSR vs SSG

The central frontend architecture decision: where and when does HTML get generated? All four are valid; the choice trades off time-to-first-content, interactivity, SEO, and server cost.

A timeline from build time to request time to in-browser, showing SSG at build time, SSR and MPA at request time on the server, and SPA rendering in the browser after JavaScript loads.build timerequest time (server)in browserSSGprebuilt filesSSR / MPArendered per requestSPAJS renders DOMfastest · cacheablestale until rebuildfresh + SEO + fast paintserver cost · hydrationapp-like, snappyslow paint · weak SEOSSR then ships JS to hydrate → interactive (best of both, at a cost)
FIG 2 · where the HTML is built The one axis that organizes all four: SSG builds HTML at deploy time, SSR/MPA build it per request on the server, SPA builds it in the browser after a JS bundle loads.
ApproachHTML built…NavigationStrengthsWeaknesses
MPA (multi-page)on the server, per request (traditional)full page reload per linksimple, SEO-friendly, fast first paintfull reloads feel slower; repeated server work
SPA (single-page)in the browser, from JS after loadclient-side routing, no reloadapp-like, snappy after loadslow first paint, weaker SEO, big JS bundle
SSR (server-side render)on the server per request, then hydratedclient-side after first loadfast first paint and SEO and interactivityserver cost; hydration adds complexity
SSG (static-site gen)at build time, served as static filesclient-side after first loadfastest, cheapest, CDN-cacheablecontent is stale until the next rebuild
The axis that matters: build HTML at request time (MPA/SSR), at build time (SSG), or in the browser (SPA).

The shorthand: SSG when content is mostly static (blogs, docs, marketing). SSR when you need fresh, per-request content and SEO and fast first paint (e-commerce, dashboards behind auth). SPA for highly interactive app-like tools where SEO doesn’t matter (internal dashboards). Modern meta-frameworks (Next.js, Astro, Remix, SvelteKit) blur the lines — they let you pick per-route.

Component architecture and the virtual DOM

React, Vue, and Angular all exist to solve the same two problems that raw DOM manipulation makes painful: keeping the UI in sync with state, and reusing UI. The shared model:

  • Components. Self-contained, reusable units pairing markup, state, and behavior. UI becomes a tree of components, each owning a slice of state.
  • Declarative rendering. You describe what the UI should look like for a given state; the framework figures out how to update the DOM. You stop writing imperative element.appendChild plumbing.
  • State management. Local component state covers most needs; when state must be shared across distant components, a store (Redux, Zustand, Pinia, or React Context) holds it centrally to avoid “prop drilling.”

The virtual DOM is React’s strategy for making declarative rendering fast. Re-rendering naively would mean rebuilding the entire page on every state change. Instead the framework builds a lightweight in-memory tree, diffs it against the previous version (“reconciliation”), and applies only the minimal set of real-DOM mutations. Real DOM operations are the expensive part (they reflow); diffing cheap JS objects first minimizes them.

Bundlers and transpilers

Browsers can’t run TypeScript, JSX, or hundreds of separate import files efficiently. The build toolchain bridges the gap, and each tool solves a distinct problem:

ToolCategoryProblem it solves
Babeltranspilerconverts modern JS / JSX / TS into syntax older browsers understand
Webpackbundlerwalks the import graph and packs many modules into few optimized files; mature, config-heavy
Vitebundler / dev serverinstant dev startup via native ES modules + esbuild; bundles with Rollup for production
Transpile = translate syntax; bundle = combine modules + tree-shake + minify.

A bundler resolves your module graph, removes dead code (tree-shaking), minifies, and emits a handful of files instead of hundreds of network requests. A transpiler translates newer or non-standard syntax (JSX, TypeScript, ESNext) down to JavaScript the browser runs. Vite won the dev-experience battle by serving source as native ES modules in development (near-instant startup) and only bundling for production.

Core Web Vitals and accessibility

Two baselines every production page should meet.

Core Web Vitals are Google’s user-centric performance metrics:

MetricMeasuresGoodDriven by
LCP — Largest Contentful Paintloading: when the main content appears≤ 2.5sslow server, render-blocking resources, large images
INP — Interaction to Next Paintresponsiveness: lag from interaction to UI update≤ 200mslong JS tasks blocking the main thread
CLS — Cumulative Layout Shiftvisual stability: unexpected layout jumps≤ 0.1images/ads without reserved dimensions
INP replaced FID as a Core Web Vital in March 2024.

Accessibility (a11y) basics — the high-leverage essentials:

  • Semantic HTML first — real <button>, <nav>, <main>, <label> give correct roles, keyboard support, and screen-reader landmarks for free.
  • alt text on meaningful images; empty alt="" for purely decorative ones so screen readers skip them.
  • ARIA only to fill gapsaria-label, role, aria-live when no native element fits. The first rule of ARIA is don’t use ARIA if a native element works; wrong ARIA is worse than none.
  • Keyboard navigation — every interactive element must be reachable and operable by keyboard, with a visible focus indicator. Don’t strip :focus outlines without a replacement.
  • Color contrast — text needs ~4.5:1 contrast against its background (WCAG AA).

01 Learning objectives

0 / 5 done

02 Curated reading

03 Knowledge check

knowledge check2 questions · pass ≥ 70%
  1. 01easy

    CORS exists to:

  2. 02medium

    Needs fast first paint, SEO, and per-request personalization. Best rendering approach?

04 Interview questions

browse all ↗

What gets asked on this topic — tap a card for how to approach it, the follow-ups, and the trap. Company tags are best-effort & sourced.

  • Meta mid concept very common What is the same-origin policy, and what problem does CORS solve?

    The same-origin policy stops a page on origin A (scheme + host + port) from reading responses from origin B by default — it limits how a document loaded from one origin can interact with a resource from another, which protects user credentials.

    CORS is the server's controlled opt-in: the server returns headers like Access-Control-Allow-Origin telling the browser it is allowed to expose the response to that origin. CORS does not turn off security — it lets a server selectively relax the same-origin policy for trusted callers.

    Red flag Saying CORS is a thing the client enables to bypass security. It is a server opt-in; the browser enforces it.

    source: MDN — Cross-Origin Resource Sharing (CORS) ↗
  • Commonly asked senior concept common What is a CORS preflight request, and what triggers one versus a 'simple' request?

    A preflight is an automatic OPTIONS request the browser sends before the real request to ask the server whether the actual call is allowed. It carries Origin, Access-Control-Request-Method, and Access-Control-Request-Headers.

    It is triggered by non-simple requests: methods other than GET/HEAD/POST, custom headers, or a Content-Type outside application/x-www-form-urlencoded, multipart/form-data, or text/plain (e.g. application/json).

    A simple request skips preflight — but the server still must return Access-Control-Allow-Origin for the script to read the response.

    Red flag Thinking simple requests need no CORS headers — they still need Access-Control-Allow-Origin to be readable. Forgetting application/json forces a preflight.

    source: MDN — Preflight request ↗
  • Commonly asked senior design common Compare SPA, MPA, SSR, and SSG, and the tradeoffs of each.

    MPA: server sends a full HTML page per navigation. Simple, great for content sites; full reloads between pages.

    SPA: one HTML shell, JS renders routes client-side. Fast in-app navigation; weak initial load and SEO, needs JS to render anything.

    SSR: server renders HTML per request, then hydrates on the client. Good first paint and SEO with dynamic data; higher server cost and TTFB.

    SSG: HTML built at deploy time, served from a CDN. Fastest and cheapest; only fits content that does not change per request (or use ISR to revalidate).

    Red flag Conflating SSR with SSG (request-time vs build-time), or claiming SPAs are inherently bad for SEO without mentioning SSR/prerendering as the fix.

    source: web.dev — Rendering on the web ↗
  • Meta mid concept common What is the virtual DOM, and is it actually faster than direct DOM manipulation?

    The virtual DOM is an in-memory tree of lightweight JS objects describing the UI. On a state change, the framework builds a new tree, diffs it against the previous one (reconciliation), and applies the minimal set of real DOM mutations.

    It is not magically faster than hand-optimized direct DOM writes — diffing has its own cost. Its value is a declarative model: you describe the target UI and let the framework batch updates and avoid redundant reflows, which is faster than naive re-rendering and far easier to reason about than manual surgery.

    Red flag Asserting the virtual DOM is always faster than direct manipulation. The real win is the declarative programming model plus batched updates.

    source: React docs — Preserving and Resetting State (reconciliation) ↗
  • Commonly asked mid concept common What problem do bundlers and transpilers solve? Distinguish Webpack/Vite from Babel.

    A bundler (Webpack, Vite, esbuild, Rollup) builds a dependency graph from your modules and produces a few optimized files — handling code-splitting, tree-shaking, asset imports, and minification. It solves 'too many modules and too many requests' and lets the browser load less.

    A transpiler (Babel, the TS compiler, SWC) converts source into a form browsers/runtimes accept: modern JS → older JS, JSX → createElement calls, TypeScript → JS.

    They complement each other: a bundler usually runs a transpiler step. Vite additionally serves native ES modules in dev for instant startup.

    Red flag Treating bundler and transpiler as synonyms. Babel transforms syntax; Webpack/Vite assemble and optimize the module graph.

    source: Vite — Why Vite ↗
  • Google mid concept very common What are the Core Web Vitals, and what does each measure?

    Three field metrics for real-user experience, judged at the 75th percentile:

    - LCP (Largest Contentful Paint) — loading; time for the largest content element to render. Good ≤ 2.5s.
    - INP (Interaction to Next Paint) — responsiveness; the latency of interactions. Good ≤ 200ms. INP became a stable Core Web Vital in 2024, replacing FID.
    - CLS (Cumulative Layout Shift) — visual stability; how much content unexpectedly shifts. Good ≤ 0.1.

    Typical fixes: optimize the LCP image / preload it; cut long tasks for INP; reserve space (width/height, aspect-ratio) for CLS.

    Red flag Citing FID as a current Core Web Vital (it was replaced by INP in 2024), or mixing up which metric covers loading vs responsiveness vs stability.

    source: web.dev — Web Vitals ↗
  • Commonly asked mid concept common What are the essentials of web accessibility (a11y) a frontend engineer must get right?

    Start with semantic HTML — native <button>, <a>, <label>, headings, and landmark elements give you roles, focus, and keyboard behavior for free.

    Key practices: meaningful alt text on images (empty alt="" for decorative ones); every form control associated with a <label>; full keyboard navigation with a visible focus indicator and logical tab order; sufficient color contrast; and ARIA only to fill gaps semantics cannot cover (custom widgets) — never to paper over a non-semantic <div>.

    Test with keyboard-only, a screen reader, and automated tools (axe/Lighthouse).

    Red flag Reaching for ARIA first instead of semantic HTML, removing focus outlines without a replacement, or treating alt text as optional.

    source: MDN — What is accessibility? ↗
  • Commonly asked mid concept common Compare cookies, localStorage, and sessionStorage for storing data in the browser.

    Cookies (~4KB) are sent to the server with every matching request. Best for auth/session tokens, ideally HttpOnly (JS cannot read them, mitigating XSS theft), Secure, and SameSite.

    localStorage (~5–10MB) is JS-only, persists until cleared, and is not sent to the server. Good for non-sensitive client state. Vulnerable to XSS, so never store tokens that must stay secret.

    sessionStorage is like localStorage but scoped to a single tab and cleared when it closes.

    Red flag Recommending localStorage for auth tokens (readable by any XSS), or thinking localStorage is sent to the server like cookies.

    source: MDN — Web Storage API ↗
  • Commonly asked mid trick occasional What is the difference between fetch and XMLHttpRequest, and does fetch reject on a 404?

    fetch is the modern Promise-based API; XMLHttpRequest is the older event/callback-based one. fetch is cleaner, streams responses, and integrates with AbortController for cancellation.

    Key gotcha: fetch only rejects on network failure, not on HTTP error status. A 404 or 500 still resolves — you must check response.ok (or response.status) yourself and throw if it is false.

    response.json() returns a Promise, so you await it twice (the response, then the body).

    Red flag Assuming a 404 lands in the catch block. It does not — fetch resolves; only network errors reject. Forgetting to check response.ok.

    source: MDN — Using the Fetch API ↗
  • Commonly asked senior concept common What is hydration in SSR, and why can it be costly? What problems does it cause?

    Hydration is the client-side step where a framework takes server-rendered HTML and attaches event listeners and reconstructs component state, making the static markup interactive. The server sends visible HTML fast (good first paint), then the browser must download the JS, re-run the components, and 'wire up' the existing DOM.

    It's costly because you effectively render twice — once on the server, once on the client — and the page can look ready but not respond to clicks until hydration finishes (the 'uncanny valley' / poor INP).

    Mitigations: less client JS, partial/progressive hydration, islands architecture (hydrate only interactive bits), streaming SSR, and server components that never ship to the client.

    What a strong answer covers
    • Hydration = attaching listeners/state to server-rendered HTML to make it interactive.

    • Work is duplicated: render on server, then re-render/wire-up on client.

    • Page can appear ready but be unresponsive until hydration completes (hurts INP).

    • Fixes: islands, partial/progressive hydration, streaming, server components.

    Red flag Thinking SSR alone makes a page interactive. The HTML is visible immediately, but interactivity waits for hydration.

    source: web.dev — Rendering on the web (hydration) ↗
  • Commonly asked mid concept common What is tree-shaking, and what does your code need to do for it to work?

    Tree-shaking is dead-code elimination at the module level: the bundler keeps only the exports you actually import and drops the rest, shrinking the bundle.

    It relies on ES modules' static structureimport/export are statically analyzable, so the bundler can trace which exports are used. CommonJS (require) is dynamic and resists shaking.

    For it to work well: use ESM, import named members (not the whole namespace), avoid modules with side effects at import time, and mark packages "sideEffects": false in package.json so the bundler can safely prune. A stray top-level side effect can force a whole module to be kept.

    What a strong answer covers
    • Tree-shaking removes unused exports to reduce bundle size.

    • Requires static ESM import/export; CommonJS require is too dynamic.

    • Side-effectful modules can't be safely dropped; "sideEffects": false signals safety.

    • Import named members, not import * as everything.

    Red flag Assuming any unused import is automatically dropped. Side effects, CommonJS, or namespace imports can defeat tree-shaking.

    source: MDN — Tree shaking ↗
  • Commonly asked mid concept common What is code-splitting and lazy loading, and how do they improve load performance?

    Code-splitting breaks one large bundle into smaller chunks that can be loaded on demand instead of all upfront. Lazy loading is fetching a chunk only when it's actually needed — typically via the dynamic import() expression, which returns a Promise and tells the bundler to emit a separate chunk.

    The payoff is a smaller initial bundle: less JS to download, parse, and execute before the page is interactive, which improves load time and INP. Common split points: per-route (load a route's code on navigation) and per-component (a heavy modal/chart loaded on first interaction).

    React pairs React.lazy(() => import('./X')) with <Suspense> for a fallback while the chunk loads.

    What a strong answer covers
    • Code-splitting = multiple chunks; lazy loading = fetch a chunk on demand.

    • Dynamic import() returns a Promise and creates a separate bundle chunk.

    • Shrinks the initial bundle → faster parse/execute → better TTI/INP.

    • Split by route and by heavy on-interaction components.

    Red flag Lazy-loading everything (request waterfalls, layout shift on load), or splitting code that's needed for first paint and delaying it.

    source: MDN — JavaScript modules (dynamic import) ↗
  • Commonly asked mid concept common What causes Cumulative Layout Shift (CLS), and how do you prevent it?

    CLS measures unexpected movement of visible content during loading — content jumping as late-arriving elements push things around. Good is ≤ 0.1 at the 75th percentile.

    Common causes: images/videos/ads without reserved space; web fonts swapping in and reflowing text (FOUT); content injected above existing content; and animating layout properties.

    Fixes: always set width/height (or aspect-ratio) on media so the browser reserves the box; reserve space for ads/embeds; use font-display: optional/swap plus size-matched fallbacks to minimize font reflow; and never insert content above what the user is viewing unless in response to an interaction.

    What a strong answer covers
    • CLS = sum of unexpected layout shifts; target ≤ 0.1 (p75).

    • Top cause: media without dimensions — set width/height or aspect-ratio.

    • Reserve space for ads/embeds and avoid injecting content above the fold.

    • Tame font swap (FOUT) with font-display and metric-matched fallbacks.

    Red flag Omitting image dimensions (relying on CSS alone) so the browser can't reserve space, or inserting banners above current content after load.

    source: web.dev — Cumulative Layout Shift (CLS) ↗
  • Commonly asked senior concept common What is XSS, what are the main types, and how do you defend against it?

    Cross-site scripting (XSS) is injecting attacker-controlled script that runs in a victim's page with the site's privileges (reading cookies, DOM, making requests as the user).

    Types: stored (malicious input saved server-side and served to others), reflected (script bounced off a request like a search param), and DOM-based (client JS writes untrusted data into the DOM, e.g. via innerHTML).

    Defenses: contextual output encoding/escaping (treat data as data, not markup); avoid innerHTML/dangerouslySetInnerHTML with untrusted input — use textContent; sanitize rich HTML with a vetted library (DOMPurify); set a strong Content-Security-Policy; and mark session cookies HttpOnly so injected JS can't read them.

    What a strong answer covers
    • XSS runs attacker script in the user's session context.

    • Three types: stored, reflected, DOM-based.

    • Primary defense: contextual output encoding; prefer textContent over innerHTML.

    • Layer with CSP, HTML sanitization (DOMPurify), and HttpOnly cookies.

    Red flag Treating input validation as sufficient. The core fix is output encoding for the right context; CSP and sanitization are defense-in-depth, not a single switch.

    source: OWASP — Cross Site Scripting (XSS) ↗
  • Commonly asked senior concept occasional What is CSRF, and how is it different from XSS? How do you defend against it?

    CSRF (cross-site request forgery) tricks a logged-in user's browser into sending an unwanted state-changing request to your site. It exploits the fact that browsers attach cookies automatically, so a forged request from another site rides the victim's session.

    Key difference from XSS: XSS is a *code injection* (attacker runs script in your page); CSRF is a *request forgery* that needs no script on your page — it abuses ambient cookie auth. XSS can defeat most CSRF defenses, so fixing XSS comes first.

    Defenses: SameSite cookies (Lax/Strict) so cookies aren't sent on cross-site requests; anti-CSRF tokens (a per-session secret the attacker can't read); and checking Origin/Referer. Avoid using GET for state changes.

    What a strong answer covers
    • CSRF: forged state-changing request riding the victim's auto-sent cookies.

    • XSS injects script; CSRF forges a request and needs no script on your page.

    • Defenses: SameSite cookies, anti-CSRF tokens, Origin/Referer checks.

    • Never perform state changes on GET; XSS can bypass CSRF tokens, so fix XSS too.

    Red flag Conflating CSRF with XSS, or thinking a CSRF token alone is enough when an XSS hole can simply read it.

    source: OWASP — Cross Site Request Forgery (CSRF) ↗
  • Commonly asked mid concept occasional How does HTTP caching work for assets? Explain Cache-Control, ETags, and cache busting.

    Cache-Control is the primary header. max-age=N lets the browser use a cached copy without revalidating for N seconds; no-cache means 'cache it but revalidate before use'; no-store means never cache; immutable promises the file won't change.

    ETags enable conditional revalidation: the server sends a content hash; the browser later sends If-None-Match, and the server returns a tiny 304 Not Modified if unchanged — saving the payload but not the round-trip.

    Cache busting combines both worlds: give bundled assets a content hash in the filename (app.a1b2c3.js) and serve them with max-age=31536000, immutable. When content changes, the filename changes, so you cache forever yet always serve fresh files. Keep the HTML entry point short-lived.

    What a strong answer covers
    • Cache-Control: max-age skips revalidation; no-cache revalidates; no-store never caches.

    • ETag + If-None-Match304 Not Modified avoids re-downloading unchanged bytes.

    • Cache busting: content-hashed filenames served immutable long-lived.

    • Hash the assets, keep the HTML short-lived so new asset URLs are discovered.

    Red flag Thinking `no-cache` means 'don't cache' (it means revalidate), or setting long max-age on un-hashed filenames so users get stale files.

    source: MDN — HTTP caching ↗
  • Commonly asked senior design common How would you optimize the Largest Contentful Paint (LCP) of a page?

    LCP marks when the largest in-viewport element (often a hero image or headline block) renders; good is ≤ 2.5s at p75. Optimize the four phases of its timeline:

    - TTFB: fast server / CDN, cache HTML, reduce redirects.
    - Resource load delay: make the LCP image discoverable early — put it in the markup (not JS-injected), fetchpriority="high", <link rel="preload">, and *don't* lazy-load it.
    - Resource load time: serve a right-sized, modern-format (AVIF/WebP), compressed image over a fast connection.
    - Render delay: cut render-blocking CSS/JS so the element can paint.

    The single biggest lever is usually ensuring the LCP image is requested as early as possible and not deferred.

    What a strong answer covers
    • LCP = render time of the largest viewport element; good ≤ 2.5s (p75).

    • Break it into TTFB, load delay, load time, render delay and attack each.

    • Don't lazy-load the LCP image; make it discoverable early (fetchpriority, preload, in-markup).

    • Serve right-sized modern-format images; reduce render-blocking resources.

    Red flag Lazy-loading or JS-injecting the hero image (the preload scanner can't find it early), or optimizing total page weight while ignoring the LCP element's own request timing.

    source: web.dev — Optimize Largest Contentful Paint ↗