Mid Frontend Engineer Interview Questions
What a Mid Frontend Engineer interview focuses on, the questions you'll face, and how to practice them with instant AI feedback.
What's expected at the Mid level
Expect framework depth, state management, performance basics and independent feature delivery.
Sample Frontend Engineer interview questions
- TechnicalExplain the browser event loop and how microtasks differ from macrotasks.What a strong answer covers
- Call stack, Web APIs, task queue, microtask queue
- Event loop phases: execute script, process microtasks, process macrotasks
- Microtasks: promise callbacks, MutationObserver; macrotasks: setTimeout, DOM events
- Microtasks run after each macrotask, before next macrotask
- Prior interview follow-up on starvation
View a sample answer
The browser event loop coordinates asynchronous execution using a call stack, Web APIs, and two task queues: macrotask and microtask. Macrotasks include setTimeout, setInterval, and I/O events. Microtasks include Promise.then/catch/finally, and MutationObserver. The event loop first executes the current task (macrotask) from the macrotask queue. After that, it processes the entire microtask queue before moving to the next macrotask. If a microtask queues another microtask, the microtask queue is processed until empty, which can starve macrotasks. This is important for UI rendering because rendering happens between macrotasks, not microtasks. A common pitfall is assuming that setTimeout(callback, 0) runs before microtask callbacks; microtasks always run first.
- TechnicalWhat triggers a re-render in React, and how do you prevent unnecessary ones?What a strong answer covers
- State/props change, parent re-render, context change
- Default: re-render entire subtree; comparison via React.memo, useMemo, useCallback
- ShouldComponentUpdate for classes; PureComponent
- Avoid inline objects/functions in JSX; keys for lists
- Profiler and devtools to identify unnecessary re-renders
View a sample answer
React re-renders a component when its state or props change, when its parent re-renders (unless memoized), or when a context value it consumes changes. React first re-renders the component, then reconciles the virtual DOM with the previous one, applying minimal DOM updates. Unnecessary re-renders can be prevented by using React.memo for functional components (shallow props comparison) or PureComponent for classes. For expensive computations, use useMemo to memoize values and useCallback to memoize callbacks. Avoid creating new objects or functions in the render body, as they break shallow comparison. For lists, provide stable keys. React DevTools' Profiler helps identify re-render causes. A common mistake is overusing memoization without profiling, which adds overhead.
- TechnicalDescribe how the CSS cascade resolves conflicting rules.What a strong answer covers
- Origin, specificity, importance, source order
- Stylesheet origins: user-agent, user, author, transition, animation, !important
- Specificity: inline > IDs > classes/attributes/pseudo-classes > elements/pseudo-elements
- Cascade layers, @layer, and precedence within layers
- Inheritance and initial/unset/revert/inherit keywords
View a sample answer
The CSS cascade determines which property value applies when multiple declarations target the same element. The process runs in order: first, collect all declarations from all origins (user-agent, user, author, animation, transition). Important declarations (!important) reverse origin priority: user-agent < user < author < animations < transitions. Then, within the same origin, sort by specificity: inline styles (style attribute) have highest; then IDs; then classes, attributes, pseudo-classes; then elements, pseudo-elements. If specificity ties, the declaration that appears later in the source order wins. Cascade layers (@layer) allow authors to group styles and explicitly set layer precedence. The cascade does not consider inheritance, but if no declaration wins, the property may inherit or take its initial value. A common misunderstanding is thinking that specificity alone decides; origin and !important take precedence first.
- CodingImplement a debounce function and explain when you would use it.What a strong answer covers
- Closure, timer reference, cancellation
- Trailing vs leading invocation
- Use cases: search input, resize handler, scroll listener
- Immediate vs delayed execution variants
- Time complexity: O(1) per call; memory: stores single timer
View a sample answer
A debounce function ensures that a callback is only executed after a specified delay since the last invocation. It uses a closure to keep a timer reference. Each call clears the previous timer and sets a new one. Optionally, you can implement a leading edge version that fires immediately then ignores subsequent calls until the delay elapses. Common use cases are search-as-you-type inputs (to reduce API calls), window resize handlers, and button clicks to prevent double submission. The time complexity of each debounced call is O(1) as it just resets a timer. The space complexity is O(1) for storing the timer ID. A common pitfall is forgetting to cancel the debounced function on component unmount to avoid memory leaks or state updates on unmounted components. The following code implements a standard trailing debounce with a cancel method.
Reference solutionjavascript function debounce(func, delay) { let timerId = null; function debounced(...args) { clearTimeout(timerId); timerId = setTimeout(() => { func.apply(this, args); timerId = null; }, delay); } debounced.cancel = function() { clearTimeout(timerId); timerId = null; }; return debounced; } // Usage example: // const handleSearch = debounce((query) => fetchResults(query), 300); // input.addEventListener('input', (e) => handleSearch(e.target.value)); - CodingBuild an accessible autocomplete component with keyboard support.What a strong answer covers
- ARIA roles: combobox, listbox, option
- Keyboard navigation: Enter, Escape, Arrow keys, Tab
- Focus management: activeIndex, aria-activedescendant
- Screen reader announcements via live regions
- Debounced input, filtering, and highlighting
View a sample answer
An accessible autocomplete requires proper ARIA roles and keyboard support. The input has role='combobox', aria-expanded, aria-controls, and aria-activedescendant pointing to the currently focused option. The options list has role='listbox', each option role='option' with aria-selected. Keyboard handling: Down/Up arrows navigate options, changing aria-activedescendant; Enter selects the highlighted option; Escape closes the list; Tab selects and moves to next field. Focus must remain on the input; do not move focus to the list. A live region (aria-live='polite') announces the number of results or selection. The component should debounce the input and filter the options. The following code provides a functional example with React hooks.
Reference solutiontypescript import React, { useState, useRef, useEffect, useCallback } from 'react'; import { debounce } from './debounce'; // assume debounce from previous answer interface AutocompleteProps { options: string[]; } const Autocomplete: React.FC<AutocompleteProps> = ({ options }) => { const [inputValue, setInputValue] = useState(''); const [filteredOptions, setFilteredOptions] = useState<string[]>([]); const [isOpen, setIsOpen] = useState(false); const [activeIndex, setActiveIndex] = useState(-1); const inputRef = useRef<HTMLInputElement>(null); const listRef = useRef<HTMLUListElement>(null); const filterOptions = useCallback( debounce((query: string) => { const filtered = options.filter(opt => opt.toLowerCase().includes(query.toLowerCase()) ); setFilteredOptions(filtered); setIsOpen(filtered.length > 0 && query.length > 0); setActiveIndex(-1); }, 300), [options] ); useEffect(() => { filterOptions(inputValue); }, [inputValue, filterOptions]); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setInputValue(e.target.value); }; const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); if (!isOpen) { setIsOpen(true); setFilteredOptions(options.filter(opt => opt.toLowerCase().includes(inputValue.toLowerCase()) )); } setActiveIndex(prev => prev < filteredOptions.length - 1 ? prev + 1 : 0 ); break; case 'ArrowUp': e.preventDefault(); setActiveIndex(prev => prev > 0 ? prev - 1 : filteredOptions.length - 1 ); break; case 'Enter': e.preventDefault(); if (activeIndex >= 0) { setInputValue(filteredOptions[activeIndex]); setIsOpen(false); } break; case 'Escape': setIsOpen(false); break; case 'Tab': if (activeIndex >= 0) { setInputValue(filteredOptions[activeIndex]); } setIsOpen(false); break; } }; return ( <div role="combobox" aria-expanded={isOpen} aria-haspopup="listbox"> <input ref={inputRef} type="text" value={inputValue} onChange={handleChange} onKeyDown={handleKeyDown} aria-autocomplete="list" aria-controls="autocomplete-list" aria-activedescendant={activeIndex >= 0 ? `option-${activeIndex}` : undefined} aria-label="Search" /> {isOpen && ( <ul ref={listRef} id="autocomplete-list" role="listbox"> {filteredOptions.map((opt, idx) => ( <li key={opt} id={`option-${idx}`} role="option" aria-selected={idx === activeIndex} style={{ backgroundColor: idx === activeIndex ? '#eee' : 'transparent', }} onClick={() => { setInputValue(opt); setIsOpen(false); }} > {opt} </li> ))} </ul> )} <div aria-live="polite" aria-atomic="true"> {isOpen ? `${filteredOptions.length} results available.` : ''} </div> </div> ); }; export default Autocomplete; - System DesignDesign the frontend architecture for a real-time collaborative document editor.What a strong answer covers
- Real-time sync via WebSockets or OT/CRDT
- State tree for document model (e.g., quill-delta, Slate)
- Component tree for header, toolbar, editor, collaboration widgets
- Awareness vs persistence layer separation
- Scalability: cursors, undo/redo, offline support
View a sample answer
The architecture for a collaborative editor must handle real-time synchronization. I'd separate the frontend into layers: a document model (e.g., using CRDT-based library like Yjs or OT library), a rendering layer (e.g., using Slate or Prosemirror), and collaboration UI (cursors, selection, presence). The document model holds the current state and applies operations from local and remote users. On changes, we send operations over a WebSocket to a server that broadcasts to other clients. The state is the single source of truth. The React component tree would include a Header (document name, share button), Toolbar (formatting options), Editor (linked to document model), and CollaborationWidgets (avatar list, remote cursors). For scalability, we decouple the editor from the sync layer: the editor dispatches operations to the model, which emits changes to the view. Undo/redo is handled via operation stacking. Offline support requires local persistence (IndexedDB) and queueing operations. A common pitfall is mixing UI state (like toolbar toggle) with document state, which should be distinct. Use useReducer or external state for the document model to avoid re-render storms.
- BehavioralTell me about a time you improved the performance of a slow page.What a strong answer covers
- Measured performance with Lighthouse/Chrome DevTools
- Identified bottleneck: expensive re-renders, large images, render-blocking resources
- Applied lazy loading (IntersectionObserver), code splitting (React.lazy)
- Used React.memo and virtualization (react-window) for long lists
- Resulted in 50% reduction in load time and 30% improvement in interactivity
View a sample answer
At my previous job, our product's dashboard was slow to load and janky when scrolling through a large table of data. I used Chrome DevTools Performance tab and Lighthouse to measure. The main bottlenecks were: 1) Load time: large bundle (2MB) and uncompressed images. 2) Rendering: the table re-rendered entirely on every data change. I implemented code splitting with React.lazy for route-level chunks, cutting the initial bundle by 60%. I compressed images via WebP and used lazy loading with IntersectionObserver for the image-heavy hero section. For the table, I switched to a virtualized list using react-window, rendering only visible rows, which reduced DOM nodes from 5000 to ~30. I also memoized row components with React.memo. These changes reduced initial load time from 4.5s to 2s and increased First Input Delay from 300ms to 90ms. A key lesson was to always measure before optimizing, and focus on user-perceived metrics.
- BehavioralHow do you handle disagreements with designers over UX trade-offs?What a strong answer covers
- Understand the design rationale and user research
- Provide data-driven arguments (A/B tests, analytics, accessibility)
- Propose compromise: maintain design vision but add escape hatches
- Document trade-offs for future reference
- Use prototyping to test both approaches
View a sample answer
When disagreeing with a designer, I first seek to understand their rationale: ask about user research, goals, and constraints. For example, a designer wanted a custom dropdown that looked modern but had poor keyboard navigation. I gathered data: the component's accessibility score dropped significantly, and our analytics showed 15% of users relied on keyboard navigation. I presented these findings and suggested a compromise: keep the visual design but add proper ARIA roles and keyboard handling by enhancing the existing library. We prototype both approaches—pure custom and enhanced version—and test with a few users. The enhanced version satisfied both aesthetics and accessibility. I also documented the decision and trade-offs. The key is to focus on the user's experience, not personal preference, and to be willing to iterate. A common pitfall is escalating without data; always bring concrete evidence.
What interviewers assess
JavaScript fundamentals
Closures, the event loop, promises/async, prototypes and `this` binding.
Framework depth
React (or Vue/Angular) rendering, reconciliation, hooks and state management.
CSS & layout
Flexbox/grid, the cascade, stacking contexts and responsive design.
Performance
Critical rendering path, bundle size, lazy loading and Core Web Vitals.
Accessibility
Semantic HTML, ARIA, keyboard navigation and screen-reader support.
How to prepare
- Talk through the rendering path out loud — interviewers score your reasoning, not just the answer.
- Always mention accessibility and performance, even when not explicitly asked.
- Practice building one component end to end without an IDE's autocomplete.
Frequently asked questions
What coding questions are common in frontend interviews?
Expect DOM manipulation, utility functions like debounce/throttle, and building a small interactive component such as an autocomplete or modal.
Do frontend interviews include system design?
At mid and senior levels, yes — usually frontend-focused design like a feed, a design system, or a collaborative editor rather than backend infrastructure.
How should I prepare for a frontend interview fast?
Drill JavaScript fundamentals, build a few components from scratch, and run timed mock interviews so you can explain your reasoning under pressure.
Practice Frontend Engineer questions with instant AI feedback
Offersly runs a mock interview tailored to your resume and target role, then scores every answer on relevance, depth, clarity and correctness.