PhantomCodeAIPhantomCodeAI
FeaturesMock InterviewDashboardJobsPricing
FeaturesMock InterviewDashboardJobsPricing
HomeInterview QuestionsFrontend Engineer
Updated for 2026 hiring loops

Frontend Engineer Interview Questions

Frontend interviews in 2026 look different from the algorithm-heavy SWE loops of five years ago. The shape that has stabilised across Meta, Google, Stripe, Airbnb, Shopify, and most mid-sized product companies: one JavaScript / DOM coding round (usually implement-from-scratch, not LeetCode), one framework round (build a small React component to spec — autocomplete, modal, tic-tac-toe — under realistic time pressure), one frontend system design round (design Twitter's feed, design Google Docs, design an image CDN), and one behavioural round. Some companies add a take-home or a second framework round in place of the system design. The signal interviewers are grading is not "do you know React" — most candidates do — but whether you can reason about the browser as a runtime: how the event loop sequences renders and network, how the rendering pipeline (style, layout, paint, composite) actually executes, why certain abstractions leak under load. Coding rounds in particular reward candidates who narrate trade-offs out loud (closures vs class instances, throttle vs debounce, controlled vs uncontrolled inputs) rather than candidates who just produce working code. System design is where seniority is decided — L5+ candidates are expected to volunteer the CDN strategy, the asset versioning approach, and the state-management split between server cache and client UI store without being prompted.

How frontend loops are structured

  • JavaScript fundamentals round. Implement debounce, throttle, deep clone, Promise.all, an EventEmitter, or a small Promise polyfill. Interviewers grade your understanding of closures, the event loop, and microtask vs macrotask scheduling — not whether you memorised the answer.
  • Framework round. Build a component to spec in React (occasionally Vue or vanilla) — typeahead with debounced fetch, an accessible modal, a virtualised list, tic-tac-toe with undo. The grading rubric is correctness, accessibility, keyboard handling, and clean component decomposition.
  • Frontend system design. Design the front-end architecture of a real product — Twitter feed, Google Docs, Pinterest, a video player. Expect questions about state management, data fetching, caching, real-time updates, virtualization, and asset delivery. The bar at L5+ is that you drive the structure unprompted.
  • Behavioural round. Frontend-specific stories — performance projects, cross-functional work with designers and product, debugging hard browser bugs. Interviewers are listening for whether you can collaborate with design and communicate trade-offs to non-engineers, not just whether you wrote good code.

Coding round Q&A

One JavaScript fundamentals round and one framework round is the standard pairing. Narrate trade-offs as you go — silent correct code reads as lucky in the interviewer's notes.

Q1. Implement a debounce function in JavaScript. Discuss when you'd use debounce vs throttle.

Debounce is a closure over a setTimeout handle — every call clears the previous timer and schedules a new one, so the wrapped function only fires after the caller has been quiet for the delay window. The basic shape is function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; }. Mention leading edge vs trailing edge — the default is trailing (fire after the quiet period), but a leading-edge variant fires immediately and then suppresses subsequent calls during the window, which feels snappier for button clicks. The distinction from throttle is the question interviewers actually grade: debounce waits for the caller to stop, throttle fires at a regular cadence regardless of how often you call it. Use debounce for search-as-you-type or window resize (you only care about the final value), use throttle for scroll handlers or mouse-move tracking (you want a steady sample). In React, wrap the debounced function in useCallback or useMemo, otherwise every render creates a new closure with a fresh timer and the debounce never actually debounces — this is the bug interviewers love to look for in follow-up.

Q2. Given a deeply nested object, write a function to flatten it into key-value pairs with dot notation (e.g., {a: {b: 1}} → {'a.b': 1}).

The clean answer is recursion: walk the object, and for each value either recurse with a longer prefix or assign the leaf at prefix.join('.'). The base cases are what trip people up — null is technically typeof 'object' in JavaScript, so check for it explicitly before recursing, and decide up front how you treat arrays (most interviewers want 'a.0.b' style indexed keys, not nested array literals). For very deep objects an iterative version with an explicit stack of [prefix, value] pairs avoids blowing the call stack — bring this up even if you write the recursive version first. The L5 follow-up is almost always cycle detection: pass a WeakSet down through the recursion, add every object you enter, and short-circuit (or throw, or emit a sentinel string) if you encounter one you've already visited — otherwise you infinite-loop on obj.self = obj. Time complexity is O(N) where N is the total number of leaves; space is O(D) for the recursion stack where D is max depth, plus the output object.

Q3. Explain React's reconciliation process and the role of keys. When does React decide to remount a component vs update it?

Reconciliation is React's algorithm for deciding what changed between two virtual DOM trees. The core heuristics: if a node's type changes (div to span, MyComp to OtherComp), React unmounts the old subtree and mounts the new one — state and DOM are thrown away. If the type is the same, React updates the existing instance in place and recurses into children. For lists of children, React uses the key prop to pair old and new nodes — without keys, it falls back to index-based pairing, which silently corrupts state when items reorder (the input you typed into row 3 suddenly shows up on row 2). Stable IDs from your data are the only correct keys; array index is correct only if the list is append-only and never reorders. The remount-vs-update rule is essentially: same parent slot + same type + same key = update; anything else = remount. In React 18 with concurrent rendering, this matters more because Suspense boundaries can re-attempt renders, and a changed key inside a Suspense boundary will force a fresh mount when the promise resolves — handy for resetting state on route changes, dangerous if you do it accidentally.

Q4. Implement infinite scroll with IntersectionObserver. Discuss the trade-offs vs scroll-event-based approaches.

Render the list, then render a sentinel div at the bottom and observe it with an IntersectionObserver — when intersection ratio crosses your threshold (usually 0 with a small positive rootMargin like '200px' so you start fetching before the user actually hits the bottom), call your loadMore. The observer fires off the main thread via the browser's compositor, so you avoid the layout thrashing that scroll listeners cause when handlers run on every scroll tick. Scroll listeners require you to debounce or rAF-wrap yourself, query getBoundingClientRect (which forces synchronous layout), and they fire even when nothing relevant changed — IntersectionObserver coalesces all of that natively. The L5 follow-up is virtualization: at a few hundred items, plain infinite scroll is fine; past a few thousand the DOM itself becomes the bottleneck (memory, paint, scrollbar accuracy), so reach for react-window or react-virtuoso, which render only the visible window plus a small buffer and recycle nodes as you scroll. Pair that with cursor-based pagination on the server (a stable opaque cursor like 'after:user_42:created_at:...') rather than offset-based — offsets re-skew when new items are inserted at the head of the list and produce duplicate or missing rows.

Frontend system design Q&A

L5+ candidates own one frontend system design round; staff candidates may get two. Start with the user-visible product surface and the data flow, then go to caching, real-time, and rendering strategy. You drive — the interviewer will not feed you the structure.

Q1. Design Google Docs in the browser — multi-user collaborative editing with conflict resolution.

Start by separating the document model from the sync model. The document is a tree of rich-text nodes (paragraphs, runs with style attributes, embeds) — modeling it as a flat sequence of operations is tempting but breaks down for nested structure like tables. The sync layer is where Operational Transform (OT) vs CRDT lives: OT was the original Google Docs design — every edit is transformed against concurrent edits before being applied, which requires a central server to linearise operations but keeps the data model simple. CRDTs (Yjs, Automerge) are the modern default for new builds — every operation carries enough metadata (site ID, logical clock) to merge in any order with the same result, which means you can sync peer-to-peer or survive partition without a central coordinator. Cursors and selections sync as ephemeral 'awareness' state (separate channel, not part of the persistent doc) so they don't bloat history. Persistence is two-layer: IndexedDB on the client for offline edits and instant reopen, plus a server-side log that the client replays diffs against on reconnect. The trade-offs to name: OT is simpler to reason about but couples you to a central server and has notoriously gnarly transformation functions for rich text; CRDTs decentralize cleanly but the document state grows with every edit (tombstones for deleted characters never go away without GC) and eventual-consistency semantics make undo/redo and intent-preservation harder. Pick CRDT for new builds, defend with Yjs' production track record in Figma, Linear, and modern Google Workspace work.

Q2. Design the front-end architecture for Twitter's home feed — what state lives where, how does new-tweet polling work, how do you handle 10k tweets in DOM without jank?

Three state buckets: server state (the timeline itself, user profiles, tweet contents) lives in React Query or SWR with infinite-query support; cross-component UI state (current tab, compose modal open, dark mode) goes in a lightweight store like Zustand or Redux Toolkit; per-component local state (input draft text, hover state) stays in useState. The 10k-tweets-without-jank answer is window-based virtualization — react-virtuoso is the right pick here because it handles variable-height items, which tweets always are (some have media, some have quote-tweets, some are plain text). Only the visible window plus a 5-10 item buffer is in the DOM; everything else is unmounted. New-tweet polling is the interesting part: a WebSocket or Server-Sent Events stream pushes new tweet IDs as they arrive, but you do not auto-prepend — instead, accumulate them and show a 'N new tweets' badge at the top, which the user clicks to scroll-to-top and merge. This avoids the cursor jumping while the user is reading. Optimistic updates for likes and replies — increment the count immediately in the local cache, reconcile when the server confirms, roll back with a toast if the request fails. Image lazy-loading via native loading='lazy' plus aspect-ratio CSS to reserve layout space (otherwise the page jumps as images load and CLS tanks). Mention the cache-invalidation question: when the user posts a new tweet, do you refetch the timeline or surgically prepend? Surgically prepend, and only invalidate on a hard reload.

Q3. Design a CDN-backed asset delivery system for a global SPA — JS bundles, CSS, images. How do you handle versioning, cache invalidation, and split bundles for 50+ teams contributing?

Content-hash filenames are the foundation: app.[contenthash].js, vendor.[contenthash].js, route-home.[contenthash].css. The hash changes only when content changes, so you can set Cache-Control: public, max-age=31536000, immutable on every asset — the browser never revalidates. The HTML itself is the cache key: serve index.html with short or no caching, and it references the current hashed asset URLs. This gives you atomic rollouts (deploy = upload assets + swap HTML pointers) and trivial rollback (revert the HTML). For 50+ teams in one monorepo, module federation (Webpack 5 / Rspack / Vite plugin) is the answer — each team builds and deploys its own remote bundle independently, the shell app loads them at runtime, and you avoid the 'one team's broken build blocks everyone' problem of a single monolithic bundle. Module federation comes with its own pain (shared dependency version coordination, runtime errors when contracts drift), so for smaller orgs static linking with route-based code splitting (dynamic import() per route) is usually enough. Component-level splitting is overkill except for genuinely heavy widgets (rich-text editor, video player). Rollout strategy: canary by region (5% of users in one PoP first, watch error rate and Core Web Vitals), then expand; feature flags to dark-launch new bundles behind a query param; multi-CDN with origin failover for the inevitable day Fastly or Cloudflare has an outage. Images go through a transformation pipeline (Cloudinary, imgix, or self-hosted with Sharp) that serves the right format (AVIF, WebP, JPEG fallback) and size per device, with srcset and sizes attributes on every img tag.

Behavioural Q&A

One round, sometimes folded into the framework or system design slot. Interviewers want specific stories about performance, cross-functional collaboration, and debugging — not values-page recitations.

Q1. Tell me about a frontend project where performance was the central problem.

Strong answers open with the specific metric you targeted — Time to Interactive, Largest Contentful Paint, Interaction to Next Paint, or sustained frame budget during a heavy interaction — not the vague 'we made it faster'. Then the investigation: Chrome DevTools Performance tab for flame charts, Lighthouse for synthetic budgets, real-user monitoring (Sentry, Datadog RUM, or in-house) for the actual distribution your users see, because synthetic and real numbers often disagree by 2-3x. Name the trade-offs you actually made — usually some combination of bundle size vs feature richness, server-side rendering cost vs client-side hydration jank, image quality vs payload, or third-party scripts vs analytics coverage. The measurable outcome matters: 'we cut LCP from 4.2s to 1.8s at the p75' lands far better than 'it felt faster'. If the project was multi-team, mention the team coordination explicitly — performance wins almost always come from UX changes (removing a hero video, lazy-loading below-the-fold) plus code changes (route splitting, deferring third-party JS) plus infra changes (CDN config, image pipeline), and getting all three teams aligned is the actual work. Interviewers grading for senior signal want to hear you led that coordination, not just shipped your slice.

Q2. Tell me about a time you disagreed with a designer about a UX decision.

The frontend-engineer version of this question is almost always about feasibility, performance, or accessibility — the designer wants a 60fps parallax hero, the design relies on a font that adds 200KB to the critical path, or the picked color contrast fails WCAG AA. The shape of a strong answer: you brought concrete evidence rather than asserting from authority — a quick prototype showing the perf cost on a mid-tier Android, a Lighthouse run, a screenshot of the contrast-checker failing. You did not kill the design; you found a third path that preserved the designer's intent within the constraint — system fonts that visually approximate the brand font, CSS-only animation that hits the same beat, an alternate palette that passes contrast and still feels on-brand. You maintained the working relationship — this is where most candidates faceplant by making the story sound like engineering 'won'. Better framing: 'we ended up shipping a version we both felt good about, and the designer now pings me earlier in the process when they're considering a heavy effect'. That sounds like collaboration; 'I convinced them I was right' sounds like a Googleyness anti-pattern even at non-Google companies.

Q3. Describe a frontend bug that was hard to track down.

The strongest answers signal pattern recognition by naming the bug category, not just the symptom. 'A race condition in async state where the response from request N+1 arrived before request N and overwrote it' is L5 territory; 'a button was broken' is not. Walk through systematic debugging — binary search (comment out half the recent changes, narrow down), git bisect when the bug is older and the diff is huge, isolating in a reduced test case, network throttling and CPU throttling in DevTools to reproduce reliably. Name the tools: React DevTools' Profiler for unnecessary re-renders, why-did-you-render to catch reference-equality bugs, the Network panel waterfall for ordering issues, the Memory panel for leaks. The meta-lesson at the end is what separates senior candidates: 'this was a stale closure bug — the useEffect captured the first render's state and never saw the updates — and after that I started reaching for the useRef pattern or the functional setState form whenever an effect needs to read latest values'. Naming the category and the fix-pattern shows you generalized, which is exactly the signal L5+ interviews are grading.

During the actual loop

Reading sample answers is preparation. The loop itself is performance under pressure — and the framework round in particular goes sideways fast when you blank on a hook signature or a CSS specificity rule. Use PhantomCodeAI as your live coding copilot during real frontend loops, or run a full dress rehearsal with our mock coding interview so the patterns are muscle memory before the real one. The transcript afterward shows exactly what the interviewer will be writing down — every pause, every trade-off you named, every spec you forgot to clarify.