Engine & Tooling
The JS engine (V8, JIT, hidden classes), transpilation (Babel), bundling and tree-shaking, module systems (CommonJS vs ESM), runtimes (Node/Deno), and polyfills.
12 questions
JuniorTheoryVery commonWhat is the difference between CommonJS and ES modules?
What is the difference between CommonJS and ES modules?
CommonJS (Node's original system) uses require() to import and module.exports to export, loading modules synchronously at runtime. ES modules (the standard) use import/export, are parsed statically before execution, and support async loading. ESM bindings are live read-only views, while a CommonJS require returns a snapshot copy of the exported value.
Common mistakes
- ✗Thinking
import/exportandrequire/module.exportsare interchangeable keyword-for-keyword - ✗Believing CommonJS
requirereturns a live binding like ESM rather than a value snapshot - ✗Assuming ES modules load synchronously the way CommonJS
requiredoes
Follow-up questions
- →Why can ES modules be tree-shaken but a typical CommonJS module cannot?
- →How does the live-binding nature of ESM exports change behavior under circular imports?
MiddleTheoryCommonWhat does a bundler do, and how do tree-shaking, minification, and code-splitting relate to it?
What does a bundler do, and how do tree-shaking, minification, and code-splitting relate to it?
A bundler (e.g. Webpack, Rollup, esbuild) follows the module graph from an entry point and combines many modules into fewer files. Tree-shaking drops unused exports — it needs static ES modules to know what is reachable. Minification shrinks the output by removing whitespace and renaming locals. Code-splitting does the opposite of combining: it carves the bundle into chunks loaded on demand.
Common mistakes
- ✗Expecting tree-shaking to work on dynamic CommonJS modules rather than static ES modules
- ✗Confusing minification (shrinks text) with tree-shaking (removes unused exports)
- ✗Thinking code-splitting always reduces total bytes rather than deferring some to load on demand
Follow-up questions
- →Why does a
require()with a computed path defeat tree-shaking entirely? - →How does code-splitting improve first-load time even though total downloaded bytes may rise?
MiddleTheoryCommonWhat does the transpiler Babel do, and how does transpilation differ from polyfilling?
What does the transpiler Babel do, and how does transpilation differ from polyfilling?
Babel is a transpiler: it rewrites modern JavaScript syntax (arrow functions, class, optional chaining) into equivalent older syntax that target engines can parse, guided by a configured target list (e.g. browserslist). Transpilation transforms syntax; polyfilling adds missing runtime APIs like Promise. Babel handles syntax and pulls in polyfills (via core-js) only for the APIs your targets lack.
Common mistakes
- ✗Conflating transpilation (rewrites syntax) with polyfilling (adds missing runtime APIs)
- ✗Thinking Babel outputs machine code rather than older-syntax JavaScript
- ✗Assuming Babel transpiles for every browser rather than only the configured targets
Follow-up questions
- →Why does Babel still need
core-jsto makeArray.prototype.flatwork on old engines? - →How does configuring a higher target in browserslist reduce the amount of transpiled output?
SeniorTheoryCommonWalk through a modern JS build pipeline from source to shipped bundle, including where source maps fit.
Walk through a modern JS build pipeline from source to shipped bundle, including where source maps fit.
Source is first transpiled (e.g. Babel) to lower modern syntax to the targets. A bundler then walks the static ESM graph to combine modules, tree-shaking unused exports out. Minification shrinks the result; polyfills are injected for APIs the targets lack (often via core-js, ideally only the needed ones). Each stage emits a source map, chained so the final map points minified positions back to original source for debugging.
Common mistakes
- ✗Getting the stage order wrong — e.g. minifying before transpiling and bundling
- ✗Shipping the whole polyfill library instead of only the APIs the targets lack
- ✗Forgetting that source maps must be chained across stages to point back to true source
Follow-up questions
- →Why must transpilation generally run before tree-shaking can analyze the module graph?
- →How do chained source maps survive multiple transforms so a stack trace stays accurate?
JuniorTheoryOccasionalWhat is a JavaScript engine, and what are its main stages of executing code?
What is a JavaScript engine, and what are its main stages of executing code?
A JS engine (e.g. V8 in Chrome and Node) is a program that runs JavaScript. It parses source into an AST, compiles it to bytecode, and starts interpreting immediately; a JIT compiler then recompiles hot code to optimized machine code at runtime. JavaScript execution itself is single-threaded — one call stack runs one task at a time.
Common mistakes
- ✗Calling JS purely interpreted, ignoring that modern engines JIT-compile hot code to machine code
- ✗Thinking the engine runs JavaScript on multiple threads in parallel rather than a single call stack
- ✗Confusing the engine with the runtime that provides APIs like timers, DOM, or the file system
Follow-up questions
- →What conditions make the JIT compiler decide a function is hot enough to optimize?
- →If JavaScript is single-threaded, how does a runtime still handle async I/O concurrently?
JuniorTheoryOccasionalWhat is a polyfill, how does it differ from a shim, and how does feature detection fit in?
What is a polyfill, how does it differ from a shim, and how does feature detection fit in?
A polyfill is code that implements a missing built-in API (e.g. Promise, Array.prototype.includes) so older environments behave as if it were native. A shim is the broader term: any code that intercepts or patches an API to provide consistent behavior, not necessarily a missing standard one. Feature detection (if (!window.Promise)) decides at runtime whether to load the polyfill, rather than sniffing the browser.
Common mistakes
- ✗Confusing a polyfill (adds a missing API) with transpilation (rewrites unsupported syntax)
- ✗Detecting features by sniffing the user-agent string instead of testing for the API directly
- ✗Treating shim and polyfill as exact synonyms rather than shim being the broader category
Follow-up questions
- →Why can a polyfill add
Array.prototype.includesbut cannot add theasync/awaitsyntax? - →How does feature detection avoid the fragility of user-agent sniffing?
JuniorTheoryOccasionalHow do browser, Node.js, and Deno runtimes differ in the APIs they provide?
How do browser, Node.js, and Deno runtimes differ in the APIs they provide?
All three embed a JS engine but expose different host APIs. Browsers add the DOM, fetch, and localStorage but no file system. Node.js adds file system, networking, and process via CommonJS modules (with ESM support). Deno (the runtime by Node's creator) uses web-standard APIs and ESM by default, ships TypeScript natively, and sandboxes file/network access behind explicit permission flags.
Common mistakes
- ✗Assuming the DOM exists in Node.js or that the file system exists in the browser
- ✗Thinking the shared engine means all host APIs are identical across runtimes
- ✗Believing Deno needs the file system enabled by default rather than behind permission flags
Follow-up questions
- →Why does code that calls
document.querySelectorthrow aReferenceErrorunder Node.js? - →What problem does Deno's permission model solve that Node.js leaves to the developer?
MiddleTheoryOccasionalHow does Node.js interoperate between CommonJS and ES modules, and where does dynamic import() fit?
How does Node.js interoperate between CommonJS and ES modules, and where does dynamic import() fit?
Node decides a file's type by .mjs/.cjs extension or the package's "type" field, then resolves specifiers via node_modules lookup and exports maps. ESM can import a CommonJS module (its module.exports becomes the default export), but CommonJS cannot statically require an ESM module because ESM loads asynchronously. The escape hatch is dynamic import(), which returns a Promise and works from either system at runtime.
Common mistakes
- ✗Trying to
require()an ES module synchronously instead of using dynamicimport() - ✗Thinking dynamic
import()returns the module directly rather than aPromise - ✗Ignoring how the package
"type"field and file extension decide a module's resolution
Follow-up questions
- →Why can't a top-level
require()of an ESM module work given ESM's async loading? - →How does an
exportsmap inpackage.jsonchange which file a bare specifier resolves to?
MiddleTheoryOccasionalWhat is a source map, and how does it help debug minified or transpiled code?
What is a source map, and how does it help debug minified or transpiled code?
A source map is a JSON file that maps positions in the generated (minified or transpiled) output back to the original source — file, line, and column. The browser loads it when DevTools is open (via a //# sourceMappingURL comment), so breakpoints, stack traces, and errors show original code instead of the mangled bundle. It is a debug aid only and does not change how the shipped code runs.
Common mistakes
- ✗Thinking the engine executes the source map instead of the generated bundle
- ✗Believing the minified bundle cannot run without its source map present
- ✗Assuming source maps change runtime behavior rather than only aiding debugging
Follow-up questions
- →Why might you serve source maps only to authenticated developers rather than all users?
- →How does the browser know where to fetch a source map if the comment is stripped?
MiddleTheoryRareHow do V8's hidden classes and inline caching speed up property access, and what triggers deoptimization?
How do V8's hidden classes and inline caching speed up property access, and what triggers deoptimization?
V8 assigns each object a hidden class (shape) describing its property layout, so objects created the same way share one shape and properties live at fixed offsets. Inline caching then remembers the shape seen at a call site to fetch the property directly next time. Changing an object's shape — adding properties in a different order, deleting one, or passing mixed shapes — invalidates the cache and triggers deoptimization back to slower generic code.
Common mistakes
- ✗Adding properties to objects in inconsistent order, forcing V8 to create divergent hidden classes
- ✗Thinking inline caching stores the property value rather than its shape and offset
- ✗Believing optimized code never deoptimizes once V8 has compiled it
Follow-up questions
- →Why does
delete obj.prophurt performance more than setting the property toundefined? - →How does a monomorphic call site outperform a polymorphic one in inline caching?
SeniorTheoryRareHow do garbage collection, memory leaks, and the event loop relate to the engine versus the runtime?
How do garbage collection, memory leaks, and the event loop relate to the engine versus the runtime?
The engine owns the heap and runs a tracing garbage collector that reclaims objects unreachable from roots; a leak is unintended reachability — lingering closures, detached DOM nodes, growing globals, or live timers. The event loop, by contrast, belongs to the runtime (browser or Node), not the engine: it feeds tasks and microtasks onto the engine's single call stack. Deoptimization is separate — triggered by shape or type changes, not by GC.
Common mistakes
- ✗Placing the event loop inside the engine when it actually belongs to the runtime
- ✗Thinking a tracing GC makes leaks impossible, ignoring unintended reachability
- ✗Attributing deoptimization to garbage collection rather than shape or type changes
Follow-up questions
- →Why does a forgotten
setIntervalkeep its captured variables alive despite a tracing GC? - →How do microtasks and macrotasks interleave on the engine's call stack via the runtime's loop?
SeniorTheoryRareWhy does tree-shaking require static ES modules, and what roles do side-effect flags play?
Why does tree-shaking require static ES modules, and what roles do side-effect flags play?
Tree-shaking is dead-code elimination over the import graph: the bundler can drop an export only if it proves no path reaches it. ES module import/export are static — analyzable before running — so the graph is known; CommonJS require can take a computed path and reassign module.exports at runtime, defeating the analysis. The sideEffects flag in package.json tells the bundler a module is pure, so even imported-but-unused modules may be removed entirely.
Common mistakes
- ✗Believing tree-shaking analyzes CommonJS as precisely as static ES modules
- ✗Thinking the
sideEffectsflag keeps modules rather than letting pure ones be removed - ✗Assuming dead-code elimination needs to run the code rather than statically prove reachability
Follow-up questions
- →Why can marking a module with side effects accidentally retain code you meant to shake out?
- →How can re-exporting through a barrel file break tree-shaking despite using ESM?