Today’s goal
Make the front-end toolchain visible as an architecture rather than a collection of commands.
We will connect:
- package management, lockfiles, scripts, and modules;
- resolution, transformation, development servers, and HMR;
- production bundling, tree shaking, minification, and source maps;
- asset fingerprints, CSS, environment variables, and code splitting;
- Vite, Rollup, esbuild, Rolldown, and Turbopack;
- linting, formatting, type checking, testing, Git, and CI;
- workspaces, monorepos, package boundaries, and dependency direction;
- an inspectable, reproducible build workflow.
By the end of today you can
- explain why development and production optimize differently;
- read a module graph and identify dependency direction;
- distinguish resolution, transformation, bundling, and serving;
- understand what tree shaking can and cannot remove;
- choose code-splitting boundaries based on route behavior;
- keep build-time configuration separate from runtime secrets;
- design fast local feedback and shared CI verification;
- use workspaces without confusing them with architecture;
- expose package APIs without leaking private files;
- inspect emitted assets instead of guessing about performance.
The central principle
A build toolchain is a delivery architecture: it transforms source modules into environment-specific artifacts while preserving reproducibility, boundaries, and useful feedback.
The tool is not only a compiler.
It resolves dependencies, serves development code, creates production artifacts, and shapes how teams work.
Runtime and development dependencies
runtime dependency → needed by the shipped application
development dependency → needed to build, test, lint, or developMisclassifying a dependency can:
- inflate production output;
- break a library consumer;
- make a production build depend on local tooling;
- hide a missing runtime requirement.
Lockfiles belong in application repositories
A lockfile records:
- concrete versions;
- resolved locations;
- integrity information;
- transitive dependency choices.
It lets local development, CI, and deployment start from the same dependency graph.
Update it intentionally and use one package manager consistently.
Aliases can improve architecture - or hide it
import { Button } from "@shared/ui/Button";Aliases can make stable package boundaries readable.
They can also conceal deep dependencies and make imports appear more independent than they are.
Use aliases to communicate architecture, not to avoid designing it.
Fast Refresh is framework-aware HMR
Framework-aware refresh can preserve component state when a change is safe.
It may reset state when:
- module exports change shape;
- boundaries are not refresh-safe;
- initialization semantics change;
- the framework cannot preserve identity.
Test both preserved and reset behavior when it matters.
Dependency pre-bundling
Third-party packages may contain many modules or formats that are expensive to request individually.
Pre-bundling can:
- reduce browser request count;
- normalize dependency formats;
- improve development startup after caching.
It does not mean the production bundle and development graph are identical.
esbuild
esbuild emphasizes very fast native transformation and bundling.
It can be useful for:
- fast development transforms;
- custom build integration;
- straightforward application builds;
- tooling that needs quick parsing and emission.
Fast transformation does not remove the need for application architecture or type checking.
Vite and Turbopack solve related problems differently
development graph and feedback
production graph and output
framework-specific incremental integrationTools can differ in when they bundle, how they cache, and how they integrate with a framework.
The stable mental model is the source graph and emitted responsibility.
Avoid toolchain configuration as a hobby
Configuration should answer a delivery or developer-experience need.
Before adding a plugin or custom transform, ask:
- which problem does it solve?
- who owns it?
- what does it change in production?
- how is it tested?
- what is the fallback if it breaks?
Complexity without a user or team benefit is a liability.
Build once, deploy predictably
source + locked dependencies + config
→ one verified artifact
→ promote the artifact across environmentsRebuilding separately for each environment can produce different output and weaken release confidence.
Keep truly runtime-varying configuration outside immutable assets where possible.
A workspace is not automatically a monorepo architecture
workspace → repository/package coordination mechanism
monorepo → repository organized around multiple related projects/packagesA workspace can host one application plus tools.
A monorepo still needs package boundaries, ownership, and dependency direction.
Circular dependencies are design feedback
A → B → C → AEven if the build succeeds, cycles can create:
- partial initialization;
- undefined exports;
- confusing evaluation order;
- difficult testing;
- architecture that cannot be layered cleanly.
Break the cycle by clarifying ownership or extracting a stable lower-level contract.
dev is not a production server
Development servers may:
- transform on demand;
- tolerate missing optimizations;
- expose source files;
- use different caching;
- allow permissive CORS or proxies;
- rely on local filesystem behavior.
Use a production build and production-like serving environment for delivery verification.
Library builds versus application builds
application → optimize one deployed experience
library → preserve a reusable public API and externalize peersLibrary output needs stable formats, declarations, exports, and consumer compatibility.
Application output can make more assumptions about its deployment.
Tree shaking and package design
Packages are easier to optimize when they:
- use analyzable ES modules;
- expose focused entry points;
- avoid import-time side effects;
- declare side-effect behavior accurately;
- avoid pulling a whole framework for one helper.
Package API design directly affects emitted application code.
Toolchain dependencies have supply-chain risk
Build tools execute code in a privileged development and CI context.
Reduce risk with:
- lockfiles and review;
- trusted registries;
- minimal dependencies;
- update monitoring;
- restricted CI credentials;
- artifact inspection.
The toolchain is part of the application’s attack surface.
Toolchain smells
Watch for:
- build configuration nobody understands;
- every project using different lint rules;
- lockfiles regenerated by multiple managers;
- production builds rarely run;
- huge initial bundles despite route structure;
- hundreds of tiny chunks;
- secrets in front-end environment variables;
- internal package imports through private paths;
- circular dependencies everywhere.
Choose tools by responsibility
application dev server + build → high-level framework tool
specialized library output → library bundler
fast transformation → native transformer
multiple local packages → workspace / monorepo tooling
framework-specific pipeline → framework-integrated toolChoose the smallest toolset that serves the responsibility.
Practical stages 1–4: create and inspect
- Create a small TypeScript application.
- Add a dynamically imported reports route.
- Inspect development requests and production chunks.
- Compare source modules with emitted assets and source maps.
Record which decisions belong to the toolchain and which belong to application architecture.
Try this yourself
Pick one initial route and answer:
- which source modules does it need?
- which dependency dominates its size?
- which feature can split at a route boundary?
- what can be tree-shaken?
- what must remain a side effect?
- which environment values are public?
Then verify every answer against the emitted build.
Troubleshooting guide (Part 1)
| Symptom | Likely cause |
|---|---|
| Dev works, production fails | Different transforms, paths, or environment assumptions |
| Reports code is in the initial chunk | Import is static or split boundary is ineffective |
| Unused package code remains | Side effects, module format, or graph opacity |
| CI differs from local | Lockfile, runtime, or global tool mismatch |
| Secret appears in client output | Public build-time variable was treated as private |
Troubleshooting guide (Part 2)
| Symptom | Likely cause |
|---|---|
| Package consumers import private files | Public API is incomplete or undocumented |
| HMR behaves strangely | Import-time side effects or cleanup missing |
| Tiny chunks hurt navigation | Split points follow files rather than user journeys |
| Build graph contains cycles | Dependency direction is unclear |
Completion checklist
- package versions and lockfile are reproducible;
- resolution and module boundaries are understandable;
- type checking is an explicit quality step;
- development and production workflows are distinguished;
- code splitting follows route or feature behavior;
- tree-shaking assumptions are validated by output;
- source maps and environment values have policy;
- package APIs protect internal files;
- CI verifies a clean production build;
- emitted artifacts are inspected rather than guessed at.
Misconceptions to leave behind (Part 1)
| Misconception | Better mental model |
|---|---|
| A build tool is just a compiler | It resolves, transforms, serves, bundles, and emits |
| Bundling means concatenation | It is graph transformation and asset design |
TypeScript must emit JavaScript through tsc | Type checking and transformation can be separate |
| Transpilation adds missing browser APIs | Polyfills and runtime support are separate |
| More code splitting is always better | Split around user journeys and costs |
| Tree shaking removes anything not called | It depends on analyzable graphs and side effects |
Misconceptions to leave behind (Part 2)
| Misconception | Better mental model |
|---|---|
| Minification makes source maps unnecessary | Debugging still needs source policy |
.env values are secret | Client-exposed values are public |
| A workspace is a monorepo architecture | Coordination tooling and boundaries differ |
| A successful circular build is healthy | Cycles are dependency-design feedback |
| The dev server is production | Production artifacts and serving behavior differ |
| Quality gates are interchangeable | Lint, format, types, tests, and build answer different questions |