Skip to content

Medium Frontend Development Interview Questions

95 medium questions with answers. Applied questions that check whether you can use the concepts on real work.

Topics in the medium set

TypeScript: 14 Browser APIs: 12 CSS Architecture and Accessibility: 12 Frontend Testing: 12 Frontend Architecture: 12 Modern JavaScript: 11 Web Performance: 11 Build Tools and Tooling: 11
Solved — mediumStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
3 questions free Unlock the rest in seconds
01

Compare Promise.all, Promise.allSettled, Promise.race and Promise.any.

Save to ▾
Show answer
Promise.all resolves with all results when every promise succeeds, but rejects immediately if any fails. Promise.allSettled always waits for all and returns each outcome with status fulfilled or rejected, useful for dashboards loading independent widgets. Promise.race settles with the first promise to settle, often used for timeouts. Promise.any resolves with the first success and rejects only if all fail, useful for trying multiple mirrors.
02

Why does await inside forEach not wait for each item?

Save to ▾
Show answer
forEach ignores the promise returned by an async callback, so all callbacks start almost at once and forEach finishes before any await completes. Code after it runs too early. For sequential processing, use a for...of loop with await inside. For parallel processing with a final result, use await Promise.all(items.map(async item => ...)). Choosing between them depends on whether order and rate limits matter.
03

What are tagged template literals?

Save to ▾
Show answer
A tagged template is a function placed before a template literal, like html`<p>${name}</p>`. The function receives the static string parts as an array and the interpolated values separately, so it can escape, transform or validate values before joining them. Libraries use this pattern for safe HTML templating, CSS-in-JS and GraphQL queries. Normal template literals simply interpolate values and support multi-line strings.
04

How do private class fields and getters work in modern JavaScript?

Save to ▾
Show answer
Fields prefixed with #, such as #balance, are truly private and cannot be accessed outside the class body, unlike the old underscore convention. Getters and setters, written get total() and set total(v), let you expose computed or validated properties while keeping internal state hidden. Static fields and static blocks are also supported. These features make classes safer for building reusable UI components and service objects.
05

Implement a throttle function.

Save to ▾
Show answer
Throttle allows a function to run at most once in a given interval: function throttle(fn, wait) { let last = 0; return function (...args) { const now = Date.now(); if (now - last >= wait) { last = now; fn.apply(this, args); } }; } window.addEventListener('scroll', throttle(updateProgressBar, 100)); Unlike debounce, which waits for calls to stop, throttle fires regularly during continuous events such as scrolling or dragging.
06

Write a fetch wrapper that retries with exponential backoff.

Save to ▾
Show answer
Retry on network errors and 5xx responses, doubling the delay each time: async function fetchWithRetry(url, options = {}, retries = 3, delay = 500) { try { const res = await fetch(url, options); if (res.status >= 500 && retries > 0) throw new Error('Server ' + res.status); return res; } catch (err) { if (retries === 0) throw err; await new Promise(r => setTimeout(r, delay)); return fetchWithRetry(url, options, retries - 1, delay * 2); } } Do not retry 4xx errors, and only retry idempotent requests.
07

Cancel an older search request when the user types again using AbortController.

Save to ▾
Show answer
Keep a controller per request and abort the previous one: let controller; async function search(term) { controller?.abort(); controller = new AbortController(); try { const res = await fetch('/api/search?q=' + encodeURIComponent(term), { signal: controller.signal }); renderResults(await res.json()); } catch (err) { if (err.name !== 'AbortError') showError(err); } } This prevents slow old responses from overwriting newer results.
08

Search results sometimes show data for an older query. What is happening?

Save to ▾
Show answer
This is a race condition: requests for 'ja' and 'java' are in flight together, and if the older one responds last, it overwrites the newer results. I would cancel the previous request with AbortController, or tag each request with an id and ignore responses that are not the latest. Debouncing the input reduces the number of requests, and caching repeated queries makes responses faster and more consistent.
09

Users in India see order dates shifted by a day. What is the likely bug?

Save to ▾
Show answer
The server probably sends a date like 2026-11-12 without time zone, and new Date('2026-11-12') treats date-only strings as UTC midnight, while other formats are parsed as local time, causing shifts around midnight. I would send full ISO timestamps with offsets, store times in UTC, and format for display with Intl.DateTimeFormat('en-IN', { timeZone: 'Asia/Kolkata' }). The Temporal API, now arriving in browsers, makes this safer.
10

Errors from failed promises are silently disappearing in production. How do you catch them?

Save to ▾
Show answer
Promises without a catch produce unhandled rejections that do not stop the app but hide failures. I would add a global window.addEventListener('unhandledrejection') handler and window.onerror that report errors to a monitoring tool like Sentry with source maps. In code, every async call should have try and catch or a .catch, and ESLint rules such as no-floating-promises in TypeScript projects catch missing handlers.
11

How would you explain a difficult JavaScript concept to a junior teammate?

Save to ▾
Show answer
Situation: a junior developer kept writing bugs with async code. Task: I wanted them to understand promises rather than copy fixes. Action: I drew the event loop on a whiteboard, wrote three small console examples for them to predict, and then reviewed one of their real pull requests together. Result: within two weeks, their async bugs stopped and they started explaining the concept to the next new joiner.
12

Explain any, unknown and never.

Save to ▾
Show answer
any turns off type checking completely, so errors slip through silently. unknown also accepts any value, but you must narrow it with typeof, instanceof or a type guard before using it, which makes it the safe choice for API responses and catch errors. never represents values that never occur, such as a function that always throws, or the leftover case in an exhaustive switch over a union.
13

How does type narrowing work in TypeScript?

Save to ▾
Show answer
Narrowing is how TypeScript refines a broad type to a specific one inside a code branch. It understands typeof checks, instanceof, the in operator, equality checks, truthiness and discriminant properties. For example, inside if (typeof value === 'string') you can call value.toUpperCase(). Custom type guard functions returning value is User extend this to your own checks, and assertion functions narrow after they run.
14

What are generics and why are they useful?

Save to ▾
Show answer
Generics let you write reusable code that keeps type information, using type parameters like <T>. For example, function first<T>(items: T[]): T | undefined returns a number for a number array and a User for a User array without losing types. Constraints such as <T extends { id: string }> restrict what T can be. Generics power typed API clients, form libraries and reusable UI components.
15

Should you use enums or string literal unions?

Save to ▾
Show answer
TypeScript enums generate runtime JavaScript objects, and numeric enums accept any number, which weakens safety. String literal unions like type Status = 'draft' | 'published' have zero runtime cost, work naturally with JSON and give the same autocomplete. When you need a runtime list of values, an as const object plus a derived type works well. Many teams avoid enums, especially since type-stripping tools do not support them.
16

Which tsconfig options are important for a strict codebase?

Save to ▾
Show answer
strict: true enables a family of checks, including strictNullChecks and noImplicitAny, and should be on for new projects. noUncheckedIndexedAccess adds undefined to array and record lookups, catching many runtime errors. exactOptionalPropertyTypes, noImplicitOverride and noFallthroughCasesInSwitch add more safety. For bundler-based apps, moduleResolution: bundler, isolatedModules and verbatimModuleSyntax keep TypeScript aligned with how tools like Vite compile code.
17

What are declaration files (.d.ts) and when do you write them?

Save to ▾
Show answer
Declaration files describe the types of JavaScript code without containing implementation. Libraries ship them so TypeScript users get type checking, and many come from DefinitelyTyped as @types packages. You write your own when using an untyped library, declaring global variables injected by scripts, or typing non-code imports like declare module '*.svg'. Generating them with declaration: true is standard when publishing a TypeScript library.
18

Write a type-safe getProperty function using generics and keyof.

Save to ▾
Show answer
Constrain the key to the object's own keys: function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const course = { title: 'Frontend', weeks: 14 }; const weeks = getProperty(course, 'weeks'); // number // getProperty(course, 'price'); // compile error T[K] is an indexed access type, so the return type matches the chosen property. Typos in key names are caught at compile time.
19

Model API request state with a discriminated union.

Save to ▾
Show answer
A shared status field lets TypeScript narrow each case: type ApiState<T> = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; message: string }; function render(state: ApiState<string[]>) { switch (state.status) { case 'success': return state.data.join(', '); case 'error': return 'Error: ' + state.message; default: return 'Loading...'; } } Impossible states, like having both data and an error, cannot be represented.
20

Create a typed fetch helper with generics.

Save to ▾
Show answer
Let callers specify the expected response type: async function getJson<T>(url: string): Promise<T> { const res = await fetch(url); if (!res.ok) throw new Error(`Request failed: ${res.status}`); return res.json() as Promise<T>; } interface Course { id: number; title: string; } const courses = await getJson<Course[]>('/api/courses'); courses[0].title.toUpperCase(); The cast trusts the server, so for untrusted data validate with a schema library like Zod before returning.
21

Write a custom type guard to check if a value is a User.

Save to ▾
Show answer
A function returning value is User narrows the type: interface User { id: number; email: string; } function isUser(value: unknown): value is User { return typeof value === 'object' && value !== null && 'id' in value && typeof (value as User).id === 'number' && 'email' in value && typeof (value as User).email === 'string'; } const data: unknown = JSON.parse(raw); if (isUser(data)) console.log(data.email); Inside the if block, data is treated as User.
22

Create a type where all fields are optional except id.

Save to ▾
Show answer
Combine Pick and Partial with an intersection: interface Product { id: string; name: string; price: number; stock: number; } type ProductUpdate = Pick<Product, 'id'> & Partial<Omit<Product, 'id'>>; const update: ProductUpdate = { id: 'p1', price: 799 }; // const bad: ProductUpdate = { price: 799 }; // error: id missing This is a common pattern for PATCH request payloads, ensuring the identifier is always present.
23

A third-party library you need has no TypeScript types. What do you do?

Save to ▾
Show answer
First I check if types exist on DefinitelyTyped by installing @types/library-name. If not, I write a minimal declaration file, for example declare module 'old-chart-lib' with only the functions I use, in a types folder included by tsconfig. This is better than a blanket any import. If the library is important to the community, contributing those types upstream helps everyone.
24

Your team uses 'as any' everywhere to silence errors. How do you improve this?

Save to ▾
Show answer
I would first understand why: often the real types are unclear or the API response is untyped. I would add shared API types, introduce unknown with type guards for external data, and enable the ESLint rule no-explicit-any as a warning. Then I would fix the most risky areas first and block new any in CI. Showing a bug that types would have caught helps get team buy-in.
25

Tell me about a time you convinced your team to adopt a new tool like TypeScript.

Save to ▾
Show answer
Situation: our JavaScript app kept breaking after API changes. Task: I believed TypeScript would help but the team worried about slowing down. Action: I converted one small module as a pilot, showed three bugs it caught, and proposed gradual adoption with no deadline pressure. Result: the team agreed, we migrated over two quarters, and production errors related to wrong data shapes dropped significantly.
26

What is the difference between a Web Worker and a Service Worker?

Save to ▾
Show answer
A Web Worker runs JavaScript on a background thread for a single page, used for heavy computations like parsing files or image processing without freezing the UI. A Service Worker sits between the page and the network for an entire origin, intercepting requests to enable offline support, caching and push notifications. It has a lifecycle of install, activate and fetch, and it works only over HTTPS.
27

Compare IntersectionObserver, ResizeObserver and MutationObserver.

Save to ▾
Show answer
IntersectionObserver reports when an element enters or leaves the viewport or a container, used for lazy loading, infinite scroll and analytics impressions. ResizeObserver reports changes to an element's size, useful for charts that must redraw when their container changes. MutationObserver reports DOM changes like added nodes or changed attributes. All three are asynchronous and far more efficient than polling or scroll and resize listeners.
28

When should you use IndexedDB instead of localStorage?

Save to ▾
Show answer
localStorage is synchronous, stores only strings and is limited to about 5 MB, so large reads block the main thread. IndexedDB is asynchronous, stores structured data including blobs, supports indexes and transactions, and can hold hundreds of megabytes depending on disk space. Use it for offline data, drafts, cached API results or files in PWAs. Wrapper libraries like idb make its event-based API easier with promises.
29

How does the History API enable client-side routing?

Save to ▾
Show answer
history.pushState(state, '', '/courses/react') changes the URL and adds a history entry without reloading the page, and replaceState updates the current entry. When users press back or forward, the popstate event fires and the app renders the matching view. The server must return the app's HTML for all routes, otherwise refreshing a deep link gives a 404. Routers in SPA frameworks are built on this API.
30

What is the same-origin policy and how does postMessage work around it?

Save to ▾
Show answer
An origin is the combination of scheme, host and port. The same-origin policy stops scripts from reading data or DOM of another origin, such as an embedded iframe from a payment provider. window.postMessage lets two windows or iframes exchange messages safely across origins. The receiver must check event.origin against an allow-list, and the sender should specify the exact target origin instead of '*'.
31

How can you communicate between multiple tabs of the same site?

Save to ▾
Show answer
BroadcastChannel lets same-origin tabs, windows and workers publish and subscribe to messages on a named channel, which suits syncing logout, theme or cart updates. The storage event fires in other tabs when localStorage changes, which is a simple fallback. SharedWorker can hold shared state for all tabs. Each option works only within the same origin, so cross-domain sync needs a server or postMessage.
32

Move a heavy calculation into a Web Worker.

Save to ▾
Show answer
The worker runs on its own thread and talks through messages: // worker.js self.onmessage = (e) => { const total = e.data.reduce((sum, row) => sum + row.amount, 0); self.postMessage(total); }; // main.js const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }); worker.postMessage(bigArrayOfRows); worker.onmessage = (e) => showTotal(e.data); The UI stays responsive. Data is copied between threads, so for huge binary data use transferable objects like ArrayBuffer.
33

Detect offline status and retry queued requests when the connection returns.

Save to ▾
Show answer
Listen for online and offline events and keep a small queue: const queue = []; async function send(req) { if (!navigator.onLine) { queue.push(req); showBanner('You are offline'); return; } await fetch(req.url, req.options); } window.addEventListener('online', async () => { hideBanner(); while (queue.length) await send(queue.shift()); }); window.addEventListener('offline', () => showBanner('You are offline')); navigator.onLine can report true on captive networks, so treat failed fetches as offline too. Persist the queue in IndexedDB for reliability.
34

Create a simple custom element with Shadow DOM.

Save to ▾
Show answer
Extend HTMLElement and register a tag name containing a hyphen: class RatingStars extends HTMLElement { connectedCallback() { const value = Number(this.getAttribute('value') || 0); const root = this.attachShadow({ mode: 'open' }); root.innerHTML = `<style>span { color: var(--star-color, orange); }</style> <span role='img' aria-label='${value} out of 5'>${'*'.repeat(value)}</span>`; } } customElements.define('rating-stars', RatingStars); // usage: <rating-stars value='4'></rating-stars> The custom property lets the page theme it despite Shadow DOM isolation.
35

When a user logs out in one tab, other open tabs stay logged in. How do you fix it?

Save to ▾
Show answer
I would broadcast a logout message using BroadcastChannel, or the storage event as a fallback, so every tab clears its in-memory state and redirects to the login page. The server must also invalidate the session or refresh token, so stale tabs fail their next API call with a 401, which the app handles by logging out. Both client sync and server invalidation are needed.
36

In a single-page app, the back button takes users out of the site instead of to the previous filter. Why?

Save to ▾
Show answer
The app changes the view without adding history entries, so the browser has nothing to go back to within the site. Each meaningful navigation, like changing a category or opening a product, should call pushState or the router's navigate function, while minor changes such as typing in search can use replaceState. The app must also listen to popstate and restore the view and scroll position from the URL.
37

How do you handle unclear requirements from a product manager?

Save to ▾
Show answer
Situation: a product manager asked for 'offline support' in our web app without details. Task: I needed clarity before estimating. Action: I listed specific questions, which screens, read-only or editing, how long offline, and shared a quick prototype showing two options with effort estimates. Result: we agreed on read-only offline access for the order history, which shipped in one sprint instead of an open-ended project.
38

Compare CSS Modules, CSS-in-JS and utility-first CSS.

Save to ▾
Show answer
CSS Modules scope class names per file at build time, giving plain CSS with no runtime cost. CSS-in-JS libraries write styles in JavaScript with dynamic props, but runtime versions add bundle size and slow server rendering, so zero-runtime options are now preferred. Utility-first frameworks like Tailwind compose small classes in markup, producing small CSS and consistent spacing. The choice depends on team preference and rendering strategy.
39

What are design tokens?

Save to ▾
Show answer
Design tokens are named values for design decisions, such as color-primary, space-4 or radius-md, stored in one place and shared by designers and developers. They are often kept in JSON and converted into CSS custom properties, Tailwind theme values or mobile platform formats. Changing a token updates every component consistently, which makes rebranding or dark mode much easier than hunting hard-coded values.
40

What does the :has() selector allow you to do?

Save to ▾
Show answer
:has() is often called the parent selector because it styles an element based on what it contains or what follows it. For example, .card:has(img) can change padding for cards with images, and form:has(input:invalid) can highlight a form with errors. It removes many cases where JavaScript was used only to toggle classes. It is supported in all modern browsers.
41

What are CSS logical properties?

Save to ▾
Show answer
Logical properties describe layout relative to the writing direction instead of physical sides. margin-inline-start means left in English but right in Arabic or Urdu, and padding-block maps to top and bottom in horizontal text. Using them, with the dir attribute on html, lets one stylesheet support both left-to-right and right-to-left languages without overrides. Sizes like inline-size replace width in the same way.
42

Animate a modal smoothly when it opens without causing layout work.

Save to ▾
Show answer
Animate only transform and opacity, which run on the compositor: .modal { opacity: 0; transform: translateY(16px) scale(0.98); transition: opacity 200ms ease, transform 200ms ease; } .modal.is-open { opacity: 1; transform: none; } @media (prefers-reduced-motion: reduce) { .modal { transition: opacity 100ms linear; transform: none; } } Animating top, height or margin instead would trigger layout on every frame and cause jank on low-end phones.
43

Define brand design tokens in Tailwind CSS v4.

Save to ▾
Show answer
Tokens go in CSS with the @theme directive: @import 'tailwindcss'; @theme { --color-brand-500: #2563eb; --color-brand-700: #1d4ed8; --font-display: 'Poppins', sans-serif; --radius-card: 12px; } /* usage in markup */ /* <button class='bg-brand-500 hover:bg-brand-700 rounded-card font-display'> */ Tailwind generates utilities like bg-brand-500 from these variables and also exposes them as CSS custom properties for use in plain CSS.
44

Write a card that changes layout based on its container width.

Save to ▾
Show answer
Use a container query so the card adapts wherever it is placed: .card-wrapper { container-type: inline-size; } .card { display: grid; gap: 1rem; } @container (min-width: 420px) { .card { grid-template-columns: 160px 1fr; align-items: center; } } In a narrow sidebar the image stacks above text; in a wide main column it sits beside the text, with no viewport media queries.
45

An accordion animation stutters on low-end Android phones. What is wrong?

Save to ▾
Show answer
Most likely it animates height, max-height or margin, which forces layout recalculation every frame. I would check the Performance panel for long layout events. Fixes include animating transform and opacity, using the newer interpolate-size or calc-size features for height where supported, or animating grid-template-rows from 0fr to 1fr, which is cheaper. Testing with CPU throttling in DevTools reveals these issues before users do.
46

Your app must now support an Arabic right-to-left version. What changes are needed in CSS?

Save to ▾
Show answer
I would set dir='rtl' on the html element for that locale and replace physical properties like margin-left, padding-right and left with logical ones such as margin-inline-start and inset-inline-start. Flexbox and Grid follow direction automatically. Directional icons like arrows need flipping with a transform in RTL, while logos and media do not. I would test layouts with real Arabic text, which has different line heights.
47

Tailwind classes built dynamically like 'bg-' + colour work locally but are missing in production. Why?

Save to ▾
Show answer
Tailwind finds classes by scanning source files for complete strings, so a class assembled at runtime never appears in the code and its CSS is not generated. The fix is to map values to full class names, such as const colours = { success: 'bg-green-600', error: 'bg-red-600' }. If truly dynamic values are needed, use inline styles with CSS variables or explicitly safelist those classes.
48

How do you handle a designer who expects pixel-perfect matching on every device?

Save to ▾
Show answer
Situation: a designer flagged small spacing differences between Figma and my build on several phones. Task: I needed to address concerns while keeping the layout fluid. Action: I fixed genuine mistakes immediately, then showed how fixed pixel values would break on other screens and proposed shared spacing tokens. Result: we agreed on tokens and breakpoints, and future handoffs had far fewer revision rounds.
49

Tell me about a time you received critical feedback in a code review.

Save to ▾
Show answer
Situation: a senior engineer rejected my pull request for a new dashboard because the CSS was hard to maintain. Task: I had to rework it without missing the sprint. Action: I asked for a short call to understand the concerns, refactored styles into reusable components with tokens, and added notes explaining my choices. Result: the PR was approved, and the components were later reused in two other screens.
50

What is Interaction to Next Paint and what usually makes it poor?

Save to ▾
Show answer
INP replaced First Input Delay in 2024. It measures the delay from a click, tap or key press until the next frame is painted, across all interactions on the page, reporting roughly the worst one. Poor INP usually comes from long JavaScript tasks blocking the main thread, heavy event handlers, large re-renders and expensive layout work after interaction. Breaking up tasks and showing immediate visual feedback improves it.
51

What is the difference between lab data and field data in performance?

Save to ▾
Show answer
Lab data comes from controlled tests like Lighthouse or WebPageTest on a fixed device and network, which is great for debugging and comparing changes. Field data, or real user monitoring, comes from actual visitors through the Chrome UX Report or your own analytics, capturing real devices, networks and behaviour. Google ranks on field data. A page can score well in the lab but fail in the field on low-end phones.
52

Explain preload, prefetch and preconnect.

Save to ▾
Show answer
preconnect opens an early connection, including DNS, TCP and TLS, to an important third-party origin like a font or API server. preload fetches a resource needed for the current page at high priority, such as the hero image or main font. prefetch downloads resources likely needed for the next navigation at low priority during idle time. Overusing preload wastes bandwidth and can delay more critical resources.
53

What are long tasks and why do they hurt user experience?

Save to ▾
Show answer
A long task is any main-thread work lasting more than 50 milliseconds. While it runs, the browser cannot respond to taps or paint updates, so the page feels frozen. Common sources are large script evaluation, heavy JSON processing, big framework re-renders and third-party scripts. The Performance panel highlights them with red corners, and splitting work into smaller chunks or moving it to a worker keeps the page responsive.
54

How do HTTP caching headers work for frontend assets?

Save to ▾
Show answer
Cache-Control controls caching. Files with content hashes in their names, like app.3f9a2c.js, can use max-age=31536000, immutable because a new build produces a new filename. HTML should use no-cache, which forces revalidation so users get new asset references after a deploy. ETag and Last-Modified allow cheap revalidation with 304 Not Modified responses. Wrong headers cause either stale apps or needless re-downloads.
55

Collect Core Web Vitals from real users with the web-vitals library.

Save to ▾
Show answer
Report metrics to your analytics endpoint: import { onLCP, onINP, onCLS } from 'web-vitals'; function send(metric) { const body = JSON.stringify({ name: metric.name, value: metric.value, rating: metric.rating, page: location.pathname }); navigator.sendBeacon('/analytics/vitals', body); } onLCP(send); onINP(send); onCLS(send); sendBeacon delivers data reliably even when the page is closing. Aggregate at the 75th percentile per page.
56

Configure cache headers for hashed assets and HTML in Nginx.

Save to ▾
Show answer
Long cache for fingerprinted files, revalidation for HTML: location ~* \.(js|css|woff2|avif|webp|svg)$ { add_header Cache-Control 'public, max-age=31536000, immutable'; } location / { try_files $uri /index.html; add_header Cache-Control 'no-cache'; } This works only when build tools add content hashes to filenames. Returning users then load assets from cache instantly, while every deploy still reaches them through fresh HTML.
57

The main JavaScript bundle is 2 MB. How do you reduce it?

Save to ▾
Show answer
I would run a bundle analyser to see what is inside. Usual wins are route-based code splitting, lazy loading heavy widgets, replacing large libraries such as full date or utility packages with smaller alternatives or native APIs, and importing only needed functions so tree shaking works. I would also remove polyfills for browsers we no longer support, and make sure production mode and minification are enabled.
58

Marketing has added many third-party scripts and the site has become slow. What do you do?

Save to ▾
Show answer
I would measure each script's cost using the Performance panel and request blocking in DevTools. Then I would remove unused tags, load the rest with async or defer, delay non-essential ones like chat widgets until user interaction or idle time, and use facades, such as a static image for embedded videos. Moving tag management to a server-side setup and agreeing on a script budget with marketing prevents repeat problems.
59

Most of your users are on low-end Android phones with slow networks. How does that change your approach?

Save to ▾
Show answer
I would test with real budget devices and network throttling rather than a fast laptop. The priorities become sending less JavaScript, rendering important content on the server, compressing images aggressively, and keeping the initial page under a strict size budget. Caching with a service worker helps repeat visits. Features like large animations and heavy carousels should be reduced or loaded only when the device can handle them.
60

An audit finds twenty performance issues. How do you decide what to fix first?

Save to ▾
Show answer
Situation: a Lighthouse and field data audit of our app listed many issues. Task: I had one sprint to show improvement. Action: I ranked issues by user impact and effort, focusing first on the LCP image, render-blocking CSS and one oversized library on the most visited pages. Result: these three fixes delivered most of the gain, and I scheduled the remaining smaller items into later sprints.
61

What is tree shaking and what can prevent it?

Save to ▾
Show answer
Tree shaking removes exports that are never imported from the final bundle. It relies on static ES module imports and exports. It fails with CommonJS modules, dynamic property access on imported objects, and code with side effects that the bundler must keep. Libraries mark themselves safe with sideEffects: false in package.json. Importing { debounce } from a modular package instead of the whole library lets tree shaking work.
62

What is transpilation and how does browserslist control it?

Save to ▾
Show answer
Transpilation converts modern JavaScript or TypeScript into syntax that target browsers understand. Tools include Babel, SWC and esbuild. A browserslist setting, such as '> 0.5%, last 2 versions, not dead', or a build target like es2020, decides which features need conversion and which polyfills to include. Targeting only modern browsers produces smaller, faster code, so the support list should be based on real analytics.
63

How do environment variables work in frontend builds, and what is the risk?

Save to ▾
Show answer
Build tools replace variables at build time, for example Vite exposes only variables prefixed with VITE_ through import.meta.env. The values become plain text inside the JavaScript bundle that anyone can read. So frontend environment variables must never hold secrets like private API keys or database passwords; those belong on a server or backend proxy. Use them only for public configuration such as API base URLs.
64

Why do build tools add content hashes to filenames?

Save to ▾
Show answer
A content hash, like main.8c1f2a.js, changes only when the file's contents change. This lets servers cache assets for a year without worrying about stale code, because a new deploy references new filenames from fresh HTML. Unchanged chunks keep their hashes, so users re-download only what changed. Splitting vendor code into its own chunk improves this, since libraries change less often than app code.
65

Configure a path alias and an API proxy in Vite.

Save to ▾
Show answer
vite.config.ts: import { defineConfig } from 'vite'; import { fileURLToPath, URL } from 'node:url'; export default defineConfig({ resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }); Imports like '@/utils/format' work anywhere, and the proxy avoids CORS issues in development. Add matching paths in tsconfig for editor support.
66

Set up ESLint with the flat config for a TypeScript project.

Save to ▾
Show answer
Install packages and create eslint.config.js: // npm i -D eslint @eslint/js typescript-eslint import js from '@eslint/js'; import tseslint from 'typescript-eslint'; export default tseslint.config( js.configs.recommended, ...tseslint.configs.recommended, { rules: { '@typescript-eslint/no-explicit-any': 'warn', 'no-console': ['warn', { allow: ['error'] }] } }, { ignores: ['dist/'] } ); Run npx eslint . locally and in CI. Leave formatting to Prettier so the two tools do not conflict.
67

Run linting and formatting automatically before each commit.

Save to ▾
Show answer
Use Husky with lint-staged: npm i -D husky lint-staged npx husky init echo 'npx lint-staged' > .husky/pre-commit // package.json "lint-staged": { "*.{ts,tsx,js}": ["eslint --fix", "prettier --write"], "*.{css,md,json}": "prettier --write" } Only staged files are checked, so commits stay fast. CI should still run the full lint because hooks can be skipped locally.
68

Write a GitHub Actions workflow that lints, tests and builds a frontend app.

Save to ▾
Show answer
A typical CI pipeline: name: CI on: [pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - run: npm ci - run: npm run lint - run: npm test -- --run - run: npm run build npm ci installs exactly from the lockfile, and caching speeds up repeat runs. Branch protection can require this check before merging.
69

The build works on your machine but fails in CI. How do you troubleshoot?

Save to ▾
Show answer
I would compare environments first: Node version, package manager version, and whether CI uses npm ci with the committed lockfile while I used npm install. Other common causes are case-sensitive file paths on Linux, missing environment variables, and files ignored by Git. Pinning Node with an .nvmrc or engines field and reproducing the CI command locally in a clean clone or container usually reveals the difference.
70

npm audit reports a critical vulnerability in a dependency. What do you do?

Save to ▾
Show answer
I would check whether the vulnerable package is actually used in the production bundle or only in build tooling, and whether the vulnerable code path is reachable. Then I would update to a patched version, using npm update or overrides for transitive dependencies. If no fix exists, I would look for an alternative package or mitigate. Automated tools like Dependabot or Renovate keep dependencies current.
71

After a release, the app shows a blank page in some older browsers. What could be wrong?

Save to ▾
Show answer
The build probably targets newer syntax or uses an API that older browsers lack, causing a parse error that stops the whole bundle. I would check the console in that browser, compare the build target and browserslist with our support policy, and add missing polyfills for used APIs. Error monitoring with browser breakdowns catches this quickly, and testing on the oldest supported browser should be part of release checks.
72

Why are many teams moving from Jest to Vitest?

Save to ▾
Show answer
Vitest reuses the Vite configuration and transform pipeline, so TypeScript, JSX, ES modules and path aliases work without separate Babel or ts-jest setup. It is fast thanks to on-demand transformation and smart watch mode, and its API is largely compatible with Jest, making migration easy. Jest remains mature and widely used, especially in projects not built with Vite. Both support mocking, snapshots and coverage.
73

What is the guiding principle of Testing Library and why query by role?

Save to ▾
Show answer
Testing Library encourages testing the way users interact: find elements by what they see and hear, not by implementation details like class names or component internals. getByRole('button', { name: 'Submit' }) is preferred because it also verifies the element is accessible. Tests written this way survive refactors and catch real accessibility problems. Test ids are a last resort when no accessible query works.
74

When should you mock in frontend tests, and what is Mock Service Worker?

Save to ▾
Show answer
Mock things that are slow, nondeterministic or outside your control, such as network calls, timers and browser APIs, but avoid mocking your own modules heavily, which makes tests pass while the real integration breaks. Mock Service Worker intercepts requests at the network level in both browser and Node, so your real fetch code runs against defined handlers. The same handlers can power tests, Storybook and local development.
75

What are the pros and cons of snapshot testing?

Save to ▾
Show answer
Snapshot tests save a component's rendered output and fail when it changes, which quickly detects unintended changes with little effort. However, large snapshots are hard to review, so developers often update them blindly, and they break on harmless markup changes. They work best for small, stable outputs like serialised config or error messages. For UI behaviour, explicit assertions on visible text and roles are more meaningful.
76

What is visual regression testing?

Save to ▾
Show answer
Visual regression testing captures screenshots of pages or components and compares them pixel by pixel with approved baselines, flagging unexpected visual changes such as broken spacing or wrong colours. Tools include Playwright's toHaveScreenshot, Chromatic with Storybook and Percy. To avoid false failures, run in consistent containers, disable animations, and mask dynamic content like dates. It catches CSS bugs that functional tests miss.
77

Mock fetch in a Vitest test to check error handling.

Save to ▾
Show answer
Stub the global fetch and assert behaviour: import { it, expect, vi, afterEach } from 'vitest'; import { loadCourses } from './api'; afterEach(() => vi.unstubAllGlobals()); it('returns an empty list when the server fails', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue( new Response('Server error', { status: 500 }) )); const result = await loadCourses(); expect(result).toEqual([]); expect(fetch).toHaveBeenCalledWith('/api/courses'); }); For larger suites, MSW handlers are cleaner than stubbing fetch in every test.
78

Write a Playwright end-to-end test for a login flow.

Save to ▾
Show answer
Use role-based locators and web-first assertions: import { test, expect } from '@playwright/test'; test('user can log in and see dashboard', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('student@example.com'); await page.getByLabel('Password').fill('Secret@123'); await page.getByRole('button', { name: 'Log in' }).click(); await expect(page).toHaveURL(/dashboard/); await expect(page.getByRole('heading', { name: 'My courses' })).toBeVisible(); }); Assertions auto-wait, so no manual sleeps are needed. Save login state with storageState to reuse in other tests.
79

Test a DOM form with Testing Library and user-event.

Save to ▾
Show answer
Simulate real typing and clicks: import { screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import { mountSignupForm } from './signup'; it('shows an error for an invalid mobile number', async () => { document.body.innerHTML = '<div id=app></div>'; mountSignupForm(document.getElementById('app')); const user = userEvent.setup(); await user.type(screen.getByLabelText('Mobile'), '12345'); await user.click(screen.getByRole('button', { name: 'Sign up' })); expect(screen.getByRole('alert')).toHaveTextContent('Enter a valid mobile number'); }); toHaveTextContent comes from @testing-library/jest-dom.
80

You must change a complex legacy module that has no tests. How do you proceed safely?

Save to ▾
Show answer
I would first write characterisation tests that capture the current behaviour, including odd cases, by calling the module with real inputs and recording outputs. With that safety net, I make small refactors to separate logic from DOM or network code, adding focused tests as I go. Then I implement the change. Small commits and feature flags let me release gradually and roll back quickly if something breaks.
81

Management requires 100 percent code coverage, but bugs still appear. How do you respond?

Save to ▾
Show answer
Coverage shows which lines ran, not whether the behaviour was verified, so tests can execute code without meaningful assertions. I would explain this with an example and propose a balanced target, perhaps 80 percent on business logic, combined with quality measures like mutation testing, critical user flows covered by end-to-end tests and tracking escaped defects. Effort is better spent on risky areas than on trivial getters.
82

How do you test code that depends on the current date or timers?

Save to ▾
Show answer
Real time makes tests slow and nondeterministic. I would use fake timers, such as vi.useFakeTimers() and vi.setSystemTime(new Date('2026-11-12T10:00:00+05:30')), then advance time with vi.advanceTimersByTime to test debounce, countdowns or expiry logic instantly. Restoring real timers after each test avoids leaking state. Designing functions to accept a clock or date parameter makes them even easier to test.
83

Tell me about a time you pushed back on releasing code without tests.

Save to ▾
Show answer
Situation: near a release deadline, a teammate suggested skipping tests for a new checkout coupon feature. Task: I felt the risk to payments was too high. Action: I proposed covering only the critical paths, valid, expired and invalid coupons, and wrote those tests myself in two hours. Result: one test caught a discount rounding bug before release, and the feature shipped on time.
84

What is the difference between local UI state, global state and server state?

Save to ▾
Show answer
Local state belongs to one component, like whether a dropdown is open. Global client state is shared across the app, like theme or the logged-in user. Server state is data owned by the backend, such as orders, that must be fetched, cached, refreshed and kept in sync. Treating server state separately with tools like TanStack Query removes much of the code teams once wrote in global stores.
85

What does component-driven development mean?

Save to ▾
Show answer
Component-driven development builds UIs from the bottom up as small, reusable, isolated components, such as buttons and inputs, composed into larger patterns like forms and pages. Each component has a clear API through props or attributes and is developed and documented in isolation, often in Storybook. Composition over configuration keeps components flexible. This approach enables design systems, parallel work and easier testing.
86

Write a minimal observable store in plain JavaScript.

Save to ▾
Show answer
A store holds state and notifies subscribers: function createStore(initial) { let state = initial; const listeners = new Set(); return { get: () => state, set(update) { state = { ...state, ...update }; listeners.forEach(fn => fn(state)); }, subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); } }; } const cart = createStore({ items: 0 }); const stop = cart.subscribe(s => (badge.textContent = s.items)); cart.set({ items: 2 }); The returned unsubscribe function prevents memory leaks.
87

Implement simple feature flags on the frontend.

Save to ▾
Show answer
Load flags once and check them where needed: let flags = {}; export async function loadFlags(userId) { const res = await fetch('/api/flags?user=' + encodeURIComponent(userId)); flags = res.ok ? await res.json() : {}; } export const isEnabled = (name) => flags[name] === true; // usage await loadFlags(currentUser.id); if (isEnabled('new-checkout')) renderNewCheckout(); else renderOldCheckout(); Defaulting to off when the request fails keeps the stable path. Flags enable gradual rollouts, but old flags must be removed to avoid clutter.
88

Write a React custom hook that fetches data and cancels on unmount.

Save to ▾
Show answer
Use AbortController inside useEffect: import { useEffect, useState } from 'react'; export function useFetch(url) { const [state, setState] = useState({ data: null, error: null, loading: true }); useEffect(() => { const controller = new AbortController(); setState(s => ({ ...s, loading: true })); fetch(url, { signal: controller.signal }) .then(r => (r.ok ? r.json() : Promise.reject(new Error(r.status)))) .then(data => setState({ data, error: null, loading: false })) .catch(err => { if (err.name !== 'AbortError') setState({ data: null, error: err, loading: false }); }); return () => controller.abort(); }, [url]); return state; } In production apps, TanStack Query adds caching and retries.
89

State is passed through many component layers and global state has become chaotic. How do you restructure it?

Save to ▾
Show answer
I would first classify state: server data moves to a data-fetching cache like TanStack Query, URL-worthy state such as filters moves to query parameters, and truly local state moves back into components. What remains global, like user session and theme, goes into a small, well-defined store or context split by concern. Composition patterns, passing components as children, also remove much prop drilling.
90

Your app needs to support Hindi and English. How do you design internationalisation?

Save to ▾
Show answer
I would move all UI text into translation files keyed by message ids, use an i18n library that supports plurals and interpolation, and format numbers, dates and currency with Intl APIs for each locale, like en-IN and hi-IN. Layouts must handle longer text and Devanagari fonts with proper line height. The locale goes in the URL, such as /hi/, with hreflang tags for SEO and lang on html.
91

A dashboard must show live order updates. Would you use polling, Server-Sent Events or WebSockets?

Save to ▾
Show answer
Polling is simplest and fine if updates every 30 seconds are acceptable. Server-Sent Events push one-way updates from server to browser over HTTP with automatic reconnection, ideal for live orders, notifications and stock tickers. WebSockets provide full two-way communication, needed for chat or collaborative editing, but require more infrastructure. For a one-way dashboard, I would choose SSE and reconnect gracefully on network changes.
92

How would you choose a frontend framework for a new project?

Save to ▾
Show answer
I would base it on project needs and team context rather than trends: team experience, hiring market in India, SEO and rendering requirements, ecosystem maturity, long-term support and performance on target devices. React has the largest ecosystem and talent pool, Angular suits large enterprise teams wanting an opinionated structure, Vue is approachable, and Svelte or Astro produce less JavaScript. A small proof of concept helps confirm the choice.
93

Users report errors you cannot reproduce. How do you set up frontend observability?

Save to ▾
Show answer
I would add an error monitoring tool like Sentry with uploaded source maps, release tags and user context such as browser and route, without personal data. Real user monitoring tracks Web Vitals and slow API calls. Breadcrumbs of clicks and network requests before an error help reproduce issues, and session replay with privacy masking shows what users saw. Alerts on error spikes after a release enable quick rollbacks.
94

How do you estimate the time needed for a frontend feature?

Save to ▾
Show answer
I break the feature into small parts: components, states such as loading, empty and error, API integration, responsive behaviour, accessibility and tests. I estimate each part, add time for reviews and unknowns, and flag dependencies like pending designs or APIs. For example, a filter panel I estimated at five days was delivered in five because I had listed edge cases upfront instead of discovering them late.
95

How do you balance technical debt against delivering new features?

Save to ▾
Show answer
Situation: our team constantly added features while an outdated build setup slowed everyone. Task: I needed time for cleanup without stalling the roadmap. Action: I quantified the cost, about 30 minutes lost per developer daily, and proposed reserving roughly 15 percent of each sprint for debt, starting with the biggest pain points. Result: the product manager agreed, build times halved, and feature delivery actually sped up.

Frequently asked questions

Who should practise medium Frontend Development questions?

Anyone with some hands-on practice: most real interview time is spent on medium questions like these.

How should I practise these?

Answer each question aloud before opening the answer, mark it as solved only when your answer matches the key idea, and save misses to Last-Minute Revision.

Other difficulty levels