Connect every framework answer to browser behavior, accessibility, performance, or user-visible state; that is where senior frontend judgment becomes visible.
Question set
32 detailed answers
01What is the box model, and how does content-box differ from border-box?
junior
Short answer: Every element is a rectangle made of four layers: content, padding, border, and margin. box-sizing determines whether padding and border are included in the specified width/height.
In detail: The layers from inside out: content → padding → border → margin.
content-box(the default):widthsets only the width of the content. The actual occupied width =width + padding + border.border-box:widthincludespaddingandborder. The actual width equals the specifiedwidth. This way of thinking is more intuitive.
.a { box-sizing: content-box; width: 200px; padding: 20px; border: 5px solid; }
/* actual width = 200 + 20*2 + 5*2 = 250px */
.b { box-sizing: border-box; width: 200px; padding: 20px; border: 5px solid; }
/* actual width = 200px (content shrinks to 150px) */
In almost every project people set this globally:
*, *::before, *::after { box-sizing: border-box; }
⚠️ Pitfall: margin is NOT part of border-box — it's always outside and adds to the occupied space. Also, percentages for padding/margin are always computed relative to the parent's width (even vertical ones!) — this is the basis of the padding-top: 56.25% trick for preserving a 16:9 aspect ratio.
02What's the difference between block, inline, inline-block, none, flex, and grid?
junior
Short answer: display controls how an element participates in the flow and what layout context it creates for its children.
In detail:
block— takes up the full available width and starts on a new line. Acceptswidth/heightand verticalmargin/padding. (div,p,section).inline— sits within a line, width sized to its content. Ignoreswidth/heightand verticalmargin; horizontalpadding/marginapply but don't push lines apart correctly. (span,a,em).inline-block— sits within a line (like inline), but acceptswidth/heightand all spacing (like block). Good for "buttons in a row."none— the element is removed from the flow and not rendered (unlikevisibility: hidden, which hides it but keeps its space).flex— a one-dimensional layout context (row OR column) for children.grid— a two-dimensional layout context (rows AND columns at once).
.inline-block-row span { display: inline-block; width: 100px; height: 40px; }
⚠️ Pitfall: inline-block produces "phantom" whitespace between elements because of line breaks in the HTML (whitespace is interpreted as a space character). Fix it with font-size: 0 on the parent, a negative margin, or by switching to flex.
03What's the difference between static, relative, absolute, fixed, and sticky? What is the element positioned relative to?
middle
Short answer: position determines the reference point for top/right/bottom/left and how the element participates in the flow.
In detail:
static(the default) — in normal flow;top/leftoffsets are ignored.relative— stays in the flow (its original space is reserved), but is visually shifted relative to itself. Creates a reference point for absolute children.absolute— removed from the flow, positioned relative to the nearest ancestor withposition != static(or, if there is none, relative to<html>/the viewport).fixed— removed from the flow, positioned relative to the viewport, doesn't move on scroll.sticky— a hybrid: behaves likerelativeuntil the scroll reaches a threshold (top: 0), then "sticks" likefixedwithin the bounds of its parent.
.parent { position: relative; } /* reference point */
.child { position: absolute; top: 0; right: 0; } /* to the top-right corner of the parent */
.header { position: sticky; top: 0; } /* sticks to the top on scroll */
⚠️ Pitfall: position: sticky doesn't work if the parent has overflow: hidden/auto/scroll, if the parent has no height, or if at least one threshold (top/bottom) isn't set. fixed/absolute lose their viewport anchoring if an ancestor has transform, filter, or will-change set — such an ancestor becomes the containing block.
04Explain Flexbox: main/cross axes, justify-content, align-items, flex-grow/shrink/basis.
middle
Short answer: Flexbox is one-dimensional layout. justify-content aligns along the main axis, align-items along the cross axis. flex (grow shrink basis) controls how an element grows/shrinks.
In detail:
The main axis is set by flex-direction (row — horizontal, column — vertical). The cross axis is perpendicular to it.
.container {
display: flex;
flex-direction: row; /* main axis — horizontal */
justify-content: space-between; /* distribution along the main axis */
align-items: center; /* alignment along the cross axis */
gap: 16px; /* gaps */
}
justify-content:flex-start | center | flex-end | space-between | space-around | space-evenly.align-items:stretch (default) | center | flex-start | flex-end | baseline.flex-grow— the growth factor when there's free space (0 = doesn't grow).flex-shrink— the shrink factor when space is tight (1 = can shrink).flex-basis— the base size before distribution (auto= based on content).
.item { flex: 1 1 0; } /* grow shrink basis — all items equal width */
.item { flex: 0 0 200px; } /* fixed 200px, doesn't grow, doesn't shrink */
When to use it: a single row/column of elements — navbars, toolbars, centering, distributing buttons.
⚠️ Pitfall: flex: 1 expands to 1 1 0%, not 1 1 auto — those are different things. With basis: 0 items share space equally; with basis: auto they share it proportionally to content. Also: min-width: auto (the default) prevents a flex item from shrinking below its content — fix it with min-width: 0.
05When should you use Grid instead of Flexbox? Tell me about template and areas.
middle
Short answer: Grid is two-dimensional layout (rows AND columns at once). Use it when you need a grid/page layout; use Flex when the layout is one-dimensional.
In detail:
.grid {
display: grid;
grid-template-columns: 200px 1fr 1fr; /* fr — a fraction of free space */
grid-template-rows: auto 1fr auto;
gap: 16px;
}
Grid areas (areas) give a visual layout:
.layout {
display: grid;
grid-template-columns: 200px 1fr;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }
Useful functions: repeat(3, 1fr), minmax(200px, 1fr), and an auto-responsive grid without media queries:
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
⚠️ Pitfall: don't pit them rigidly against each other — they complement one another: Grid for the overall page layout, Flex for the content inside cells. fr accounts for gap while percentages don't, so 1fr is more reliable than 33% for columns.
06How is specificity calculated? What does !important do, and how does the cascade work?
middle
Short answer: Specificity is a selector's weight expressed as a tuple (inline, id, class, tag). When specificity is equal, the rule declared later wins. !important overrides normal specificity.
In detail: Count by group (a, b, c, d):
- a — inline style (
style="...") = 1000 notional points. - b — the number of id selectors (
#nav) = 100 each. - c — the number of classes, attributes, and pseudo-classes (
.btn,[type],:hover) = 10 each. - d — the number of tags and pseudo-elements (
div,::before) = 1 each.
#nav .item a /* 0,1,1,1 = 111 */
.menu .item a /* 0,0,2,1 = 21 */
ul li a /* 0,0,0,3 = 3 */
/* the first one wins */
* (the universal selector) and combinators (>, +, ~) add no specificity.
The cascade resolves conflicts in this order: importance (!important) → specificity → declaration order.
⚠️ Pitfall: !important is a last resort; !important wars can only be beaten by another !important with higher specificity. An inline style with !important is practically unbeatable (except by !important in a user stylesheet). The modern alternative is @layer (cascade layers) for managing priority without specificity.
07What kinds of selectors and combinators are there? How does a pseudo-class differ from a pseudo-element?
junior
Short answer: Combinators: A B (descendant), A > B (direct child), A + B (next sibling), A ~ B (all following siblings). A pseudo-class is a state (:hover); a pseudo-element is a virtual part (::before).
In detail:
.list li { } /* descendant (any nesting level) */
.list > li { } /* direct children only */
h2 + p { } /* the first <p> immediately after an <h2> */
h2 ~ p { } /* all <p> after an <h2> at the same level */
Pseudo-classes (single colon) — states and position:
a:hover { }
input:focus { }
li:nth-child(2n) { } /* even ones */
li:first-child { }
li:not(.active) { }
Pseudo-elements (double colon) — generated/partial content:
.quote::before { content: "«"; }
p::first-line { font-weight: bold; }
input::placeholder { color: gray; }
⚠️ Pitfall: :nth-child(n) counts among ALL of the parent's children, whereas :nth-of-type(n) counts only among elements of the same tag. ::before/::after don't work without the content property (even content: "") and don't appear on replaced elements (img, input).
08Which properties are inherited and which are not?
junior
Short answer: Mostly text-related properties are inherited (color, font-*, line-height, text-align, visibility). NOT inherited: margin, padding, border, width, background, display, position.
In detail: You can control inheritance explicitly:
.child { color: inherit; } /* take it from the parent */
.child { all: unset; } /* reset everything */
button { font: inherit; } /* buttons don't inherit the font — a common fix */
The values: inherit (inherit), initial (the spec's default value), unset (inherit if the property is inherited, otherwise initial), revert (roll back to the browser's style).
⚠️ Pitfall: form elements (button, input, select, textarea) do NOT inherit font-family/font-size from the parent by default — which is why people often write button { font: inherit; }. The computed value is inherited; for example, line-height: 1.5 is inherited as a multiplier, while line-height: 24px is inherited as a fixed value.
09What's the difference between px, em, rem, %, vw, vh, and fr? What's the difference between em and rem?
junior
Short answer: px — absolute pixels; em — relative to the font-size of the current element; rem — relative to the font-size of the root (<html>); % — relative to the parent; vw/vh — % of the viewport's width/height; fr — a fraction of the free space in a grid.
In detail:
html { font-size: 16px; }
.box { font-size: 2rem; } /* 32px — from the root */
.box { padding: 1em; } /* 32px — from .box's own font-size (=32) */
.full { width: 100vw; height: 100vh; } /* the whole screen */
emcascades/multiplies: nested elements withfont-size: 1.5emmultiply sizes down the chain → it's easy to get unpredictable results.remis always tied to a single source (the root) → predictable, which is why it's preferred for typography and spacing.emis handy when you need to scale something relative to its own font (for example, a button'spaddinginemscales together with its text).
⚠️ Pitfall: 100vw includes the width of the vertical scrollbar → horizontal scroll on desktop. The new units dvh/svh/lvh (dynamic/small/large viewport height) solve the "jumping" address-bar problem on mobile, where 100vh is inaccurate.
10What is a stacking context, and why does z-index "not work"?
senior
Short answer: z-index only takes effect within a single stacking context. An element with a high z-index inside a context with a low z-index will still end up below a neighboring context.
In detail: A stacking context is created, in particular, by:
- the root
<html>; - an element with
position != staticAND a setz-index(notauto); opacity < 1,transform,filter,will-change,mix-blend-mode,isolation: isolate;- a flex/grid child with
z-index != auto.
.modal-parent { opacity: 0.99; } /* creates a new context! */
.modal { position: fixed; z-index: 9999; }
/* the modal with z-index 9999 will end up UNDER a neighboring block,
because it's trapped inside the .modal-parent context */
Within a context the order is: parent's background → negative z-index → block flow → float → inline → z-index:auto/0 → positive z-index.
⚠️ Pitfall: the classic pain — z-index: 999999 doesn't help because a parent with transform or opacity created an isolated context. The fix: move the element higher up the tree (a portal) or remove the property that creates the context. Use isolation: isolate to deliberately create a context and prevent z-index from "leaking."
11What is margin collapse?
middle
Short answer: The vertical margins of adjacent blocks, and of a parent and its first child, collapse into one — the larger value is used, not the sum.
In detail: Three cases:
- Adjacent blocks:
margin-bottom: 20px+margin-top: 30px= a30pxgap (not 50). - A parent and its first/last child: the child's
margin-top"leaks" outside the parent. - An empty block: its own top and bottom collapse together.
.a { margin-bottom: 20px; }
.b { margin-top: 30px; }
/* the actual gap between .a and .b = 30px */
How to prevent a parent from collapsing with its child: give the parent padding, a border, overflow: hidden, or make it a flex/grid container (there's no collapse in those).
⚠️ Pitfall: only vertical margins collapse in normal flow. Horizontal ones never do. Flex/grid containers have no margin collapse at all — a common reason for "why did the spacing suddenly change" when switching to flex.
12What is float, and why do you need a clearfix?
middle
Short answer: float was historically used for wrapping text around images and for column layouts. A clearfix makes the parent "see" the height of its floated children, which are removed from the flow.
In detail:
img { float: left; margin-right: 1em; } /* text wraps around the image */
The problem: floated elements don't contribute height to the parent → the parent "collapses" to zero height. The fix is a clearfix:
.clearfix::after {
content: "";
display: block;
clear: both;
}
The modern replacement for both layout cases is Flexbox/Grid. float remains only for text wrapping.
⚠️ Pitfall: clear: both clears the float on both the left AND the right. Today float isn't used for layouts — but in interviews clearfix is asked about as "legacy you need to understand." An alternative to a clearfix without a pseudo-element is overflow: auto/display: flow-root on the parent.
13How do you do responsiveness? What is mobile-first, and how does responsive differ from adaptive?
middle
Short answer: Media queries change styles based on screen width. Mobile-first — write the base styles for mobile and extend up with min-width. Responsive — a fluid, stretchy layout; adaptive — several fixed layouts for specific breakpoints.
In detail:
/* mobile-first: the base is mobile, extend upward */
.col { width: 100%; }
@media (min-width: 768px) { .col { width: 50%; } } /* tablet+ */
@media (min-width: 1024px) { .col { width: 33.33%; } } /* desktop+ */
- Responsive — a single layout that stretches smoothly (
%,fr,clamp(),minmax). - Adaptive — several pre-built variants switched at breakpoints.
Useful: clamp(min, preferred, max) for "fluid" typography without media queries:
h1 { font-size: clamp(1.5rem, 4vw, 3rem); }
Don't forget the viewport meta tag:
<meta name="viewport" content="width=device-width, initial-scale=1">
⚠️ Pitfall: mobile-first uses min-width (styles are added going up), desktop-first uses max-width. Mixing them is risky: overlaps produce unexpected results. Container queries (@container) are a new approach: the style depends on the container's width, not the viewport's.
14What are CSS variables (custom properties)?
junior
Short answer: They're custom properties --name, read via var(). Unlike preprocessor variables, they live at runtime, cascade, and are inherited.
In detail:
:root {
--primary: #3b82f6;
--gap: 16px;
}
.btn { background: var(--primary); padding: var(--gap); }
.btn-danger { --primary: #ef4444; } /* override within a scope */
You can change them from JS — great for theming:
document.documentElement.style.setProperty('--primary', '#10b981');
var() supports a fallback: var(--x, 8px).
⚠️ Pitfall: unlike Sass variables (which are compiled away and disappear), CSS variables are dynamic and cascade — a plus for theming, but var() can't be used in media-query conditions (@media (min-width: var(--bp)) doesn't work). Names are case-sensitive: --Primary ≠ --primary.
15transition vs animation/keyframes? Which properties are cheap to animate?
middle
Short answer: transition animates a change between two states (it requires a trigger, e.g. :hover). animation + @keyframes is a self-running, multi-step animation with loops. It's cheap to animate transform and opacity; expensive to animate properties that trigger layout (width, top, margin).
In detail:
/* transition: A -> B on an event */
.btn { transition: transform 0.2s ease; }
.btn:hover { transform: scale(1.1); }
/* animation: self-running, can loop */
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.dot { animation: pulse 1s infinite; }
Why transform/opacity are cheap: the compositor can animate them on the GPU — without recomputing layout and without repainting layers. width, height, top, left, margin cause a reflow on every frame → the whole screen janks.
⚠️ Pitfall: animating box-shadow, filter, or background causes a repaint every frame — expensive. It's better to move an element via transform: translateX() rather than left. will-change: transform hints to the browser to create a layer ahead of time, but overusing it eats memory.
16How do you center an element? Name a few ways.
junior
Short answer: Flexbox (justify-content + align-items), Grid (place-items: center), position + transform, margin: auto.
In detail:
/* 1. Flexbox — the most common */
.parent { display: flex; justify-content: center; align-items: center; }
/* 2. Grid — the shortest */
.parent { display: grid; place-items: center; }
/* 3. absolute + transform — without knowing the sizes */
.child {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
}
/* 4. margin: auto in flex */
.child { margin: auto; }
/* 5. horizontal centering of a block with a known width */
.child { width: 300px; margin: 0 auto; }
⚠️ Pitfall: margin: 0 auto centers only horizontally and only block elements with a set width; you can't center vertically this way (in normal flow). transform: translate(-50%, -50%) can produce blurry text on fractional pixels — sometimes people add will-change or round the values.
17Briefly: BEM, CSS-in-JS, CSS Modules, Tailwind — what are they about?
middle
Short answer: These are different approaches to organizing styles and fighting the global nature of CSS.
In detail:
- BEM (Block-Element-Modifier) — a class naming convention:
.card__title--active. Solves name conflicts through discipline, without tooling. Flat specificity (everything is classes). - CSS Modules —
import styles from './x.module.css', names are hashed into unique ones (Card_title_a3f9) → local scope, no collisions. Build-time. - CSS-in-JS (styled-components, Emotion) — styles in JS, dynamic based on props. Flexible, but with runtime overhead (some are migrating to zero-runtime).
- Tailwind — utility-first: atomic classes (
flex p-4 text-lg) right in the markup. No naming, small final CSS (unused styles purged), but "noisy" markup.
<!-- BEM --> <div class="card card--featured"><h2 class="card__title">…</h2></div>
<!-- Tailwind --> <div class="rounded-lg p-4 shadow"><h2 class="text-xl font-bold">…</h2></div>
⚠️ Pitfall: they all share one goal — isolating and making styles predictable in a large project. There's no single "right" choice — it depends on the team/stack. CSS-in-JS adds a runtime cost; Tailwind takes getting used to and works best with components to avoid duplicating classes.
18Describe how the browser renders a page.
senior
Short answer: HTML → DOM, CSS → CSSOM, combining them → Render Tree, then Layout (reflow) → Paint → Composite.
In detail:
- Parsing HTML → DOM — the tokenizer builds a tree of nodes. When it hits a
<script>withoutasync/defer, the parser stops. - Parsing CSS → CSSOM — the style tree. CSS is render-blocking: rendering waits for the CSSOM to be built.
- Render Tree — the merge of DOM and CSSOM, only visible nodes (no
display: none, butvisibility: hiddenstays). - Layout (reflow) — computing geometry: the coordinates and sizes of every node. Depends on the viewport.
- Paint — filling pixels layer by layer (text, colors, shadows, borders) into a bitmap.
- Composite — assembling the layers into the final image, often on the GPU (accounting for
transform,opacity, z-order).
HTML ─► DOM ─┐
├─► Render Tree ─► Layout ─► Paint ─► Composite ─► screen
CSS ─► CSSOM┘
⚠️ Pitfall: display: none excludes a node from the render tree (no layout/paint), whereas visibility: hidden/opacity: 0 keep it in layout (its space is occupied). JS can read styles in the middle of this pipeline and force a synchronous reflow.
19Reflow vs repaint vs composite — which is more expensive, and what triggers each?
senior
Short answer: Reflow (layout) — recomputing geometry, the most expensive. Repaint — redrawing pixels without changing geometry, cheaper. Composite — reassembling layers on the GPU, the cheapest.
In detail:
| Stage | What changes | Triggers | Cost |
|---|---|---|---|
| Reflow/Layout | sizes/positions | width, height, top/left, margin, font-size, adding DOM nodes, reading offsetWidth |
🔴 high |
| Repaint | colors/visuals | color, background, box-shadow, visibility |
🟡 medium |
| Composite | layer only | transform, opacity (on a separate layer) |
🟢 low |
Reflow drags repaint and composite along with it (a cascade). That's why reflow is exactly what you minimize.
How to minimize it:
- Animate
transform/opacity, nottop/width. - Batch DOM changes, avoid layout thrashing (interleaving writes and reads of geometry):
// BAD: forced synchronous layout in a loop
for (const el of items) {
el.style.height = el.offsetHeight + 10 + 'px'; // read → write → read…
}
// GOOD: all reads first, then all writes
const hs = items.map(el => el.offsetHeight);
items.forEach((el, i) => el.style.height = hs[i] + 10 + 'px');
⚠️ Pitfall: reading offsetTop/offsetWidth/getBoundingClientRect()/getComputedStyle() right after changing a style forces a synchronous reflow — the browser is forced to recompute layout immediately. This is "layout thrashing" and the main source of jank in scripts with animation.
20What is the critical rendering path and render-blocking resources? Why is CSS in the head while JS goes at the bottom/async/defer?
senior
Short answer: The Critical Rendering Path is the sequence of steps from receiving the HTML to the first pixel. CSS blocks rendering, and synchronous JS blocks both parsing and rendering. That's why CSS is loaded early (in the <head>) and scripts are loaded non-blockingly.
In detail:
- CSS is render-blocking: the browser won't paint content until it has built the CSSOM (otherwise there'd be a FOUC — a flash of unstyled content). So CSS needs to come as early as possible and be minimal (see Critical CSS).
- A synchronous
<script>is parser-blocking: on encountering it, the parser stops building the DOM, downloads and executes the script (and the script in turn waits for the CSSOM if it reads styles). That's why scripts were historically placed before</body>.
<head>
<link rel="stylesheet" href="styles.css"> <!-- load styles early -->
</head>
<body>
…content…
<script src="app.js" defer></script> <!-- doesn't block parsing -->
</body>
The goal is to shorten the path: fewer critical resources, smaller in size, fewer back-and-forth round trips over the network.
⚠️ Pitfall: CSS blocks not only paint but also the execution of JS that comes after it (the browser is afraid the script will read styles that aren't ready yet). So a huge CSS file also slows down JS. An @import inside CSS adds an extra sequential request — a <link> is better.
21What's the difference between async and defer on a `<script>`?
middle
Short answer: Both load the script in parallel with HTML parsing (they don't block it during loading). defer executes scripts after the DOM is parsed and in declaration order; async executes as soon as it's loaded, in arbitrary order, interrupting parsing.
In detail:
<script src="a.js"></script> <!-- blocks parsing: download + execute -->
<script src="a.js" async></script> <!-- download ∥ parse, execute ASAP -->
<script src="a.js" defer></script> <!-- download ∥ parse, execute after the DOMContentLoaded stage, in order -->
| When it loads | When it executes | Order | |
|---|---|---|---|
| normal | blocks parsing | immediately | in order |
async |
in parallel | as soon as downloaded (may interrupt parsing) | NOT guaranteed |
defer |
in parallel | after the DOM is fully parsed | in order |
defer— for scripts that need a ready DOM and where order matters (most app code).async— for independent scripts (analytics, ads) that don't care when or in what order they run.
⚠️ Pitfall: async/defer are ignored on inline scripts (without src). Scripts of type module behave like defer by default. An async script may execute before the DOM is ready — you can't rely on elements being present.
22What are Core Web Vitals (LCP, INP/FID, CLS) and the TTFB and FCP metrics?
middle
Short answer: Core Web Vitals are Google's three key metrics: LCP (loading speed), INP/FID (responsiveness), CLS (visual stability). TTFB and FCP are supporting timing metrics.
In detail:
- TTFB (Time To First Byte) — the time until the first byte of the server's response. Affects everything that follows.
- FCP (First Contentful Paint) — the first rendered content (text/image).
- LCP (Largest Contentful Paint) — the render of the largest visible element (the main hero/image). Target: < 2.5 s.
- FID (First Input Delay) — the delay in responding to the first input. Replaced by INP (Interaction to Next Paint) — responsiveness throughout the whole session. INP target: < 200 ms.
- CLS (Cumulative Layout Shift) — the cumulative layout shift (when content "jumps"). Target: < 0.1.
How to improve them:
- LCP — preload the hero image/font, a fast server/CDN, optimizing critical CSS.
- INP — break up long JS tasks,
requestIdleCallback, web workers. - CLS — set
width/heightonimg, reserve space for ads/banners,font-display: optional.
<img src="hero.jpg" width="1200" height="600" alt="…"> <!-- reserve space against CLS -->
<link rel="preload" as="image" href="hero.jpg"> <!-- speeds up LCP -->
⚠️ Pitfall: CLS is most often broken by images without width/height, dynamically inserted banners, and fonts that change metrics when they load. FID measures only the first interaction — INP is more honest because it accounts for the entire session.
23What are the ways to optimize page load?
middle
Short answer: Reduce size (minification, compression, code splitting), load lazily (lazy loading, defer), cache (HTTP cache, CDN), preload what's critical (preload/prefetch).
In detail:
- Minification — stripping whitespace/comments from JS/CSS/HTML.
- Compression at the transport layer —
gzip/brotli(br is more efficient). - Lazy loading of images and iframes:
<img loading="lazy">; for components — dynamic import. - Code splitting — break the bundle up by routes/features, load on demand (
import()), tree-shaking removes dead code. - Caching —
Cache-Control, hashed file names (app.a3f9.js) for long-lived caching + instant invalidation. - CDN — serving static assets from the node closest to the user (less latency).
- Resource hints:
preload(needed now, high priority),prefetch(needed later),preconnect/dns-prefetch(establish the connection ahead of time).
<link rel="preload" as="font" href="font.woff2" crossorigin> <!-- critical font -->
<link rel="prefetch" href="/next-page.js"> <!-- next route -->
<link rel="preconnect" href="https://api.example.com"> <!-- open the connection ahead of time -->
<script type="module" src="main.js"></script> <!-- modern bundle -->
Bundle dependency tree — an analyzer (webpack-bundle-analyzer) shows what bloated the build → we remove heavy/duplicate libraries and switch to lightweight alternatives.
⚠️ Gotcha: don't overuse preload — too many high-priority requests compete for bandwidth and slow down the critical ones. You can't put lazy on the LCP image (hero) — it will slow down LCP. Tree-shaking doesn't work on CommonJS modules or when there are side effects.
24What is Critical CSS and the FOUT/FOIT font problems?
senior
Short answer: Critical CSS is the minimal set of styles for above-the-fold content, inlined into <head> for a fast first render. FOUT (Flash of Unstyled Text) — text in a system font until the custom one loads; FOIT (Flash of Invisible Text) — invisible text while the font is loading.
In detail:
Critical CSS: we extract the styles for the visible part, inline them, and load the rest of the CSS asynchronously — so the first paint doesn't wait for a large stylesheet:
<head>
<style>/* critical: hero, header, fonts */</style>
<link rel="preload" href="full.css" as="style" onload="this.rel='stylesheet'">
</head>
Fonts are controlled by font-display:
swap— show the fallback immediately, then swap it in (FOUT) — text is always visible.block— a short period of invisibility, waiting for the font (FOIT).optional— if the font isn't ready in time, use the system one and don't disturb the layout (best for CLS).
@font-face {
font-family: 'Inter';
src: url('inter.woff2') format('woff2');
font-display: swap;
}
⚠️ Gotcha: FOUT causes layout shift (CLS) if the custom and fallback fonts have different metrics — fix it with size-adjust/ascent-override or font-display: optional. Use woff2 (best compression), load only the weights you need, and preload critical fonts.
25What is the DOM? Why are operations on it expensive, and what is DocumentFragment?
middle
Short answer: The DOM (Document Object Model) is a tree-shaped object representation of the HTML through which JS reads and changes the page. DOM changes are expensive because they can trigger reflow/repaint. DocumentFragment is a lightweight offscreen container for batch insertion.
In detail: Every DOM change can force the browser to recalculate layout and repaint. Inserting nodes one at a time in a loop = lots of reflows.
// BAD: 1000 insertions into the live DOM → potentially 1000 reflows
for (let i = 0; i < 1000; i++) {
list.appendChild(document.createElement('li'));
}
// GOOD: build up in a fragment, insert all at once → 1 reflow
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
frag.appendChild(document.createElement('li'));
}
list.appendChild(frag);
Expensive operations: reading geometry (offsetWidth, getBoundingClientRect), frequent insertions/removals, changes that affect dimensions.
⚠️ Gotcha: innerHTML += in a loop is an anti-pattern: each time the entire HTML is re-parsed and rebuilt, and event handlers are lost. It's better to build a string and assign it once, or use a fragment. The virtual DOM (React) exists precisely to minimize real DOM operations through diffing and batching.
26Explain event delegation, bubbling/capturing, preventDefault/stopPropagation.
middle
Short answer: Events go through three phases: capture (top down), target, bubbling (bottom up). Delegation — a single handler on the parent catches events from bubbling children. preventDefault() cancels the browser's default action; stopPropagation() stops further propagation.
In detail:
// Delegation: a single listener instead of hundreds
document.querySelector('#list').addEventListener('click', (e) => {
const li = e.target.closest('li');
if (li) handle(li.dataset.id);
});
- Capturing — the event travels from
documentdown to the target (addEventListener(type, fn, true)). - Target — it reaches the target element.
- Bubbling — it bubbles back up (by default listeners fire here).
e.preventDefault(); // cancel the default (following a link, submitting a form)
e.stopPropagation(); // don't let the event travel further up the tree
e.stopImmediatePropagation(); // + don't call other listeners on this same element
⚠️ Gotcha: preventDefault() and stopPropagation() are different things: the former doesn't stop bubbling, the latter doesn't cancel the default action. Not all events bubble (focus, blur, mouseenter don't bubble; there are bubbling counterparts focusin, mouseover). Delegation won't work for non-bubbling events.
27What's the difference between localStorage, sessionStorage, and cookies?
junior
Short answer: They all store data on the client. localStorage — persistently, sessionStorage — for the lifetime of the tab, cookies — are sent to the server with every request and have an expiration.
In detail:
| localStorage | sessionStorage | cookies | |
|---|---|---|---|
| Lifetime | until deleted | until the tab is closed | per expires/max-age |
| Size | ~5–10 MB | ~5 MB | ~4 KB |
| Sent to server | no | no | yes, with every request |
| Scope | origin, shared across tabs | the current tab only | origin + path |
| API | synchronous JS | synchronous JS | document.cookie / headers |
localStorage.setItem('theme', 'dark'); // survives a reload
sessionStorage.setItem('step', '2'); // this tab/session only
document.cookie = 'token=abc; Secure; SameSite=Strict; HttpOnly';
When to use what:
- localStorage — UI settings, theme, caching non-critical data.
- sessionStorage — data for a single session/wizard, not needed after closing.
- cookies — authentication/sessions, anything the server needs (especially
HttpOnlyfor tokens — inaccessible to JS, protection against XSS).
⚠️ Gotcha: localStorage/sessionStorage are synchronous and block the thread — don't dump megabytes into them. They only store strings (objects — via JSON.stringify). Tokens in localStorage are vulnerable to XSS; for sessions, HttpOnly+Secure cookies are safer. Cookies are sent with every request → they bloat traffic.
28What's important to know about accessibility (a11y)?
junior
Short answer: Semantic HTML, text alternatives (alt), ARIA attributes where semantics fall short, and sufficient color contrast.
In detail:
- Semantics:
<button>,<nav>,<header>,<main>,<h1>…<h6>give structure to screen readers and provide keyboard navigation for free. Don't make a button out of a<div onclick>. alton images: a description for blind users; an emptyalt=""for decorative ones.- ARIA:
aria-label,role,aria-expanded,aria-live— when native semantics aren't enough (custom widgets). - Contrast: text to background ≥ 4.5:1 (WCAG AA), large text — ≥ 3:1.
- Keyboard: everything clickable must be reachable with Tab/Enter; a visible
:focus.
<button aria-label="Close" aria-expanded="false">✕</button>
<img src="logo.png" alt="Company logo">
<img src="divider.png" alt=""> <!-- decorative -->
⚠️ Gotcha: "the first rule of ARIA is don't use ARIA if there's a native element." aria-label on a non-interactive element is useless. Removing outline on :focus without a replacement is a common accessibility mistake; use :focus-visible. A placeholder is not a substitute for a <label>.
29What happens when you type a URL and hit Enter? (with a focus on rendering)
concept
Short answer: DNS resolution → TCP/TLS connection → HTTP request → server response → the browser parses HTML into the DOM, CSS into the CSSOM, builds the render tree, does layout, paint, composite, and outputs pixels.
In detail:
- DNS — domain name → IP address (with caching at various levels).
- TCP + TLS — establishing the connection and encryption (the handshake for HTTPS).
- HTTP request → the server returns HTML (TTFB — time to first byte).
- Parsing HTML → DOM, while the browser's "preload scanner" finds and preloads resources.
- CSS → CSSOM (render-blocking), JS executes (parser-blocking if synchronous).
- Render Tree = DOM + CSSOM (visible nodes).
- Layout (reflow) — geometry; Paint — pixels by layer; Composite — assembly (often on the GPU).
- FCP/LCP — the user sees content; then hydration/interactivity.
URL → DNS → TCP/TLS → HTTP → HTML
├─► DOM ─┐
│ ├─► Render Tree → Layout → Paint → Composite → pixels
└─► CSS ─► CSSOM┘
⚠️ Gotcha: in a frontend interview the emphasis is precisely on the part "after the HTML is received" (rendering), not on the networking. Mention render-blocking CSS, parser-blocking JS, the critical rendering path, and why the metrics matter (LCP/FCP).
30Why are DOM manipulations "expensive"?
concept
Short answer: Changing the DOM can trigger reflow (recalculating the geometry of the whole page) and repaint, and reading geometry right after a write forces a synchronous layout. These recalculations are the most expensive part of a frame.
In detail: The DOM itself is just a tree of objects, and accessing it isn't slow. What's expensive is the side effect: the browser has to recompute layout and repaint. The larger the tree and the more often reads/writes alternate, the worse it gets.
// layout thrashing: write → read → write → read… each step = reflow
box.style.width = '100px';
console.log(box.offsetHeight); // forces a reflow
box.style.height = '50px';
console.log(box.offsetTop); // another reflow
Ways to make it cheaper: batch changes, use DocumentFragment, change a class instead of many inline styles, read geometry in a batch before writes, animate transform/opacity, use requestAnimationFrame to sync with the frame.
⚠️ Gotcha: this is exactly why virtual DOMs (React) and batching appeared — they collect changes and apply a minimal set of real operations in a single pass, avoiding extra reflows.
31Why is animating transform cheaper than top/left?
concept
Short answer: transform/opacity are handled at the composite stage (often on the GPU) without recalculating layout or repainting. top/left/width/margin change geometry → they trigger a reflow + repaint on every frame.
In detail: An animation runs 60 times per second. If every frame requires a full reflow of the whole page, the browser can't keep up with 16.6 ms and frames get "dropped" (jank).
/* EXPENSIVE: every frame = layout + paint + composite */
.bad { transition: left 0.3s; }
.bad:hover { left: 100px; }
/* CHEAP: composite only, on the GPU */
.good { transition: transform 0.3s; }
.good:hover { transform: translateX(100px); }
The browser can promote an element animated via transform/opacity onto a separate compositor layer and move it independently of the rest of the layout. will-change: transform hints that the layer should be created ahead of time.
⚠️ Gotcha: the cheapness only holds while the element is on its own layer; too many layers or will-change on everything bloats GPU memory. transform affects the visual but not the space occupied in flow — other elements won't shift.
32Why do you need event delegation?
concept
Short answer: So that instead of hundreds of handlers on each child element, you attach a single one on a common parent, using bubbling. This saves memory and automatically works for dynamically added elements.
In detail:
// Without delegation: N listeners, and new elements won't get them
document.querySelectorAll('.item').forEach(el =>
el.addEventListener('click', handler)
);
// With delegation: 1 listener, works for future .item too
document.querySelector('#list').addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) handler(item);
});
Upsides:
- Fewer listeners → less memory and faster attachment.
- Dynamically added elements are handled without re-attaching.
- Easier to manage (a single source of truth).
⚠️ Gotcha: it only works for bubbling events; for focus/blur/mouseenter you need the non-bubbling exceptions (use focusin/mouseover). Always check e.target.closest(...), otherwise a click on nested elements inside .item may miss. You have to account for stopPropagation() inside children — it will break delegation.
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.