Dependency Graph Construction
An asset pipeline must understand the relationships between source files to build correctly. The dependency graph is constructed by parsing import statements from every source file.
# Given source files:
# app.js imports utils.js and api.js
# utils.js imports helpers.js
graph = {
"app.js": ["utils.js", "api.js"],
"utils.js": ["helpers.js"],
"api.js": [],
"helpers.js": []
}
Construction algorithm: start from configured entry points (e.g., src/index.js), recursively traverse imports, add edges for each dependency. Build both a forward graph (module → its dependencies) and a reverse graph (module → modules that depend on it). The reverse graph is essential for incremental compilation.
Detect circular dependencies during graph construction using depth-first search with a visited-and-in-stack marker. Warn on cycles — they are valid in CommonJS but can cause initialization order bugs.
Incremental Compilation
Full rebuilds are expensive. On a large codebase, a cold build might take 60 seconds. Incremental builds target only what changed.
Algorithm:
- On file change, compute the new content hash for the changed file.
- Compare to cached hash. If unchanged (file was touched but content is identical): skip.
- Look up the changed file in the reverse dependency graph to find all modules that (transitively) depend on it.
- Recompile only the changed module and its affected dependents.
- Update the module cache with new compiled output and new content hash.
Cache key: SHA256(source_content + compiler_version + compiler_options). Changing compiler options (e.g., enabling a new Babel plugin) invalidates all cached outputs. Persist the cache to disk so it survives process restarts. Cold build warms the cache; subsequent builds read from it.
Result: warm incremental build time drops from 60s to 1-3s for a single file change in a large project.
Content-Hash Fingerprinting
Output filenames include a hash of the module's content:
bundle.js → bundle.a3f4b2c1.js
vendor.js → vendor.b91c3d4e.js
The hash is computed after all transforms: transpilation, minification. Identical source that produces identical output always generates the same hash. This is what enables long-lived CDN caching — the URL is immutable for a given content version.
The pipeline also emits an asset manifest mapping logical names to fingerprinted URLs. Server-side templates read this manifest to generate correct asset references.
Tree Shaking
Tree shaking eliminates dead code — exports that are defined but never imported anywhere in the application.
Requirements: ES module syntax (import/export) only. CommonJS (require/module.exports) cannot be statically analyzed.
Algorithm:
- Parse all modules, collect all exported symbols.
- Starting from entry points, traverse imports and mark every symbol that is actually used.
- In the minification pass, remove all unmarked exports and their dependent code.
A 200KB utility library where only 3 functions are used might contribute only 5KB to the bundle after tree shaking. Effective on large utility libraries (lodash-es, date-fns).
Side-effect annotations: modules that declare "sideEffects": false in their package.json can be more aggressively tree-shaken. Modules with side effects (CSS imports, polyfills) must be excluded from dead code elimination.
Code Splitting
Instead of one large bundle, split output into chunks to enable parallel loading and caching granularity:
- Route-based splitting: Each route/page has its own chunk. Only load code needed for the current page.
- Vendor splitting: Extract
node_modulesinto a separate vendor chunk. Vendor code changes less frequently than application code — users cache it longer. - Dynamic imports:
import('./heavy-module')creates an async chunk boundary. The chunk is loaded on demand, not at initial page load.
The manifest maps each route to its required chunks. Initial HTML loads the entry chunk, which lazily fetches route chunks as the user navigates.
Parallel Compilation
Module compilation (transpilation, minification) is CPU-bound and embarrassingly parallel. Distribute work across worker threads:
- Main thread manages the dependency graph, cache, and coordinates work.
- Worker pool (one worker per CPU core) handles individual module compilation.
- Independent modules compile concurrently; dependent modules compile after their dependencies complete.
On an 8-core machine, parallel compilation gives roughly 6-7x speedup over single-threaded (accounting for coordination overhead).
Source Maps, HMR, and Build Caching
Source maps map minified bundle positions back to original source files and line numbers. Essential for production error debugging. Generate as separate .map files and upload to your error tracking service (Sentry), not publicly served.
Hot Module Replacement (HMR): In development, the pipeline watches for file changes and pushes updated modules to the browser via WebSocket. The browser swaps the module in place without a full reload, preserving application state (React component tree, scroll position, form input). HMR requires modules to declare an accept handler:
if (module.hot) { module.hot.accept('./App', () => { render(); }); }
Build caching: Persist the compiled module cache to disk between runs. A cold build (no cache) takes ~60s on a large codebase. A warm build (cache hit for all unchanged modules) takes ~2s — only linking and asset manifest generation run. Cache is stored keyed by content hash and is safe to share across CI machines via a remote cache (e.g., S3-backed Turborepo cache).
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How does incremental compilation avoid rebuilding unchanged modules?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The pipeline maintains a dependency graph and a content-hash cache keyed by input file hash; on each build it computes hashes for changed source files, propagates dirtiness only to graph nodes that transitively depend on those files, and skips compilation for any module whose input hash matches the cached value. Persistent caches stored on disk or in a shared remote cache (e.g., Bazel remote cache) extend this benefit across CI runs and developer machines.”
}
},
{
“@type”: “Question”,
“name”: “How does content-hash fingerprinting enable infinite cache TTLs?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Each output asset's filename includes an MD5 or SHA-256 digest of its content (e.g., main.a3f2c1d8.js), so any change to the file produces a new URL; assets are served with Cache-Control: max-age=31536000, immutable, allowing CDNs and browsers to cache them forever without revalidation. The HTML document itself is served with a short TTL or no-cache so it always references the latest fingerprinted asset URLs.”
}
},
{
“@type”: “Question”,
“name”: “How does tree shaking eliminate dead code?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Tree shaking relies on ES module static import/export syntax, which is analyzable at compile time without execution; the bundler builds a module graph, marks all exports reachable from the entry point as live, and omits any export — and code reachable only from that export — from the output bundle. Side-effectful modules (marked sideEffects: true in package.json) are exempt from elimination even if their exports are unused.”
}
},
{
“@type”: “Question”,
“name”: “How does Hot Module Replacement work without a full page reload?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The dev server watches source files and on change sends an update manifest over a WebSocket to the in-browser HMR runtime, which fetches only the changed module chunk and calls the module's accept handler if defined, swapping the old module reference in the module registry for the new one. If no accept handler is found the runtime bubbles the update up the dependency chain until it either finds one or falls back to a full page reload.”
}
}
]
}
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Atlassian Interview Guide