Top 20 interview questions
Event Loop
JavaScript is single-threaded - it can only do one thing at a time. The event loop is the mechanism that handles asynchronous operations.
Components
text┌─────────────────────────────────┐ │ Call Stack │ <- executes synchronous code ├─────────────────────────────────┤ │ Microtask Queue │ <- Promise callbacks, queueMicrotask ├─────────────────────────────────┤ │ Macrotask Queue │ <- setTimeout, setInterval, I/O └─────────────────────────────────┘Order of execution
- Run all synchronous code (call stack).
- Run all microtasks (Promise
.then,queueMicrotask). - Run one macrotask (setTimeout, setInterval).
- Repeat from step 2.
Example
jsconsole.log('1 - sync'); setTimeout(() => console.log('2 - macrotask'), 0); Promise.resolve().then(() => console.log('3 - microtask')); console.log('4 - sync'); // Output: // 1 - sync // 4 - sync // 3 - microtask // 2 - macrotaskWhy microtasks come first
jsPromise.resolve() .then(() => console.log('micro 1')) .then(() => console.log('micro 2')); setTimeout(() => console.log('macro'), 0); // micro 1 -> micro 2 -> macro // ALL microtasks drain before any macrotask runsUnderstanding the event loop is critical for debugging timing bugs, infinite loops in Promises, and understanding why
setTimeout(fn, 0)doesn't mean "run immediately".updated
did you know it?Closures
A closure is a function that remembers the variables from its outer scope even after that scope has finished executing.
Basic example
jsfunction makeCounter() { let count = 0; // this variable is "closed over" return function() { count++; return count; }; } const counter = makeCounter(); counter(); // 1 counter(); // 2 counter(); // 3 // count is private - can't be accessed from outsidePractical use cases
Private state (data encapsulation)
jsfunction createStore(initialValue) { let state = initialValue; return { get: () => state, set: (value) => { state = value; }, }; }Function factories
jsfunction multiply(factor) { return (number) => number * factor; } const double = multiply(2); const triple = multiply(3); double(5); // 10 triple(5); // 15Memoization
jsfunction memoize(fn) { const cache = new Map(); return function(key) { if (cache.has(key)) return cache.get(key); const result = fn(key); cache.set(key, result); return result; }; }Every function in JavaScript is a closure - it always has access to its surrounding scope. The interesting cases are when the outer function has already returned.
updated
did you know it?Promises vs async/await
Both handle asynchronous operations -
async/awaitis syntactic sugar built on top of Promises.Promise
jsfunction getUser(id) { return fetch(`/api/users/${id}`) .then(res => res.json()) .then(user => { return fetch(`/api/posts/${user.id}`); }) .then(res => res.json()) .catch(err => console.error(err)); }async/await - same logic, readable code
jsasync function getUser(id) { try { const res = await fetch(`/api/users/${id}`); const user = await res.json(); const postsRes = await fetch(`/api/posts/${user.id}`); return await postsRes.json(); } catch (err) { console.error(err); } }Promise states
State Description pendingInitial state - not yet settled fulfilledOperation completed successfully rejectedOperation failed Parallel execution
js// sequential - slow (waits for each) const user = await getUser(); const posts = await getPosts(); // parallel - fast (runs simultaneously) const [user, posts] = await Promise.all([getUser(), getPosts()]);Other Promise methods
jsPromise.allSettled([...]) // waits for all, even if some reject Promise.race([...]) // resolves/rejects with the first settled Promise.any([...]) // resolves with the first fulfilledUse
async/awaitfor readability. Use raw Promises when composing withPromise.all,Promise.race, or building utilities.updated
did you know it?Prototypal Inheritance
In JavaScript, objects inherit from other objects through a prototype chain. Every object has an internal
[[Prototype]]link to another object.Prototype chain
jsconst animal = { speak() { return `${this.name} makes a sound`; } }; const dog = Object.create(animal); dog.name = 'Rex'; dog.speak(); // 'Rex makes a sound' // dog has no speak() - JS looks up the chain to animalHow property lookup works
textdog.speak() -> look in dog: not found -> look in animal (dog's prototype): found! -> look in Object.prototype: ... -> null - end of chainClass syntax (sugar over prototypes)
jsclass Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; } } class Dog extends Animal { speak() { return `${this.name} barks`; } } const d = new Dog('Rex'); d.speak(); // 'Rex barks' // under the hood: // Dog.prototype -> Animal.prototype -> Object.prototype -> nullChecking the chain
jsd instanceof Dog; // true d instanceof Animal; // true Object.getPrototypeOf(d) === Dog.prototype; // trueJavaScript
classis syntactic sugar - under the hood it still uses prototype chains. Understanding this helps debug unexpected behavior withthisand inheritance.updated
did you know it?call, apply, bind
All three control what
thisrefers to when calling a function.call - invoke immediately with args spread
jsfunction greet(greeting, punctuation) { return `${greeting}, ${this.name}${punctuation}`; } const user = { name: 'Alice' }; greet.call(user, 'Hello', '!'); // 'Hello, Alice!'apply - invoke immediately with args as array
jsgreet.apply(user, ['Hello', '!']); // 'Hello, Alice!' // useful for spreading arrays into functions const nums = [3, 1, 4, 1, 5]; Math.max.apply(null, nums); // 5 // modern equivalent: Math.max(...nums)bind - returns a new function with this locked
jsconst greetAlice = greet.bind(user, 'Hello'); greetAlice('!'); // 'Hello, Alice!' greetAlice('?'); // 'Hello, Alice?' // common use case - preserving this in callbacks class Timer { constructor() { this.count = 0; } start() { setInterval(this.tick.bind(this), 1000); } tick() { this.count++; } }Comparison
Invokes immediately Args format Returns callYes Spread Result applyYes Array Result bindNo Spread New function In modern code
bindis often replaced by arrow functions (() => this.tick()), andapplyis replaced by the spread operator. But understanding all three is essential for interviews.updated
did you know it?type vs interface
Both define the shape of an object, but have different capabilities.
Key differences
typeinterfaceObject shape Yes Yes Primitives/unions Yes No Declaration merging No Yes Extends Via &Via extendsMapped types ( [K in keyof T])Yes No Declaration merging (interface only)
tsinterface User { name: string; } interface User { age: number; } // result: { name: string; age: number; }Union types (type only)
tstype ID = string | number; type Status = 'active' | 'inactive' | 'banned';Extending
ts// interface interface Animal { name: string; } interface Dog extends Animal { breed: string; } // type type Animal = { name: string; }; type Dog = Animal & { breed: string; };Rule of thumb: use
interfacefor object shapes that may be extended or merged (e.g. library public APIs). Usetypefor unions, intersections, and utility types.updated
did you know it?unknown vs any
Both represent "any possible value" but with very different safety guarantees.
any - turns off type checking
tslet value: any = 'hello'; value.toUpperCase(); // ok value.nonExistentMethod(); // ok - no error, but crashes at runtime value = 42; // okanyis a complete escape hatch - TypeScript stops checking entirely.unknown - type-safe alternative
tslet value: unknown = 'hello'; value.toUpperCase(); // Error - can't use without narrowing // you must narrow the type first if (typeof value === 'string') { value.toUpperCase(); // ok }Comparison
anyunknownAssignable to anything Yes No Operations without check Yes No Type narrowing required No Yes Safety None Full When to use
ts// any - only when migrating JS code or in tests const legacy: any = getLegacyData(); // unknown - for external data (API, JSON.parse, try/catch) try { await riskyOperation(); } catch (error: unknown) { if (error instanceof Error) { console.log(error.message); } }Prefer
unknownoveranywhenever possible - it forces you to handle all cases explicitly.updated
did you know it?Generics
Generics allow you to write reusable, type-safe code that works with different types without losing type information.
Without generics
ts// loses type information function identity(value: any): any { return value; }With generics
tsfunction identity<T>(value: T): T { return value; } const str = identity('hello'); // type: string const num = identity(42); // type: numberGeneric constraints
tsfunction getLength<T extends { length: number }>(value: T): number { return value.length; } getLength('hello'); // 5 getLength([1, 2, 3]); // 3Generic interfaces and types
tsinterface ApiResponse<T> { data: T; status: number; message: string; } type UserResponse = ApiResponse<User>; type ListResponse = ApiResponse<User[]>;Generics are the foundation of reusable utilities like
Array<T>,Promise<T>,Record<K, V>- they are everywhere in TypeScript.updated
did you know it?Virtual DOM
Virtual DOM is a lightweight in-memory representation of the real DOM tree as JavaScript objects.
How the update process works
- When state changes, React creates a new virtual tree.
- The reconciliation algorithm compares the old and new trees (diffing).
- React computes the minimal set of changes.
- Only those changes are applied to the real DOM.
Why it matters
Real DOM operations are expensive. Manipulating JS objects is faster, so batching and the diff algorithm provide a noticeable performance boost.
React 18+ has concurrent rendering: updates marked as transitions (
startTransition,useDeferredValue) can be interrupted and deferred so urgent input stays responsive. It's opt-in per update, not a separate "Concurrent Mode".updated
did you know it?Reconciliation and keys
The reconciliation algorithm
React compares the old and new virtual trees using two rules:
- Different element types - the old tree is destroyed and a new one is mounted.
- Same types - only changed attributes/props are updated.
Why keys are needed
When rendering lists React uses
keyto identify elements between renders.tsx// Bad - React may mix up elements items.map((item, index) => <Item key={index} {...item} />) // Good - stable unique identifier items.map((item) => <Item key={item.id} {...item} />)What happens with wrong keys
- Unnecessary unmount/mount instead of update.
- Loss of component state.
- Broken animations.
keyis also used as an intentional state reset - changing a component'skeyfully recreates it.updated
did you know it?useMemo vs useCallback
useMemomemoizes a computed value and returns it.useCallbackmemoizes a function reference.When to use
Hook Use when useMemoYou want to avoid expensive recalculations useCallbackYou want to prevent unnecessary re-renders caused by new function references Example
tsx// useMemo - memoizes a value const sortedList = useMemo(() => { return items.toSorted((a, b) => a.price - b.price); // sort() would mutate props }, [items]); // useCallback - memoizes a function const handleClick = useCallback(() => { doSomething(id); }, [id]);React Compiler
React Compiler (stable since October 2025) memoizes components, values and callbacks automatically at build time. In a project that uses it, manual
useMemo/useCallbackare rarely needed - mostly as an escape hatch, e.g. for a value used as an effect dependency.Note: without the compiler, don't overuse these hooks - they have their own overhead. Apply only where a profiler shows a real problem.
updated
did you know it?useEffect vs useLayoutEffect
Execution order
textRender -> DOM mutations -> useLayoutEffect -> Browser paints -> useEffectuseEffectuseLayoutEffectWhen After paint (async) Before paint (sync) Blocks browser No Yes SSR Doesn't run on the server Doesn't run on the server (the SSR warning was removed in React 19) When to use
useLayoutEffect- You need to measure DOM element dimensions (
getBoundingClientRect). - You need to synchronously update the DOM to avoid visual flickering.
tsxuseLayoutEffect(() => { const { height } = ref.current.getBoundingClientRect(); setHeight(height); // before paint - user won't see a jump }, []);Rule: use
useEffectby default. Switch touseLayoutEffectonly when you see visible visual artifacts.updated
did you know it?- You need to measure DOM element dimensions (
React Context
Contextlets you pass data through the component tree without prop drilling.How it works
tsxconst ThemeContext = createContext('light'); function App() { return ( // React 19: the context itself is the provider (<ThemeContext.Provider> still works) <ThemeContext value="dark"> <Child /> </ThemeContext> ); } function Child() { const theme = useContext(ThemeContext); return <div>{theme}</div>; }Performance problem
Every context consumer re-renders on any change to
value- even if it only uses part of the data.Solutions
- Split contexts - a separate one for frequently changing data.
- Memoize
valuewithuseMemo. - Use a state manager (Zustand, Jotai) for high-frequency updates.
Context works great for rarely changing data: theme, locale, authentication.
updated
did you know it?React Server Components (RSC)
RSC are components that render exclusively on the server and send no JavaScript to the client.
Comparison
Server Component Client Component Rendering Server Client + Server (hydration) JS in bundle No Yes Hooks ( useStateetc.)No Yes Direct DB/FS access Yes No Interactivity No Yes Example
tsx// ServerComponent.tsx - no 'use client' async function ProductPage({ id }: { id: string }) { const product = await db.products.findById(id); // directly on the server return <ProductCard product={product} />; }Key benefits
- Less JavaScript in the bundle - faster load.
- Data fetching close to the data source.
- Sensitive data never reaches the client.
The
'use client'directive turns a component into a Client Component. In Next.js App Router all components are Server Components by default.updated
did you know it?Types of State
Understanding where state lives is the foundation of good frontend architecture.
Local state
Belongs to a single component. Not shared.
tsxfunction Counter() { const [count, setCount] = useState(0); // local return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }Global (client) state
Shared across many components. Managed by a store.
tsx// auth, theme, shopping cart, notifications const user = useAuthStore(s => s.user); const theme = useThemeStore(s => s.theme);Server state
Data that lives on the server - fetched, cached, and synchronized.
tsx// React Query / SWR handle this const { data: posts, isLoading } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, });URL state
State encoded in the URL - shareable and bookmarkable.
tsx// search filters, pagination, active tab // /products?category=shoes&sort=price&page=2 const [searchParams, setSearchParams] = useSearchParams(); const category = searchParams.get('category');Where each belongs
Type Examples Tool Local Modal open, form input, toggle useState,useReducerGlobal Auth, theme, cart Zustand, Jotai, Redux Server API data, lists, user profile React Query, SWR URL Filters, pagination, tabs useSearchParams, routerOne of the most common architecture mistakes is storing server state in a global store (Redux). React Query / SWR exist specifically to handle server state - let them do it.
updated
did you know it?CSS Specificity
Specificity determines which CSS rule wins when multiple rules target the same element. It's calculated as a score in three categories.
Specificity score: (A, B, C)
Category What counts Example A - IDs #id#header= (1,0,0)B - Classes/attributes/pseudo-classes .class,[attr],:hover.btn:hover= (0,2,0)C - Elements/pseudo-elements div,p,::beforediv p= (0,0,2)Calculation examples
cssp /* (0,0,1) */ .class /* (0,1,0) */ #id /* (1,0,0) */ div .class p /* (0,1,2) */ #id .class:hover /* (1,2,0) */Comparing scores
css/* which color wins? */ #header .nav a { color: red; } /* (1,1,1) */ .nav a:hover { color: blue; } /* (0,2,1) */ /* red wins - (1,1,1) beats (0,2,1) */ /* A column always beats any number of B or C */Special cases
css/* inline styles - highest specificity (beats everything except !important) */ <div style="color: red"> /* !important - overrides everything */ .btn { color: blue !important; } /* :is(), :not(), :has() - take specificity of their most specific argument */ :is(#id, .class) { } /* (1,0,0) */ /* :where() - always zero specificity, easy to override */ :where(#id, .class) { } /* (0,0,0) */Cascade layers
Specificity is compared only inside the same
@layer. A rule in a later layer wins over a more specific rule in an earlier one, and unlayered styles beat all layers:css@layer reset, components; @layer reset { #app a { color: gray; } } /* (1,0,1) but loses */ @layer components { .link { color: blue; } } /* wins - later layer */Avoid
!important- it breaks the cascade and makes debugging painful. Use:where()for low-specificity defaults and@layerto order groups of styles instead of fighting with selectors.updated
did you know it?Flexbox vs CSS Grid
Flexbox - one-dimensional layout
Designed for laying out items in a single row or column.
css.nav { display: flex; justify-content: space-between; /* main axis */ align-items: center; /* cross axis */ gap: 16px; }css/* centering - the classic use case */ .container { display: flex; justify-content: center; align-items: center; }CSS Grid - two-dimensional layout
Designed for laying out items in rows and columns simultaneously.
css.layout { display: grid; grid-template-columns: 240px 1fr; grid-template-rows: 60px 1fr 40px; grid-template-areas: 'sidebar header' 'sidebar main' 'sidebar footer'; gap: 16px; } .sidebar { grid-area: sidebar; } .header { grid-area: header; } .main { grid-area: main; }When to use which
Use case Flexbox Grid Navigation bar Yes - Centering content Yes Yes Card row Yes - Page layout - Yes Complex 2D layouts - Yes Item alignment in a line Yes - They work great together
css/* Grid for page structure */ .page { display: grid; grid-template-columns: 240px 1fr; } /* Flex for items inside a grid cell */ .header { display: flex; justify-content: space-between; }Rule of thumb: if you're thinking in rows OR columns - use Flexbox. If you're thinking in rows AND columns - use Grid.
updated
did you know it?Critical Rendering Path
The critical rendering path is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into pixels on screen.
Steps
textHTML -> DOM CSS -> CSSOM DOM + CSSOM -> Render Tree -> Layout -> Paint -> Composite1 - Parsing
textHTML bytes -> tokens -> nodes -> DOM tree CSS bytes -> tokens -> nodes -> CSSOM treeJavaScript blocks HTML parsing by default - the parser stops until the script is downloaded and executed.
2 - Render Tree
The browser combines DOM + CSSOM into a render tree - only visible elements with their computed styles.
display: noneelements are excluded.3 - Layout (Reflow)
Calculates the exact position and size of every element. Triggered by any change to geometry: width, height, margin, font size, DOM additions.
4 - Paint
Fills in pixels - colors, borders, shadows, text. Expensive for complex styles like
box-shadow.5 - Composite
Layers are combined in the correct order by the GPU. Compositing is cheap - properties like
transformandopacityonly trigger this step.Performance implications
texttransform / opacity -> Composite only (GPU) - fastest color / background -> Paint + Composite - fast width / margin -> Layout + Paint + Composite - slowBlocking resources
html<!-- blocks rendering until downloaded + executed --> <script src="app.js"></script> <!-- downloaded in parallel, executed after HTML parsed --> <script src="app.js" defer></script> <!-- downloaded in parallel, executed immediately when ready --> <script src="app.js" async></script>To optimize: minimize render-blocking resources, use
deferon scripts, avoid forced synchronous layouts (reading layout properties after DOM writes), and animate onlytransformandopacity.updated
did you know it?CORS - Cross-Origin Resource Sharing
CORS is a browser mechanism that relaxes the same-origin policy: by default a page can't read responses from a different origin, and CORS headers let a server allow specific origins to do so.
What is an origin
texthttps://app.example.com:443/path |_____| |_____________| |__| scheme host port // same origin https://app.example.com/other-path - same // different origin (cross-origin) http://app.example.com - different scheme https://api.example.com - different host https://app.example.com:8080 - different portSimple requests
GET, HEAD or POST with basic headers and form/text content types - the browser sends the request right away and then checks the response headers. If they don't allow the origin, the request still reached the server, but JavaScript can't read the response.
textRequest: GET https://api.example.com/data Origin: https://app.example.com Response: Access-Control-Allow-Origin: https://app.example.com (or * for any origin)Preflight requests
For non-simple requests (PUT, DELETE, custom headers) - browser sends an OPTIONS request first.
textPreflight OPTIONS: Origin: https://app.example.com Access-Control-Request-Method: DELETE Access-Control-Request-Headers: Authorization Server response: Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Authorization, Content-Type Access-Control-Max-Age: 86400 (cache preflight for 24h)Server configuration (Express)
tsimport cors from 'cors'; app.use(cors({ origin: ['https://app.example.com', 'https://admin.example.com'], methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Authorization', 'Content-Type'], credentials: true, // allow cookies }));Credentials and cookies
ts// client must opt in fetch('https://api.example.com/data', { credentials: 'include' }); // server must explicitly allow // Access-Control-Allow-Credentials: true // Access-Control-Allow-Origin must NOT be * when credentials: trueCORS is enforced by the browser, not the server. Direct requests from curl or Postman bypass it entirely. The server only needs to send the right headers.
updated
did you know it?Rendering Strategies
CSR - Client-Side Rendering
The server sends a minimal HTML shell. The browser downloads JS and renders everything.
textServer: <div id="root"></div> + bundle.js Browser: downloads JS -> executes -> renders page- Fast navigation after initial load (SPA).
- Slow first load, weaker SEO (content not in initial HTML).
- Tools: Vite + React (Create React App is deprecated since 2025).
SSR - Server-Side Rendering
HTML is generated on the server for every request.
textRequest -> server fetches data -> renders HTML -> sends to browsertsx// Next.js App Router - async server component async function Page({ params }: PageProps<'/posts/[id]'>) { const { id } = await params; const post = await db.post.findUnique({ where: { id } }); // no need to call your own API return <Post data={post} />; }- Great SEO, fast first paint.
- Higher server load, slower than static files.
- Best for: dashboards, personalized content.
SSG - Static Site Generation
HTML is generated at build time - same file served to everyone.
textbuild -> generates all HTML files -> deploy to CDN request -> CDN returns pre-built HTML instantly- Fastest possible load (CDN-served static files).
- Content is stale until next build.
- Best for: blogs, docs, marketing pages.
ISR - Incremental Static Regeneration
Static pages that can revalidate in the background after a time interval.
tsx// Next.js 16 (Cache Components) - static, refreshed in the background every hour async function getPosts() { 'use cache'; cacheLife('hours'); return db.post.findMany(); } // Before Cache Components: fetch(url, { next: { revalidate: 3600 } })- Best of SSG and SSR: fast delivery + fresh data.
- Best for: e-commerce product pages, news articles.
Comparison
CSR SSR SSG ISR First load Slow Fast Fastest Fastest SEO Poor Excellent Excellent Excellent Data freshness Always fresh Always fresh Stale Configurable Server load Low High None Low Build time Fast Fast Slow Medium Modern apps often mix strategies: SSG for marketing, SSR for user dashboards, CSR for highly interactive features. Partial Prerendering mixes them even within one page: a static shell from the CDN with dynamic parts streamed in.
updated
did you know it?