Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

20 chapters
⌕
Beginner15›
00Scouting reportR1Screening callR2Machine codingR3JavaScript & TSR3·TSTypeScriptR4React & Next.jsR5Node & NestJSR6Databases & RedisR7DSA roundR8System designR9AWS, Docker, CI/CDR10Resume grillingR11BehaviouralR12HR & the number✓The week before
Advanced5›
50LWhat changes at ₹50L50LHard DSA50LDistributed systems50LRuntime internals50LStaff behavioural
/ search[ ] chaptert top

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
50L

Runtime internals

LengthFolded into the deep dive
WhoStaff engineer
BarBelow the framework
Fail modeKnowing the API, not the machine
saasproduct

At ₹20–28L nobody asks how V8 stores an object. At the top of the band they do, because at that level you are expected to debug things the framework cannot explain. This round is finite and learnable — perhaps two weekends of reading — and it is disproportionately impressive because most candidates have never looked.

S3.1
How does V8 execute your JavaScript?
What they are really testingWhether you know why some JavaScript is 100x slower than other JavaScript that looks identical.

Source → parser → AST → Ignition, the bytecode interpreter, which starts executing immediately. While running, V8 collects type feedback. Functions that run hot get sent to TurboFan, the optimising compiler, which produces machine code specialised to the types it has observed. If a later call violates those assumptions, the code is deoptimised and falls back to bytecode.

Hidden classes (V8 calls them Maps) are the mechanism underneath. Objects with the same properties added in the same order share a hidden class, which lets property access compile to a fixed memory offset instead of a hash lookup. Inline caches then remember "at this call site, the object had hidden class X, so the property is at offset 4" — and that is what makes property access fast.

why these two are not the same speed
// SAME hidden class — monomorphic call site, fast
function P(x, y) { this.x = x; this.y = y }
const a = new P(1, 2), b = new P(3, 4)

// DIFFERENT hidden classes — property order differs
const c = { x: 1 }; c.y = 2
const d = { y: 2 }; d.x = 1
// a function reading .x from both goes polymorphic, then megamorphic,
// and the inline cache stops helping entirely

// also deoptimising: adding a property after construction,
// deleting a property (delete o.x), and mixing types in an array
const arr = [1, 2, 3]      // PACKED_SMI — fastest element kind
arr.push(1.5)              // → PACKED_DOUBLE
arr.push('x')              // → PACKED_ELEMENTS, boxed, slow
arr[100] = 1               // → HOLEY, slower still, and it never goes back

The practical rules that follow, which is what they actually want: initialise all properties in the constructor and in the same order; never delete a property (set it to null or undefined); keep arrays type-homogeneous and hole-free; and prefer monomorphic functions — one that receives four different object shapes is far slower than four specialised ones.

They will push with
  • What is a megamorphic call site?
  • Why is delete so expensive?
  • How would you actually verify any of this? (--trace-deopt, --allow-natives-syntax.)
S3.2
Garbage collection, and how you find a leak
What they are really testingDirectly relevant to a long-running Node service, and a real staff-level debugging skill.

V8's heap is generational, on the observation that most objects die young.

  • Young generation (nursery). Collected by Scavenger, a copying collector: live objects are copied to the other semi-space and everything else is discarded wholesale. Frequent, very fast, pauses of well under a millisecond. An object that survives two scavenges is promoted.
  • Old generation. Collected by mark-sweep-compact: mark what is reachable from the roots, sweep the rest, compact to remove fragmentation. Much less frequent, much more expensive. Modern V8 does most of the marking concurrently and incrementally to keep pauses short, but a major GC on a large heap is still measured in tens of milliseconds.

The key insight: allocating a lot of short-lived objects is cheap — that is what the nursery is for. Allocating objects that survive is expensive, because they get promoted and then cost you major GCs. So the performance problem is rarely "too many allocations"; it is "too many long-lived allocations".

the leak-hunting workflow, which is the real answer
// 1. confirm it is a leak, not just a large heap
node --expose-gc app.js
process.memoryUsage()   // heapUsed climbing across forced GCs = a real leak

// 2. three heap snapshots: baseline, after load, after more load
//    Chrome DevTools → Memory → Comparison view
//    look at "Delta" — what keeps growing between snapshots?

// 3. select the growing constructor → Retainers panel
//    the retainer chain names the thing holding it alive.
//    In Node it is almost always one of four things:
//      - an event listener never removed
//      - a Map or array used as a cache with no eviction
//      - a closure capturing a request context
//      - a timer that was never cleared

The fixes map one to one: remove listeners on cleanup or use once; give every in-memory cache a size bound and a TTL, or use a WeakMap keyed by an object whose lifetime you do not control; clear timers; and in production, expose heap metrics so you see the sawtooth flatten into a ramp before it becomes an out-of-memory crash.

They will push with
  • What is the difference between a memory leak and high memory usage?
  • Why is a WeakMap the right cache key sometimes?
  • What does --max-old-space-size actually change?
S3.3
The browser rendering pipeline, and what triggers each stage
What they are really testingThe frontend half of internals. It is what separates "I used a CSS transition" from "I know why that one janks".

Style → Layout → Paint → Composite. The cost of a change depends entirely on how far up that chain it starts.

ChangeTriggersCost
width, height, top, margin, font-sizeLayout → Paint → CompositeMost expensive — geometry of other elements may change too
color, background, box-shadow, border-radiusPaint → CompositeModerate — repaints the affected area
transform, opacityComposite onlyCheapest — handled on the compositor, often off the main thread entirely

Therefore: animate transform and opacity, never left/top/width. That one rule is the entire practical takeaway, and being able to explain why in terms of the pipeline is what gets scored.

layout thrashing — the classic main-thread killer
// BAD: read, write, read, write — forces a synchronous layout every iteration
for (const el of els) {
  el.style.height = el.offsetHeight + 10 + 'px'   // read forces flush of pending writes
}

// GOOD: batch all reads, then all writes
const heights = els.map(el => el.offsetHeight)    // read phase
els.forEach((el, i) => el.style.height = heights[i] + 10 + 'px')  // write phase

The properties that force a synchronous layout when read — offsetHeight, getBoundingClientRect, scrollTop, getComputedStyle — are worth memorising, because reading one after a write is what causes the thrash.

will-change promotes an element to its own compositor layer, which makes it cheap to animate — but each layer costs GPU memory, and applying it to everything makes things slower, not faster. Add it just before the animation and remove it after.

And connect it to INP from R4: a long task blocks the main thread, so the next paint after an interaction is delayed. That is why breaking up long tasks (with scheduler.yield() or setTimeout) improves a metric that looks like it should be about rendering.

They will push with
  • What is the compositor thread and what can it do without the main thread?
  • Why is a CSS animation often smoother than a JS one?
  • How would you find a long task in production?
S3.4
Node internals — libuv, event loop lag, and where the threads are
What they are really testingDeeper than R5.1. This is the version asked when they suspect you actually know.

Node has four thread pools you should be able to distinguish: the single main thread running your JavaScript; the libuv thread pool (default 4) used by file I/O, DNS via getaddrinfo, zlib and crypto; the V8 threads for concurrent GC and TurboFan compilation; and any worker_threads you create.

Network I/O does not use the thread pool — it uses the operating system's event notification (epoll on Linux, kqueue on BSD, IOCP on Windows), which is why Node handles tens of thousands of sockets on one thread but stalls on four concurrent bcrypt calls.

measuring event loop lag — the metric that finds the problem
// the crude version, good enough to alert on
let last = process.hrtime.bigint()
setInterval(() => {
  const now = process.hrtime.bigint()
  const lag = Number(now - last) / 1e6 - 100     // expected 100ms interval
  last = now
  metrics.gauge('eventloop.lag_ms', lag)          // >50ms sustained = trouble
}, 100)

// the proper version, built in:
const { monitorEventLoopDelay } = require('node:perf_hooks')
const h = monitorEventLoopDelay({ resolution: 10 })
h.enable()
// h.mean, h.percentile(99) — alert on the p99, not the mean

This is the single most useful Node production metric and almost nobody instruments it. High event loop lag with low CPU means you are blocked on something synchronous; high lag with high CPU means genuine CPU work that belongs on a worker thread or a queue. Being able to say that diagnostic split is a staff-level answer.

Also worth knowing: process.nextTick has its own queue that drains before promise microtasks and can starve the loop if it recurses; and setImmediate versus setTimeout(fn, 0) is non-deterministic at the top level but deterministic inside an I/O callback, where setImmediate always fires first because the check phase follows poll.

They will push with
  • You see 300ms event loop lag in production. Walk me through the diagnosis.
  • When would you reach for worker_threads over a queue?
  • What does clustering actually give you and what does it not?
S3.5
The network layer — HTTP/2, HTTP/3, and connection cost
What they are really testingPerformance work at this level goes below the framework. Directly relevant to your page-load claims.
  • HTTP/1.1 — one request at a time per connection, so browsers open about six connections per origin. Head-of-line blocking at the application layer, which is why bundling and sprite sheets existed.
  • HTTP/2 — multiplexed streams over one connection, header compression (HPACK), server push (now largely deprecated). This is why aggressive bundling became counterproductive: many small cacheable files are now often better than one big one.
  • HTTP/3 / QUIC — runs over UDP. Solves the remaining problem: HTTP/2 still suffered TCP-level head-of-line blocking, where one lost packet stalls every multiplexed stream. QUIC makes streams independent, and adds 0-RTT connection resumption.

The costs worth quoting: a TCP handshake is one round trip; TLS 1.3 adds one more (TLS 1.2 added two); so a fresh HTTPS connection to a distant origin costs roughly 2 round trips before a single byte of your content moves. At 150ms cross-continent that is 300ms of nothing.

Which explains the fixes: preconnect for origins you will definitely use, keep-alive and connection reuse, and reducing the number of distinct origins — every third-party domain is another handshake.

Resource hints, in order of aggressiveness: dns-prefetch (resolve only) → preconnect (resolve, connect, TLS) → preload (fetch this now, high priority, I need it this navigation) → prefetch (fetch idly, I will probably need it next navigation). Misusing preload for everything makes things worse by competing with the LCP resource for bandwidth.

They will push with
  • Why did bundling everything become an anti-pattern with HTTP/2?
  • What is 0-RTT and what is its security caveat? (Replay attacks.)
  • How does a CDN change any of this?
←previousDistributed systems↑ CovernextStaff behavioural→