Frontend Development Interview Questions for 2 Years Experience
40 questions with answers for candidates with 2 years of experience — 6 easy, 34 medium.
What interviewers expect: 2 years experience
At two years, interviews cover performance basics such as Core Web Vitals and code splitting, build tools, state management choices, testing with Vitest or Playwright, and reusable component design. Scenario questions test how you debug slow pages, flaky tests and production errors. You should explain trade-offs, not just definitions.
This set is weighted towards CSS Architecture and Accessibility (8), Frontend Architecture (7), TypeScript (6), Modern JavaScript (6).
Web performance Component design Build tooling Debugging production issues
Solved at this levelStart 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
Write a type-safe getProperty function using generics and keyof.
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.
02
npm audit reports a critical vulnerability in a dependency. What do you do?
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.
A higher-order function takes a function as an argument or returns one, like map, filter or a custom withLogging(fn) wrapper. Currying transforms a function with many arguments into a chain of single-argument functions, so add(2)(3) returns 5. Currying makes partial application easy, for example creating formatINR = formatCurrency('INR') once and reusing it. Overusing it can reduce readability for teams unfamiliar with functional style.
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.
05
At a high level, how does React's virtual DOM and reconciliation work?
React components return a description of the UI as a tree of elements. When state changes, React renders a new tree and compares it with the previous one, a process called reconciliation, then applies only the minimal DOM changes needed. Keys help it match list items correctly. The React Compiler now adds automatic memoisation, reducing unnecessary re-renders without manual useMemo calls.
06
Model API request state with a discriminated union.
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.
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.
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.
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.
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.
11
Search results sometimes show data for an older query. What is happening?
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.
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.
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.
14
What are pure functions and why does immutability matter in frontend code?
A pure function returns the same output for the same input and has no side effects such as changing external variables. Immutability means creating new objects instead of modifying existing ones, for example using spread syntax to update state. Together they make code predictable and easy to test, and they let frameworks detect changes with a simple reference comparison, which enables efficient re-rendering.
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.
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.
17
Animate a modal smoothly when it opens without causing layout work.
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.
18
Write a card that changes layout based on its container width.
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.
19
What are declaration files (.d.ts) and when do you write them?
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.
20
How do you test code that depends on the current date or timers?
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.
21
How would you choose a frontend framework for a new project?
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.
22
An audit finds twenty performance issues. How do you decide what to fix first?
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.
23
Compare SPA, MPA, server-side rendering and static site generation.
A single-page app loads once and renders views in the browser, giving app-like navigation but slower first load. A multi-page app renders each page on the server with full reloads. Server-side rendering generates HTML per request for fast content and SEO, then hydrates. Static site generation pre-builds HTML at build time and serves it from a CDN, which is fastest for content that changes rarely, like blogs.
24
Tell me about a time you pushed back on releasing code without tests.
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.
25
Write a React custom hook that fetches data and cancels on unmount.
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.
27
Write a Playwright end-to-end test for a login flow.
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.
28
You must change a complex legacy module that has no tests. How do you proceed safely?
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.
29
Tell me about a time you received critical feedback in a code review.
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.
30
How does the History API enable client-side routing?
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.
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.
32
Explain optional chaining and the nullish coalescing operator.
Optional chaining ?. stops evaluation and returns undefined if the value before it is null or undefined, so user?.address?.pincode never throws. Nullish coalescing ?? returns the right side only when the left is null or undefined, unlike || which also replaces 0, false and empty strings. For example, const qty = input ?? 1 keeps a valid 0, while input || 1 would wrongly change 0 into 1.
33
What is prefers-reduced-motion and why should you respect it?
prefers-reduced-motion is a media query that detects when users have asked their operating system to minimise animation, often because motion causes dizziness or nausea. Inside @media (prefers-reduced-motion: reduce), you should remove or tone down parallax, large transitions and auto-playing animations, while keeping essential feedback such as simple fades. It is a small change that makes interfaces accessible to people with vestibular disorders.
34
Write a minimal observable store in plain JavaScript.
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.
36
Your app must now support an Arabic right-to-left version. What changes are needed in CSS?
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.
37
A dashboard must show live order updates. Would you use polling, Server-Sent Events or WebSockets?
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.
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.
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.
40
Tell me about a time you convinced your team to adopt a new tool like TypeScript.
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.
Frequently asked questions
What is asked in Frontend Development interviews for 2 years of experience?
At two years, interviews cover performance basics such as Core Web Vitals and code splitting, build tools, state management choices, testing with Vitest or Playwright, and reusable component design. Scenario questions test how you debug slow pages, flaky tests and production errors. You should explain trade-offs, not just definitions.
How many questions should I practise before a Frontend Development interview?
Work through all 40 questions here, then the company-wise sets for the employers you are targeting. Quality matters more than count: be able to explain why each answer is right.
Which topics matter most for 2 years of experience?
Focus first on Web performance, Component design, Build tooling, Debugging production issues.