Frontend · Guide

Build Tooling

npm and lockfiles, bundling with Vite, and the linting and formatting that stop reviews being about whitespace.

— min read Frontend

What the Build Is For

Browsers cannot run your source directly at any reasonable speed: hundreds of module requests, syntax older browsers do not know, and none of it minified. The build turns source into something a browser loads quickly.

Three jobs, essentially. Resolve the module graph from your entry point. Transform syntax the target browsers do not support. Optimise — bundle, minify, split and hash for caching.

Modern tools split development from production deliberately. In development, native ES modules give near-instant startup with no bundling at all; for production, everything is bundled and optimised. That asymmetry is why dev servers became fast.

npm, Lockfiles & semver

package.json states what you want; the lockfile records exactly what you got. Both matter, and the lockfile is the one that makes builds reproducible.

RangeAcceptsMeaning
1.2.3Exactly thatPinned
~1.2.31.2.xPatches only
^1.2.31.x.xMinors and patches — the default
*AnythingNever do this

Semver promises that major breaks, minor adds, patch fixes. It is a convention, not a guarantee — plenty of minor releases have broken things — which is exactly why the lockfile is committed.

Use npm ci in CI, not npm install. It installs precisely what the lockfile says and fails if the lockfile disagrees with package.json. install may quietly resolve something newer, so CI tests a different tree than your laptop.

dependencies ship to production; devDependencies are build and test only. Getting that wrong either bloats the install or breaks the build with a missing package.

Vite & Module Bundling

A bundler follows imports from an entry point, builds the module graph, and emits files for the browser. The optimisations along the way are what make it worth the complexity.

StepDoesGains
Tree shakingDrops exports nobody importsSmaller bundle
Code splittingSeparate chunk per routeLoad only this page's code
MinificationStrips names and whitespaceFewer bytes
Content hashingFilename changes when content doesCache forever, safely
TranspilingRewrites new syntaxOlder browsers work

Tree shaking only works on static ES module imports — the tool must see at build time what is used. CommonJS require and dynamic imports of computed paths defeat it, which is one practical reason ESM won.

Content hashing is what makes aggressive caching safe: app.4f2a1c.js can be cached for a year, because changing the file changes its name. Any asset served immutably needs this, or a fix ships to nobody.

ESLint & Prettier

They do different jobs and the distinction matters. Prettier formats — where the line breaks, how it indents. ESLint finds problems — an unused variable, a missing effect dependency, an await that was forgotten.

Formatting should never be discussed in review. Adopt the defaults, run on save and in CI, and the entire category of comment disappears.

RunsWhereOn failure
FormatOn save, and a pre-commit hookFixes it silently
LintEditor, and CIBlocks the merge
Type checkCIBlocks the merge
TestsCIBlocks the merge
A lint rule nobody can satisfy gets disabled with an inline comment, and then the rule protects nothing. If a rule is being suppressed repeatedly, either fix the underlying pattern or remove the rule — leaving both is the worst of the three.

Interview Questions

Why commit a lockfile?

package.json records ranges; the lockfile records the exact resolved tree. Without it two installs a week apart can produce different dependencies, so a build passing locally fails in CI for reasons nothing in the diff explains.

npm ci or npm install in CI?

ci. It installs exactly the lockfile and fails if it disagrees with package.json. install may resolve something newer, so CI would be testing a tree nobody else has.

What is tree shaking, and when does it fail?

Dropping exports nothing imports. It needs static analysis, so it works on ES module imports and fails on CommonJS require or dynamic imports with computed paths.

Why hash filenames?

So the file can be cached indefinitely. The name changes when the content does, so a new deploy produces a new URL and nobody is served a stale copy. Without it you must choose between stale assets and no caching.

ESLint and Prettier — why both?

Prettier decides formatting, which stops style discussion in review. ESLint catches real problems — unused variables, missing dependencies, unhandled promises. Formatting is not correctness, and correctness is not formatting.

Quick Quiz

1. ^1.2.3 accepts…
2. In CI you should run…
3. Tree shaking fails on…
4. Content hashing in a filename exists to…
5. A missing effect dependency is caught by…