Skip to content
RED SAGSkills

Frontend Developer interview questions and answers

30 questions interviewers really ask, with what a strong answer covers. Read them, then test yourself in a timed mock interview.

Start a Frontend Developer mock interview

Easy questions easy

1. Explain the CSS box model. What is the difference between content-box and border-box?

A strong answer covers:

  • Every element is a box of content, padding, border and margin
  • content-box: width applies to content only
  • border-box: width includes padding and border
  • border-box makes layouts easier to reason about
  • Margins are outside the box and can collapse vertically
Show model answer

Every element in CSS is drawn as a rectangular box with four layers: the content in the middle, then padding, then border, and finally margin outside. With the default box-sizing: content-box, the width and height you set apply only to the content, so padding and border are added on top. A 200px box with 20px padding becomes 240px wide. With box-sizing: border-box, the width includes padding and border, so the box stays 200px and the content shrinks. Most teams set border-box globally because it makes sizes predictable, especially in responsive grids. Margin is never part of the width; it is space between boxes, and vertical margins between block elements can collapse into one margin.

2. What is the difference between var, let and const in JavaScript?

A strong answer covers:

  • var is function scoped; let and const are block scoped
  • var is hoisted and initialised to undefined
  • let and const are hoisted but in the temporal dead zone
  • const cannot be reassigned but objects can still be mutated
  • Prefer const by default, let when reassigning
Show model answer

var is function scoped, so a var declared inside an if block is visible in the whole function. It is also hoisted and set to undefined, so you can read it before the line where it is declared without an error. let and const are block scoped, meaning they only exist inside the nearest curly braces. They are hoisted too, but they stay in the temporal dead zone until the declaration runs, so reading them early throws a ReferenceError. const means the variable binding cannot be reassigned, but if it holds an object or array you can still change its contents. In modern code I use const by default, let when I really need to reassign, and avoid var.

3. What is semantic HTML and why does it matter?

A strong answer covers:

  • Use elements that describe meaning, like header, nav, main, article, button
  • Helps screen readers and keyboard users
  • Improves SEO because search engines understand structure
  • Gives built-in behaviour, for example button is focusable
  • Easier for developers to read and maintain
Show model answer

Semantic HTML means using elements that describe what the content is, not just how it looks. For example, header, nav, main, article and footer for page structure, h1 to h6 for headings in order, and button for things you click instead of a styled div. It matters first for accessibility: screen readers use landmarks and headings to let people jump around the page, and a real button is focusable and works with Enter and Space for free. It also helps SEO because search engines understand the structure and the main content better. Finally, it makes code easier to read and maintain, and it needs less ARIA and less custom JavaScript.

4. In React, what is the difference between props and state?

A strong answer covers:

  • Props are passed from parent and are read-only in the child
  • State is owned and updated by the component itself
  • Changing state or receiving new props triggers a re-render
  • Lift state up when several components need it
  • Data flows one way, from parent to child
Show model answer

Props are inputs a parent passes to a child component, like function arguments. The child must treat them as read-only. State is data a component owns and can change over time, usually with useState or useReducer. When state changes, or when a parent passes new props, React re-renders the component. Data in React flows one way, from parent to child. If two siblings need the same data, I lift the state up to their closest common parent and pass it down as props, along with a callback so the child can ask the parent to update it. A good rule is to keep state as small as possible and derive everything else during render.

5. How do you make a web page responsive for mobile, tablet and desktop?

A strong answer covers:

  • Viewport meta tag
  • Mobile-first CSS with media queries
  • Flexible layouts with Flexbox and Grid, relative units
  • Responsive images with srcset and sizes
  • Test on real devices and DevTools device mode
Show model answer

First I make sure the page has the viewport meta tag so mobile browsers do not zoom out. I write CSS mobile-first: base styles for small screens, then min-width media queries to add layout for larger screens. For layout I use Flexbox and Grid with flexible units like percentages, fr, rem and clamp for font sizes, instead of fixed pixel widths. Container queries help when a component must adapt to its own space rather than the whole screen. For images I use srcset and sizes so phones download smaller files, and I make sure tap targets are large enough. Finally I test in DevTools device mode and on at least one real Android phone and iPhone.

6. What is the difference between Flexbox and CSS Grid, and when would you use each?

A strong answer covers:

  • Flexbox is one-dimensional: it lays items out in a row or a column
  • Grid is two-dimensional: it controls rows and columns together
  • Flexbox suits components like nav bars, toolbars and centring content
  • Grid suits page layouts and card galleries with aligned rows and columns
  • They are often combined: Grid for the page, Flexbox inside components
Show model answer

Flexbox is a one-dimensional layout system. It places items along one axis, a row or a column, and is great at distributing space and aligning items, for example a navigation bar, a toolbar of buttons, or centring something inside a box. Content size drives the layout, and items can wrap onto new lines. CSS Grid is two-dimensional. You define rows and columns together and place items into that structure, so things line up both across and down. I use it for page layouts with a header, sidebar and main area, and for card galleries, often with repeat(auto-fit, minmax(200px, 1fr)) so it responds without media queries. In practice I combine them: Grid for the overall page or a section, and Flexbox inside individual components. The rule of thumb is that if I care about alignment in both directions, Grid is easier; if I just need a line of items, Flexbox is simpler.

7. What is a closure in JavaScript? Give a practical example of where you use one.

A strong answer covers:

  • A closure is a function that remembers variables from the scope where it was created
  • The inner function can use those variables even after the outer function has returned
  • Used for private state, factories, and callbacks such as event handlers
  • React hooks and handlers rely on closures, which explains stale value bugs
Show model answer

A closure is a function bundled together with the variables from the scope where it was defined. Because of that, an inner function can keep reading and updating those variables even after the outer function has finished running. A simple example is a counter factory: function makeCounter() { let count = 0; return () => ++count }. Each counter returned has its own private count that no other code can touch. I use closures all the time without thinking about it: event handlers that remember an item id, debounce helpers that keep a timer id between calls, and module patterns for private state. In React, every render creates new handler functions that close over that render’s props and state. That is also the source of stale closure bugs, for example a setInterval callback created in an effect with an empty dependency array always seeing the first value. Knowing how closures work makes those bugs easy to explain and fix.

8. How do you make a form accessible?

A strong answer covers:

  • Every input has a visible label linked with for and id
  • Use the correct native input types and elements, and group related fields with fieldset and legend
  • Show errors in text, link them to fields with aria-describedby and set aria-invalid
  • The whole form works with a keyboard and has a logical focus order
  • Do not rely on placeholder or colour alone to convey meaning
Show model answer

I start with native HTML. Every field gets a visible label connected with for and id, because placeholders disappear when people type and are not reliable labels. I use the right input types such as email, tel and number, which also give mobile users the right keyboard, plus autocomplete attributes so browsers can fill details. Related options like radio buttons go inside a fieldset with a legend. For validation, I show the error as text next to the field, connect it with aria-describedby, set aria-invalid on the input, and on submit I move focus to the first invalid field or an error summary. I never use colour alone to show an error. I make sure everything works with the keyboard, the tab order follows the visual order, and the submit is a real button. Finally I test with a screen reader and with keyboard only, and run an automated check such as axe.

9. A button on a page does not work when clicked. How would you use browser DevTools to find the problem?

A strong answer covers:

  • Check the Console for JavaScript errors and warnings
  • Use the Elements panel to confirm the element exists and is not covered by another element
  • Check event listeners on the element and set breakpoints in the Sources panel
  • Use the Network panel to see whether the expected request is sent and what it returns
  • Reproduce step by step and narrow down the cause before changing code
Show model answer

First I reproduce the bug myself and open DevTools. I look at the Console, because an earlier JavaScript error often stops a handler from being attached. Next I inspect the button in the Elements panel: is it the element I expect, is it disabled, or is an invisible overlay sitting on top of it? The Event Listeners tab shows whether a click handler is actually attached. If it is, I set a breakpoint in the Sources panel, or use a click event listener breakpoint, and step through the code to see where it stops or takes the wrong branch. If the button should call an API, I open the Network panel to check whether the request goes out, its status code and the response body. That tells me whether the problem is in the frontend or the backend. Once I find the cause, I fix it, add a test if possible, and confirm the fix in the browsers where it was reported.

10. What is a Promise, and how does async/await relate to it?

A strong answer covers:

  • A Promise represents a value that will be available later, and is pending, fulfilled or rejected
  • then and catch register callbacks for success and failure
  • async functions always return a Promise
  • await pauses the async function until the Promise settles, without blocking the main thread
  • Errors are handled with try/catch, and independent work can run in parallel with Promise.all
Show model answer

A Promise is an object that stands for a result that is not ready yet, like the response of a network request. It starts as pending and then becomes either fulfilled with a value or rejected with an error, and you react with then and catch. async/await is syntax on top of promises that makes asynchronous code read like normal step-by-step code. An async function always returns a Promise. Inside it, await pauses that function until the promise settles and gives you the value, or throws the error so you can use try/catch. It does not block the browser; other code and rendering keep running. A common mistake is awaiting independent requests one after another, which makes them sequential. When they do not depend on each other, I start them together and use Promise.all, or Promise.allSettled if partial failure is acceptable. I also remember that fetch only rejects on network errors, so I check response.ok.

Medium questions medium

1. Explain the JavaScript event loop, including microtasks and macrotasks.

A strong answer covers:

  • JavaScript runs on a single main thread with a call stack
  • Async work is handled by browser APIs and queued callbacks
  • Microtasks (promises, queueMicrotask) run after each task, fully drained
  • Macrotasks (setTimeout, events, I/O) run one per loop turn
  • Rendering happens between tasks; long tasks block the UI
Show model answer

JavaScript in the browser runs on one main thread with a call stack. When code calls something async, like setTimeout or fetch, the browser handles the waiting and later puts a callback in a queue. The event loop takes one task from the task queue, runs it until the stack is empty, then drains the whole microtask queue, which holds promise callbacks and queueMicrotask callbacks. After that the browser may render a frame, and then it picks the next task. This is why a resolved promise callback runs before a setTimeout of 0. It also means a long synchronous task, or an endless chain of microtasks, blocks rendering and clicks, which hurts INP. I split heavy work into chunks or move it to a Web Worker.

2. How do you decide where to keep state in a React app: local state, context, a store, or a server-cache library?

A strong answer covers:

  • Keep state local by default and lift only when needed
  • Separate server state from client UI state
  • Use a server-cache library like TanStack Query for API data
  • Context for rarely-changing global values like theme or user
  • A store like Zustand or Redux for complex shared client state
  • URL for state that should be shareable, like filters
Show model answer

I start with local state and only move it higher when another component truly needs it. Then I separate two kinds of state. Server state is data from APIs: it needs caching, refetching, loading and error states, so I use TanStack Query or the framework data layer instead of putting it in a global store. Client UI state, like a modal being open, usually stays local. For values that many components read but that rarely change, like theme, locale or the logged-in user, React context works well. If there is complex client state shared across many screens, such as a multi-step editor, I use a small store like Zustand or Redux Toolkit, which also avoids context re-render issues. Filters and pagination often belong in the URL so users can share and refresh them.

3. A page has a poor LCP score. How would you investigate and fix it?

A strong answer covers:

  • Measure with field data (CrUX, RUM) and Lighthouse or DevTools
  • Identify the LCP element
  • Reduce server response time and use a CDN
  • Optimise and prioritise the LCP image (fetchpriority, preload, modern formats)
  • Remove render-blocking CSS and JavaScript, reduce bundle size
  • Avoid lazy-loading the LCP element
Show model answer

I first check real user data from CrUX or our RUM tool, because lab scores can differ. Then I use DevTools Performance panel or Lighthouse to find which element is the LCP, usually a hero image or a big heading. I break LCP into parts: server time, resource load delay, load time and render delay. For slow server time I look at caching and a CDN. If the LCP is an image, I make sure it is in the HTML, not injected by JavaScript, not lazy-loaded, has fetchpriority="high", and uses a properly sized AVIF or WebP file. I remove render-blocking CSS and scripts, inline critical CSS, and split large JavaScript bundles. Then I measure again and set a budget in CI.

4. What testing strategy would you use for a frontend application?

A strong answer covers:

  • Unit tests for pure logic and utilities
  • Component tests that test behaviour like a user would
  • End-to-end tests for critical user journeys
  • Mock network at the boundary, for example with MSW
  • Run tests in CI, keep them fast and not flaky
  • Add accessibility and visual checks where useful
Show model answer

I use a mix, sometimes called the testing trophy. Pure logic, like formatting and validation, gets fast unit tests with Vitest. Most of the value comes from component and integration tests with React Testing Library, where I render a component and interact with it like a user, finding elements by role and label rather than class names. I mock the network at the boundary with MSW so tests use realistic responses. For the few critical journeys, like sign-up, login and checkout, I write end-to-end tests with Playwright against a real build. Everything runs in CI on each pull request. I keep tests independent and avoid fixed waits to prevent flakiness, and I add automated accessibility checks like axe on key pages.

5. Explain client-side rendering, server-side rendering and static site generation. When would you use each?

A strong answer covers:

  • CSR: browser downloads JS and builds the page
  • SSR: server renders HTML per request, then hydrates
  • SSG: HTML built at build time and served from a CDN
  • Trade-offs: SEO, first load speed, server cost, freshness
  • Hybrid options like ISR and streaming
Show model answer

With client-side rendering the server sends almost empty HTML and a JavaScript bundle, and the browser builds the page. It is simple to host and good for app-like dashboards behind login, but the first load is slower and SEO is weaker. With server-side rendering the server builds HTML for each request, so users and crawlers see content quickly, then the page hydrates to become interactive. It suits personalised or frequently changing pages, but costs server time. Static site generation builds HTML at build time and serves it from a CDN, which is the fastest and cheapest, good for blogs, docs and marketing pages. Modern frameworks like Next.js mix these per route, with incremental regeneration and streaming for the parts that are slow.

6. What happens between typing a URL in the browser and seeing the page rendered? Focus on the frontend side.

A strong answer covers:

  • DNS lookup, TCP/TLS connection and the HTTP request for the HTML
  • The browser parses HTML into the DOM and CSS into the CSSOM
  • Scripts can block parsing unless they use defer, async or type module
  • DOM and CSSOM combine into the render tree, followed by layout, paint and compositing
  • Optimise the critical rendering path: less render-blocking CSS and JS, preload key resources
Show model answer

The browser resolves the domain with DNS, opens a connection with TLS, and sends an HTTP request; caches and a CDN can shorten this. As the HTML streams in, the parser builds the DOM and the preload scanner starts fetching CSS, scripts and images it finds. CSS is parsed into the CSSOM and is render-blocking, because the browser will not paint without styles. A classic script without defer or async pauses parsing while it downloads and runs. Once DOM and CSSOM are ready, the browser builds the render tree, calculates layout, which is the size and position of every box, then paints pixels into layers and composites them on the GPU. Later changes from JavaScript can trigger layout or paint again. To speed this up I reduce render-blocking resources, inline critical CSS, defer scripts, preload the LCP image and fonts, and keep the HTML small so the first paint happens early.

7. A React screen feels slow. How do you find and reduce unnecessary re-renders?

A strong answer covers:

  • Measure first with the React DevTools Profiler and the browser Performance panel
  • Move state down closer to where it is used, or lift content up as children
  • Keep props stable with useMemo and useCallback and use React.memo where it pays off
  • Avoid new object or array values in context and split contexts
  • Consider the React Compiler, virtualization for long lists, and transitions for heavy updates
Show model answer

I measure before changing anything. The React DevTools Profiler shows which components render, how often and why, and the Performance panel shows whether the cost is in rendering, layout or something else. Common fixes start with structure rather than memoization: I move state down to the component that needs it, so a typing field does not re-render the whole page, or pass heavy subtrees as children so they are not re-created. Next, I look at referential equality. Inline objects and functions passed to memoized children or as a context value break memoization, so I stabilise them with useMemo and useCallback, wrap expensive pure components in React.memo, and split large contexts. If the project uses the React Compiler, much of this memoization happens automatically. For long lists I use virtualization, and for heavy updates triggered by typing I use startTransition or useDeferredValue. I confirm each change with the profiler again.

8. How do you handle loading, error and empty states when a screen fetches data from an API?

A strong answer covers:

  • Model the request as explicit states: loading, success, empty and error
  • Use skeletons that match the layout to avoid layout shift, not just spinners
  • Show helpful error messages with a retry, and check response.ok
  • Handle race conditions and cancel stale requests with AbortController
  • Use a server-cache library such as TanStack Query or SWR for caching, retries and deduplication
Show model answer

I treat every request as having at least four states: loading, success with data, success but empty, and error. Modelling them explicitly, for example with a status field or a discriminated union in TypeScript, prevents impossible combinations like showing data and an error together. For loading I prefer skeletons shaped like the final content, which feel faster and avoid layout shift, and I avoid flashing a spinner for very fast responses. Empty states explain what is missing and offer an action, like "Create your first project". For errors, I check response.ok, show a clear message in plain language, offer a retry, and log details to monitoring. I also handle races: if the user changes filters quickly, I abort older requests with AbortController or ignore stale responses. In real projects I usually use TanStack Query or SWR, which provide caching, retries, deduplication and background refetching, so components stay simple.

9. How do you use TypeScript effectively in a React codebase?

A strong answer covers:

  • Type props with interfaces or types, including children and event handlers
  • Use discriminated unions for variants and state machines
  • Use generics for reusable components such as lists, tables and selects
  • Treat API data as unknown and validate it at the boundary, for example with Zod
  • Enable strict mode and avoid any; prefer inference where it is clear
Show model answer

I turn on strict mode from day one and avoid any, because each any switches off checking for everything it touches. For components, I type props with a type or interface, including children as React.ReactNode and callbacks with exact signatures, and I extend native element props, such as React.ComponentProps<"button">, so wrappers accept every standard attribute. I let inference handle local variables and hook state where it is obvious. For components that change shape, I use discriminated unions, for example a button that is either a link with href or a button with onClick, so invalid combinations do not compile. The same idea works for request state: loading, error with a message, or success with data. Reusable pieces like a Select or Table use generics so the item type flows through to callbacks. Finally, types do not exist at runtime, so I validate API responses at the boundary with a schema library like Zod and infer the types from the schema.

10. How would you handle user authentication and tokens in a single-page app or Next.js frontend?

A strong answer covers:

  • Prefer HttpOnly, Secure, SameSite cookies managed by the server over tokens in localStorage
  • Protect against CSRF with SameSite and CSRF tokens for state-changing requests
  • Short-lived access tokens with refresh handled server-side or by a backend-for-frontend
  • Protect routes on the server, not only by hiding UI on the client
  • Handle expiry gracefully: refresh, retry once, or redirect to login and keep the intended path
Show model answer

My default is to let the server manage the session in an HttpOnly, Secure, SameSite cookie. JavaScript cannot read it, so an XSS bug cannot simply steal the token, which is the main weakness of storing tokens in localStorage. Because cookies are sent automatically, I add CSRF protection: SameSite=Lax or Strict, safe GET endpoints, and CSRF tokens for sensitive actions. When an external identity provider is involved, I use the authorization code flow with PKCE, ideally through a backend-for-frontend so access and refresh tokens never reach the browser. On the frontend I treat auth state as data from the server, not as truth: hiding a menu item is only UX, and real checks happen in the API and in server-side route protection, such as middleware or server components in Next.js. When a session expires, I try a silent refresh once, otherwise redirect to login and return the user to the page they wanted.

Hard questions hard

1. How would you design and build a shared component library (design system) used by several product teams?

A strong answer covers:

  • Design tokens for colour, spacing and typography, with theming
  • Accessible, composable component APIs
  • Versioning with semver, changelogs and migration guides
  • Documentation and a playground like Storybook
  • Testing: unit, visual regression and accessibility
  • Governance and contribution process with design
Show model answer

I would start with design tokens for colour, spacing, radius and typography, stored as data and exported to CSS variables so themes like dark mode are just token swaps. Components should be accessible by default, so I build on proven primitives such as Radix or React Aria for focus, keyboard and ARIA behaviour. APIs should be small and composable, for example compound components and an asChild or slot pattern instead of dozens of props. I ship it as a tree-shakable package with TypeScript types, versioned with semver, changesets and clear migration notes for breaking changes. Storybook serves as documentation and a playground. Each component has interaction tests, visual regression tests and axe checks in CI. Finally, I set up governance with designers so teams can contribute without the library fragmenting.

2. A page must show a table with 50,000 rows and stay smooth while scrolling and filtering. How would you build it?

A strong answer covers:

  • Virtualise rows so only visible rows are in the DOM
  • Paginate or stream data from the server where possible
  • Do heavy filtering and sorting off the main thread or on the server
  • Memoise rows and keep props stable
  • Use transitions or deferred values to keep input responsive
  • Handle accessibility and variable row heights
Show model answer

Rendering 50,000 DOM rows will be slow, so the core idea is virtualisation: only render the rows visible in the viewport plus a small buffer, using a library like TanStack Virtual, and position them inside a tall scroll container. I would ask if the user really needs all rows on the client; often server-side pagination, filtering and sorting is better. If the data must be local, I move filtering and sorting to a Web Worker or use efficient indexes, so the main thread stays free. Rows are memoised with stable keys and callbacks. The filter input updates immediately while the list update uses useDeferredValue or startTransition. I also handle variable row heights by measuring, keep keyboard navigation and ARIA row counts correct, and profile with DevTools to confirm frames stay under 16ms.

3. What are the main security risks for a frontend application and how do you protect against them?

A strong answer covers:

  • XSS: escape output, avoid raw HTML, sanitise, use CSP
  • CSRF: SameSite cookies and CSRF tokens
  • Token storage: prefer httpOnly secure cookies over localStorage
  • Supply chain: audit and pin dependencies, lockfiles
  • Never trust the client; validate on the server
  • Clickjacking: frame-ancestors or X-Frame-Options
Show model answer

The biggest risk is XSS. Frameworks like React escape text by default, so I avoid dangerouslySetInnerHTML, and when I must render user HTML I sanitise it with DOMPurify. I add a strict Content Security Policy with nonces so injected scripts cannot run. For auth, I prefer httpOnly, Secure, SameSite cookies over localStorage, because script cannot read them if XSS happens. SameSite plus CSRF tokens on state-changing requests protects against CSRF. Supply chain attacks are real, so I keep a lockfile, review new dependencies, run audits and limit third-party scripts. I set frame-ancestors to stop clickjacking. Most importantly, the frontend is never a security boundary: every permission and input must be checked on the server, and I never ship secrets in client bundles.

4. Your company wants to split a large frontend into micro-frontends. What are the trade-offs and how would you approach it?

A strong answer covers:

  • Benefit: independent teams and deployments
  • Costs: duplicate dependencies, bigger bundles, inconsistent UX
  • Integration options: build-time packages, runtime module federation, route-level split
  • Shared design system and contracts between apps
  • Consider a well-structured monorepo first
  • Cross-cutting concerns: auth, routing, observability
Show model answer

Micro-frontends mainly solve an organisational problem: many teams that need to ship independently without waiting for one big release. The costs are real: risk of loading React twice, larger bundles, inconsistent UI, harder debugging and more infrastructure. Before agreeing, I would check if a monorepo with clear module boundaries, code owners and fast CI solves the pain. If we go ahead, I prefer splitting by route or business domain, so each app owns full pages, which can be composed by the edge or a thin shell. Runtime module federation allows finer composition but needs strict version contracts for shared libraries. All apps should use the shared design system and a common auth, routing and telemetry layer. I would migrate one area first and measure performance and team speed.

5. What are React Server Components, and how do they change the way you build a React app?

A strong answer covers:

  • Components that render only on the server and send no JS to the client
  • Can access data sources directly and be async
  • Client Components marked with "use client" for interactivity
  • Props across the boundary must be serialisable
  • Reduce bundle size; combine with Suspense streaming
  • Need care with caching and data freshness
Show model answer

React Server Components run only on the server, during the build or per request. Their code and dependencies are never sent to the browser, which shrinks the JavaScript bundle. They can be async and read from a database or internal API directly, so data fetching moves closer to the data and avoids client waterfalls. Anything interactive, using state, effects or browser APIs, must be a Client Component marked with "use client". The main rule is that props passed from a server to a client component must be serialisable, so no functions except Server Actions. In practice, I keep most of the tree as server components and push client components to the leaves. With Suspense, slow parts can stream in later. The trade-off is a new mental model, and I must think carefully about caching and when data is revalidated.

6. How would you design a web app that keeps working offline, for example for field workers with poor connectivity?

A strong answer covers:

  • Service worker caches the app shell and static assets with suitable strategies
  • Store data locally in IndexedDB and read from it first
  • Queue writes in an outbox and sync them when the connection returns
  • Plan for conflicts: versioning, last-write-wins or server-side merge rules
  • Clear UI for offline and sync status, plus safe app updates
Show model answer

I would build it as a PWA. A service worker precaches the app shell and versioned static assets, so the app opens without a network. HTML uses network-first with a cache fallback, while images can use stale-while-revalidate. Data lives in IndexedDB, through a small wrapper such as Dexie, and the UI always reads from the local store, so screens look the same online and offline. Every change is written locally and added to an outbox queue with a unique id. When the connection returns, a sync process sends the queue in order; the ids make retries idempotent. Conflicts are the hard part, so I agree rules with the backend: record versions or timestamps, last-write-wins for simple fields, and a review screen for important clashes. The UI shows clear status: offline banner, pending changes count and sync errors. I also handle storage limits, request persistent storage, and make service worker updates safe so users never lose queued work.

7. You join a team with a large JavaScript React codebase. How would you migrate it to TypeScript without stopping feature work?

A strong answer covers:

  • Incremental migration with allowJs and checkJs, converting file by file
  • Start from leaves and shared utilities, then types for API models
  • Tighten strictness gradually and track progress, for example with a count of any or ts-expect-error
  • Keep CI green: type checks in the pipeline, no big-bang rewrite branch
  • Agree on conventions, give training and make new files TypeScript by default
Show model answer

I would never do a big-bang rewrite. First I add TypeScript with allowJs so .js and .ts files live together, wire tsc --noEmit into CI, and agree on one rule: new files are written in TypeScript. Then I convert from the leaves up: shared utilities, hooks and design system components first, because many files depend on them and their types spread value quickly. Early on I define types for API models, ideally generated from an OpenAPI schema or validated with Zod, since data shapes cause many real bugs. I start with a loose config and tighten it over time, enabling strict and noImplicitAny per folder or using ts-expect-error comments as a visible to-do list, and I track the number of remaining any and JavaScript files on a dashboard. Teams convert files when they touch them for features, so migration rides along with normal work. Codemods help with mechanical changes, and short pairing sessions spread good patterns.

8. How would you build the frontend for a real-time feature like a shared task board where many users edit at the same time?

A strong answer covers:

  • Use a persistent connection such as WebSockets or Server-Sent Events, with reconnect and backoff
  • Normalised client state updated from server events, with ordering or version numbers
  • Optimistic updates with reconciliation and rollback
  • Conflict handling: server authority, operational transforms or CRDTs for rich collaborative editing
  • Performance: batch updates, avoid re-rendering the whole board, presence indicators
Show model answer

I would open a WebSocket connection, or Server-Sent Events if clients mostly listen, with automatic reconnect, exponential backoff and a resync step that fetches missed changes using the last known version. Client state is normalised by id, so an incoming event like "task moved" updates one record and only the affected column re-renders. Every change is applied optimistically with a client-generated id; the server confirms it, assigns a version and broadcasts it. If the server rejects it, I roll back and show a message. For conflicts on simple fields, the server is the authority and uses versions to detect stale writes. For rich shared text, I would use a CRDT library such as Yjs instead of inventing my own merging. To stay fast, I batch bursts of events into one render, virtualise long columns and keep drag state local. I also show presence, like avatars and who is editing a card, and test with simulated latency and dropped connections.

9. Users report that your single-page app becomes slow and crashes after being open for a few hours. How do you investigate and fix it?

A strong answer covers:

  • Reproduce with a repeatable scenario and watch memory in the Performance monitor
  • Compare heap snapshots before and after repeating an action to find growing objects
  • Look at retainers to find what keeps detached DOM nodes or large objects alive
  • Typical causes: missing effect cleanups, global listeners, timers, unbounded caches, third-party widgets
  • Fix, add guards such as cache limits, and monitor memory or long tasks in production
Show model answer

First I try to turn the report into a repeatable scenario, such as opening and closing a page twenty times, while watching the JS heap and DOM node count in the Performance monitor. If memory keeps climbing after garbage collection, it is a leak, not normal usage. I take a heap snapshot, repeat the action several times, take another, and use the comparison view to see which objects keep growing, often detached DOM elements or large arrays. The retainers panel shows the chain of references keeping them alive, which usually points to the cause: an effect without cleanup, a window or socket listener never removed, an interval still running, a module-level cache with no limit, or a chart or map widget that was never destroyed. I fix the root cause, add limits like an LRU cache size, and write a regression check if possible. In production I track long tasks and errors through RUM so we spot similar issues early.

10. How do you know your frontend is healthy in production? Describe the monitoring you would set up.

A strong answer covers:

  • Real user monitoring of Core Web Vitals (LCP, INP, CLS) by page, device and region
  • Error tracking with source maps, release tags and user context such as Sentry
  • Business and funnel metrics to catch silent failures
  • Alerts and budgets tied to releases, plus synthetic checks for key journeys
  • Respect privacy: no personal data in logs, and sampling to control cost
Show model answer

Lab tools like Lighthouse are not enough, because real users have slower devices and networks. I collect real user monitoring with the web-vitals library, sending LCP, INP and CLS with attribution data, broken down by route, device type, country and release, and I watch the 75th percentile. For errors I use a tool like Sentry with uploaded source maps, release tags and breadcrumbs, so a new error can be traced to the deploy that caused it. Unhandled promise rejections and failed API calls are tracked too. Some failures are silent, so I also watch product metrics, like checkout completion or sign-up rate, which drop when a button stops working on one browser. Synthetic tests with Playwright run key journeys every few minutes against production. Alerts fire on error spikes or vitals regressions after a release, ideally linked to quick rollback. Throughout, I scrub personal data and use sampling to keep cost and privacy under control.