Chapter 26 — Build Tools and Bundlers
Section 1 — Why source files are not always what the browser receives
When you write a front-end project, you organise code for humans. You split it into small modules, you name files clearly, you write styles in a way that is easy to maintain, and you keep assets in folders that make sense to you and your team. The browser, however, does not need human organisation. It needs files it can fetch, read, and run efficiently, in an order it can follow, without unnecessary work. Those two needs, human readability and browser efficiency, are not the same, and a build step exists to bridge the gap between them. It takes the source you wrote for humans and produces output that is shaped for browsers and visitors.
This distinction becomes clear as soon as a project grows beyond a handful of files. A human wants many small modules, each with one job, because small files are easier to understand, test, and change. A browser, fetching each file separately, must make a request for every module, wait for each one, and follow every import, which adds work and waiting time, especially on slower connections. A human wants styles written in a maintainable structure, perhaps split across many files by feature. A browser wants styles it can apply without resolving a tangle of imports at runtime. A human wants images in their original, high-quality form for editing. A browser wants images in a form that downloads quickly and displays correctly. Source and output serve different audiences, and trying to make one set of files serve both perfectly usually serves neither well.
A build tool does not change what your site does. It changes how your site's code is packaged for delivery. It can combine many modules into fewer files, transform styles into a form browsers understand directly, optimise images, and remove code that is never used. The result should behave the same as the source, from the user's point of view, but it should load and run more smoothly, because the packaging has been optimised for delivery. That sameness of behaviour is the test of a good build. If a build changes what users see or do, something has gone wrong in the build, or in the source that the build revealed, and the build output should be checked, not assumed correct.
Understanding this separation also helps you organise a project. Source files live in one place, under version control, edited by humans, reviewed by teammates, and tested in development. Built output lives in another place, produced by a tool, not edited by hand, and often not committed to version control, because it can be rebuilt from source at any time. Keeping those two worlds distinct prevents a common confusion, where a developer edits a built file directly, only to see the edit vanish on the next build, because the build overwrites its output from source. Edit source, build output, and let the tool do the translation between them. That rule, simple as it sounds, is the foundation for everything else in this chapter.
Check: Why might the files you write differ from the files a browser receives?
Section 2 — What a build step is
A build step is a process that takes your source files as input and produces built files as output, ready for browsers to use. Think of it as a small factory for your site. Raw materials go in one end, in the form of modules, styles, images, and other assets you wrote and organised for humans. Finished products come out the other end, in the form of files shaped for delivery, combined, transformed, and optimised as needed. The factory itself is a tool, or a set of tools, configured for your project, and you run it whenever you want fresh output, whether for testing a production-like version locally or for deploying to visitors.
The build step is not a single action, but a sequence of actions, each handling a different kind of file or a different kind of optimisation. One action might follow imports and combine modules. Another might transform styles. Another might copy or optimise images. Another might remove unused code or shorten names in a safe way. The order of those actions matters, because some transformations depend on others having happened first, and a build tool arranges them in a sensible sequence, so you do not need to run each action by hand in the right order. That arrangement is part of what a build tool provides: not just the individual transformations, but a pipeline that runs them reliably, every time, in the same order, with the same settings.
You will encounter build steps in two main contexts. During development, a build tool often runs continuously, watching your source files and rebuilding quickly whenever you save, so you can test changes almost immediately. For production, you run a build once, deliberately, to produce the final output that will be deployed. The development build prioritises speed and debuggability, so you can work quickly and see errors clearly. The production build prioritises efficiency and correctness for visitors, even if it takes longer to run, because visitors will use its output many times, while you will run the build only occasionally. Understanding that the same tool can produce different output for different purposes, depending on its mode, is key to using build tools well.
A build step is also a checkpoint. If the build fails, it usually means something in the source is wrong, such as a missing import, a syntax error, or a file that cannot be found, and the build's error message points at the problem, often more precisely than a browser error would. That early failure is valuable, because it catches problems before deployment, while they are still cheap to fix. A build that succeeds, on the other hand, gives you confidence that the source, as a whole, can be packaged for delivery, even if it does not guarantee that every feature works correctly. The build is a packaging check, not a substitute for testing, but it is a check that every serious project should pass before its output is shared with visitors.
Check: In your own words, what does a build step do?
Section 3 — Development versus production
Development and production are two different contexts for the same site, and they have different priorities. Development is the context in which you write, test, and change code. It prioritises speed of feedback, clarity of errors, and ease of debugging. You want to save a file and see the result quickly, to read an error message that points at your source, not at transformed output, and to experiment without worrying about whether every optimisation is perfect. Production is the context in which visitors use the finished site. It prioritises loading speed, reliability, and efficiency, even at the cost of longer build times and harder debugging, because visitors will load the site many times, and a small saving per load adds up across many visits.
Build tools reflect this difference by offering different modes, or different settings, for development and production. A development build might keep code readable, preserve original file structure as much as possible, include detailed source maps that link output back to source, and skip time-consuming optimisations that do not affect whether a feature works. A production build might combine files, remove unused code, shorten internal names where safe, optimise images, and produce output that is as small and efficient as possible, even if it is hard for a human to read. Neither mode is more correct than the other. Each is correct for its context, and using the wrong mode in the wrong context causes problems: a production build during development slows feedback and hides errors, while a development build in production wastes visitors' time and bandwidth.
It is important to test both modes, not just the one you develop in. A site that works in development can fail in production, if a production optimisation removes code that was actually needed, or if a transformation changes behaviour in a subtle way. Conversely, a site that works in production but is hard to debug in development will slow future work, because every fix will be harder to trace. The habit of building for production locally, before deploying, and opening the built output to check that key features still work, catches production-only problems while they are still cheap to fix. It also builds familiarity with what production output looks like, so that a production error, when it occurs, is less mysterious.
Think of development and production as two lenses on the same project. Development asks, "Can I work with this quickly and understand it clearly?" Production asks, "Will visitors receive this quickly and reliably?" A good build setup answers both questions well, by giving you a fast, debuggable development experience and an efficient, reliable production output, from the same source, with different settings. Keeping those questions separate, and testing both answers, is a hallmark of professional front-end work.
Check: Why should a production build be different from a development build?
Section 4 — What a bundler does
A bundler is a build tool whose main job is to combine many source files into fewer output files, called bundles, that a browser can load efficiently. It starts from an entry point, follows the imports in that file to find everything the page needs, follows the imports in those files in turn, and continues until it has built a complete graph of dependencies, much like the import graph described in the modules chapter. It then writes that graph's code into one or more output files, in an order that respects dependencies, so that a function is defined before it is used, and a module's setup runs before the code that depends on it. The browser then loads the bundle, or bundles, instead of loading dozens of separate modules one by one.
The benefit is not just fewer files, although fewer files usually means fewer requests and less waiting. A bundler also resolves problems that plain browser loading cannot solve on its own. It can translate bare imports, which name a library without a path, into real files from the project's downloaded libraries, because it knows where those libraries live and how to find the right file inside them. It can handle imports of things that are not JavaScript, such as styles or images, by transforming them into a form the browser can use, or by copying them to the output folder and rewriting the import to point at the copy. It can also apply transformations to the code itself, such as converting newer syntax into a more widely supported form, or removing code that is never used, as covered in later sections. The bundler is therefore not just a combiner. It is a resolver, a transformer, and a packager, all in one.
Bundlers produce output in different shapes, depending on how they are configured. A simple project might produce a single bundle for the whole site, which is easy to understand and to deploy, but which loads all code on every page, even code that only one page needs. A larger project might produce several bundles, one per page or per feature, so that each page loads only the code it needs, plus shared code that many pages use. That splitting, covered later as code splitting, is a refinement of bundling, not a replacement for it. The core idea remains: start from entry points, follow dependencies, and write output files that contain exactly the code those entry points need, in the right order, ready for the browser.
For a beginner, the most important thing to understand about a bundler is that it does not change your source. It reads source and writes output, leaving source untouched. If the output behaves differently from the source, the problem is in the bundler's configuration, in a transformation it applied, or in a source assumption that the bundler exposed, such as a reliance on a file order that bundling changed. Debugging a bundler problem therefore starts with comparing source and output, and with reading the bundler's errors and warnings, which usually name the file and the import that caused trouble. The bundler is a tool you configure and inspect, not a black box you must accept blindly.
Check: What is a bundler's main job?
Section 5 — Entry point and output
Every bundler needs to know where to start and where to put the result. The starting point is called the entry point, and it is usually the main file of your app, the file that the page loads and that imports everything else the page needs. The bundler reads the entry point, follows its imports to build the dependency graph, and includes in the output only the code that is reachable from that entry point, directly or indirectly. Code that is never imported from the entry point is not part of the app, as far as the bundler is concerned, and it will not appear in the output, however correct it may be on its own. That reachability rule is why a new file must be imported, through some chain, from the entry point before it will be bundled.
The output is the set of files the bundler writes, usually into a dedicated output folder, separate from source. That folder is the built version of the site, or at least the built JavaScript and related assets, ready to be served or deployed. Its contents are generated, not hand-written, and they should not be edited by hand, for the same reason that generated files in general should not be edited: the next build will overwrite them from source, discarding any hand edit. The output folder's name and location are configured for the project, and different projects use different conventions, but the principle is the same: source in, output out, and the two kept separate so that it is always clear which files are human work and which are tool output.
A project can have more than one entry point, when it has more than one page or more than one distinct app sharing the same source. A site with a public page and an admin page, for example, might have an entry point for each, so that each page's bundle contains only the code that page needs, plus shared code that both use. Multiple entry points keep bundles focused and pages light, at the cost of a slightly more complex configuration, because each entry point must be listed and its output named. A single-page project, by contrast, usually has a single entry point, which keeps configuration simple and is the right starting point for learning.
When you configure or debug a bundler, the entry point and the output folder are the first two things to check. Is the entry point the file you think it is, and does it import the code you expect? Is the output going to the folder you think it is, and are you opening the output files, not the source files, when you test the built site? Many bundler confusions come from checking the wrong files, either source when you meant output, or output when you meant source. Keeping entry and output clear, in configuration and in your own mental model, prevents that confusion and makes the bundler's behaviour predictable.
Check: Why does code that is never imported from the entry point not appear in a bundle?
Section 6 — Following imports to build a graph
A bundler, like a browser loading modules, builds a graph by following imports. It starts at the entry point, reads its import statements, and adds each imported file to the graph. It then reads those files' imports, adds their dependencies, and continues, level by level, until it reaches files that import nothing. The result is a complete map of what the entry point needs, including libraries, helpers, styles, and assets, organised by dependency: a file appears after the files it depends on, so that when the bundle runs, every value is defined before it is used. That ordering is not a convenience. It is essential, because code that uses a value before it is defined will fail, and the graph is how the bundler guarantees the order is correct.
The graph also reveals problems that are hard to see by reading files one by one. A circular import, where two files import each other, appears in the graph as a loop, and the bundler must decide how to handle it, usually by warning or by ordering the files in a way that may leave a value undefined at first use, as discussed in the modules chapter. A missing file appears as a broken edge in the graph, where an import points at a path that does not exist, and the bundler reports it as an error, naming the importing file and the missing path. A duplicate dependency, where two versions of the same library are pulled in through different paths, appears as two separate nodes that should have been one, and the bundler's configuration can often resolve it by pointing both paths at a single copy. Reading the graph, through the bundler's output and warnings, is therefore a debugging skill, not just an abstract concept.
The graph also determines what is included and what is left out. Only files reachable from the entry point are included, which means that dead code, files that are never imported, and exports that are never used, can be identified and, in production, removed, as covered in the section on tree shaking. That selectivity is a strength, because it keeps bundles focused on what the app actually needs, rather than on everything that happens to exist in the project folder. It also means that the graph is a form of documentation: it shows, in a precise, machine-readable way, how the app's pieces depend on each other, which is information that is easy to lose in a large project where dependencies are only implicit in import statements scattered across files.
For a developer, the practical lesson is to keep the graph clean. Keep imports precise, importing only what a file uses. Avoid circular imports, by extracting shared pieces into their own files. Keep the dependency structure layered, with entry points at the top, features in the middle, and small shared helpers at the bottom, imported by many but importing little themselves. A clean graph bundles cleanly, debugs cleanly, and scales cleanly, because every file's place in the system is clear from its imports and its importers. A tangled graph bundles slowly, warns frequently, and breaks in ways that are hard to trace, because no one, human or tool, can easily tell what depends on what.
Check: What does a bundler learn by following imports from the entry point?
Section 7 — Why bundling helps loading
Loading a page involves fetching files, and each fetch takes time, especially on slower connections or on networks where each request has overhead beyond the file's size itself. A page with many separate modules, styles, and assets therefore spends time not just downloading content, but also requesting, waiting for, and receiving each file, one after another, in an order dictated by dependencies. Bundling reduces that overhead by combining many files into fewer, larger files, so that the browser makes fewer requests and can start running code sooner, after fewer round trips. The total amount of code does not necessarily shrink, although other optimisations can shrink it, but the number of separate fetches does, and that reduction alone can make a page feel noticeably faster.
Bundling also helps with ordering and reliability. When the browser loads many separate modules, it must follow the import graph at runtime, fetching each dependency as it is discovered, which can lead to a cascade of requests that is hard to predict and hard to optimise. When a bundler has already followed the graph at build time, it can write the code in the correct order, in a single file, so that the browser's job is simply to load that file and run it, without needing to discover dependencies on the fly. That pre-computed order is more reliable, because it was worked out once, during the build, with full knowledge of the project, rather than being worked out repeatedly, at runtime, under network conditions that vary from visitor to visitor.
There is a limit to this benefit, which is why bundling is not simply a matter of combining everything into one enormous file. A single bundle for a large site loads all code on every page, including code that only one page needs, which wastes time for visitors who never visit that page. A bundle that is too large also takes longer to download as a single file, and it must be downloaded again in full if any part of it changes, because the browser's cache treats it as one unit. The solution, covered later as code splitting, is to bundle thoughtfully: combine what is needed together, split what is needed separately, and share what is needed everywhere, so that each page loads a sensible set of bundles, not too many and not too few. Bundling helps loading when it is done with that balance in mind, not when it is done blindly.
For a beginner, the takeaway is not a rule about how many bundles to produce, but an understanding of the trade-off. Fewer files mean fewer requests and simpler ordering, but larger files mean more code per page and less effective caching when part of the code changes. A good bundler configuration finds a balance for the project at hand, and that balance may change as the project grows. Start with a simple bundling setup, measure how the built site actually loads, and adjust the splitting only when loading measurements, not guesses, suggest that a change will help. Loading is a user experience, and it should be judged by what users experience, not by how tidy the bundle count looks in a configuration file.
Check: How does combining files into bundles help a page load?
Section 8 — Transpiling: newer syntax for wider support
Transpiling, sometimes called transforming, is the process of converting code written in one form of a language into another form of the same language, so that it runs in more places. In front-end work, this usually means converting code that uses newer language features, which are convenient for developers, into code that uses older, more widely supported features, which run in a broader range of browsers. The converted code should behave the same as the original, from the user's point of view, but it is written in a style that older browsers can understand, because the newer features have been replaced with equivalent older patterns.
Why would a developer write newer features if they need converting? Because newer features often make code clearer, shorter, and less error-prone. A feature that lets you write a function more concisely, handle asynchronous work more readably, or express a data transformation in a single line, can make a codebase easier to maintain, even if it needs conversion before delivery. Transpiling lets developers use those conveniences without excluding visitors whose browsers do not yet support them, by doing the conversion automatically at build time, rather than requiring developers to write only in the older style by hand. That automation is valuable, because hand-converting newer code to older code is tedious and error-prone, and it would discourage developers from using helpful features at all.
A transpiler is configured with a target: a description of which browsers, or which level of language support, the output should aim for. A broad target, covering many older browsers, requires more conversion and produces output that is more different from the source, but that runs almost everywhere. A narrow target, covering only recent browsers, requires less conversion and produces output closer to the source, but that may not run in older browsers. Choosing a target is therefore a trade-off between developer convenience, output size, and visitor coverage, and it should be based on who actually visits the site, not on a guess about which browsers matter. If the site's visitors mostly use recent browsers, a narrow target is reasonable. If the site must serve a wide audience, including older devices, a broad target is safer, even if it produces larger output.
Transpiling is not magic, and it has limits. It can convert syntax, which is how code is written, but it cannot always provide new built-in features, such as new methods or objects, that older browsers simply do not have. Those features require additional code, sometimes called a polyfill, which adds the missing feature at runtime, and that is a separate decision from transpiling syntax. A build setup may include both, but they should be understood separately: transpiling changes how code is written, polyfilling adds what code can call. Keeping that distinction clear helps you debug cases where code still fails in an older browser after transpiling, because the failure may be due to a missing built-in feature, not to unconverted syntax.
Check: What does transpiling change, and what does it leave unchanged?
Section 9 — Handling styles in a build
Styles are not JavaScript, but a build tool must handle them, because a site's appearance is part of its delivery, and styles have their own dependencies and ordering rules. In a source project, styles might live in separate files, imported by JavaScript modules, or linked from HTML, or written in a form that needs preprocessing before browsers can use it. A build tool takes those source styles, follows their imports, applies any needed transformations, and produces output styles that the browser can apply directly, usually as one or more stylesheet files linked from the built HTML, or, in some setups, injected into the page by JavaScript at runtime. The exact output shape depends on configuration, but the principle is the same: source styles in, browser-ready styles out.
Ordering matters for styles in a way that is different from JavaScript. In JavaScript, order is about definitions before uses, governed by the dependency graph. In styles, order is about the cascade: when two rules conflict, the later rule, or the more specific rule, wins, as covered in earlier chapters. A build tool that combines styles from many files must therefore preserve a sensible order, usually the order in which styles are imported, so that the cascade in the output matches the cascade the developer intended in source. If the build reorders styles arbitrarily, a page can look different in production from how it looked in development, even though no style rule itself changed. That kind of bug is frustrating, because the diff shows no change to any rule, only a change in order, and it is a reminder that bundling styles is not just concatenation. It is concatenation in the right order.
Styles also have their own optimisation opportunities. Unused styles can sometimes be removed, although detecting unused styles reliably is harder than detecting unused JavaScript, because a style might be applied by JavaScript at runtime, or by HTML that is generated dynamically, and removing it would break a feature that only appears in certain conditions. Duplicate rules can be merged, whitespace can be removed, and long names can be shortened in some setups, but each optimisation carries a risk if it changes the cascade or removes a rule that was actually needed. A cautious build setup therefore optimises styles conservatively, prioritising correctness over maximum size reduction, and it tests the built output visually, because a style optimisation that breaks a layout is a failure, however small the output becomes.
For a developer, the practical lesson is to treat styles as part of the build, not as an afterthought. Check that built styles load, that their order matches the intended cascade, and that the built page looks as the source page looked, by opening both and comparing. If a style works in development but not in production, the build's handling of styles, its ordering, its transformations, or its optimisations, is a likely culprit, and the built stylesheet, not just the source, deserves inspection. Styles are code, and build tools treat them as code, with dependencies, order, and output that must be checked like any other code.
Check: Why does the order of combined styles matter?
Section 10 — Handling images and other assets
A front-end site uses more than code and styles. It uses images, fonts, icons, videos, and other files, collectively called assets, and a build tool must handle them as part of packaging the site for delivery. In source, an asset might be referenced by a path, imported by a module, or linked from HTML, and it might exist in its original, high-quality form, suitable for editing but not necessarily for fast delivery. A build tool takes that reference, finds the asset, and decides what to do with it: copy it to the output folder, optimise it, rename it with a fingerprint for caching, or, for very small assets, embed it directly into code or styles so that no separate request is needed. The choice depends on the asset's size, its use, and the project's configuration.
Optimisation for images deserves particular attention, because images often account for a large share of a page's download size, and optimising them can improve loading more than any code optimisation. Optimisation can mean converting an image to a more efficient format, reducing its dimensions to match how it will actually be displayed, or compressing it in a way that preserves visual quality while reducing file size. Each of those steps involves a trade-off between quality and size, and the right balance depends on the image's role. A large hero image, displayed prominently, deserves higher quality than a small icon, which can be compressed more aggressively without anyone noticing. A build tool can apply sensible defaults, but a developer should check the built images visually, because an optimisation that makes an image look noticeably worse is a failure, however much size it saves.
Assets also raise questions of organisation and referencing. In source, an asset's path is relative to the file that references it, or to the project structure, but in output, the asset may live in a different folder, under a different name, especially if it has been fingerprinted for caching. The build tool must therefore rewrite references to point at the output location and name, so that the built page finds its assets correctly. If a reference is not rewritten, or is rewritten incorrectly, the built page will show a broken image or a missing font, even though the source page worked perfectly. Checking built pages for broken assets, by opening them and looking, is therefore an essential part of testing a build, not an optional extra.
Finally, assets should be treated as part of the project, under version control in their source form, and as part of the build output in their optimised form, not edited in output. If an image needs changing, change the source image and rebuild, so that the optimisation and fingerprinting are reapplied consistently. Editing a built image directly, like editing a built script directly, will be lost on the next build, and it creates a discrepancy between source and output that is hard to track. Source assets in, built assets out, and the build tool as the bridge between them, is the pattern to follow.
Check: Why should a build tool rewrite asset references in output?
Section 11 — Minification: smaller code, same behaviour
Minification is the process of removing unnecessary characters from code without changing what the code does. In JavaScript and styles, unnecessary characters include whitespace, line breaks, comments, and, in some cases, long variable names that can be safely shortened to shorter ones. A minified file looks like a dense block of text, hard for a human to read, but it behaves exactly like the original, because the characters removed were never part of the logic. They were there for human readability, and browsers do not need them to run the code. Removing them reduces file size, which reduces download time, which improves loading, especially on slower connections where every byte matters.
It is important to understand what minification does not do. It does not change logic, rename public interfaces, or remove code that is actually used. A function called from another file, or from HTML, must keep a name that callers can find, and a minifier that renamed it would break the site. Good minifiers therefore distinguish between internal names, which can be safely shortened because they are only used inside the bundle, and external names, which must be preserved because they are part of a public contract. Similarly, minification does not remove comments that carry meaning, such as licence notices, if the configuration preserves them, and it does not change the order of operations in a way that would alter behaviour. The goal is smaller, not different.
Minification is a production optimisation, not a development practice. During development, you want code that is readable, with meaningful names, comments, and whitespace, so you can debug it quickly. Minifying during development would slow debugging and hide errors, for no benefit, because development output is not served to visitors. In production, the trade-off reverses: visitors benefit from smaller files, and readability for humans matters less, because developers debug production output through source maps, covered later, which link minified output back to readable source. That division, readable in development, minified in production, is a standard build setup, and it reflects the different priorities of the two contexts.
Testing minified output is essential, because a minifier, like any tool, can expose a bug in source that was previously hidden. For example, code that relies on a particular whitespace behaviour, or on a function name that the minifier shortens, may work in development and fail after minification. Such failures are rare with well-tested minifiers, but they do happen, and they are a reason to build for production locally and test key features in the built output, rather than assuming that minification is always safe. Minification should make code smaller. If it makes code behave differently, the build configuration or the source needs attention, and the built output, not just the source, deserves inspection.
Check: What does minification remove, and what must it preserve?
Section 12 — Tree shaking: removing unused code
Tree shaking is a production optimisation that removes code that is never used, by analysing which exports from a module are actually imported and used, and discarding the rest from the bundle. The name comes from a metaphor: imagine the dependency graph as a tree, with the entry point as the trunk and modules as branches. Shaking the tree drops the dead leaves, the code that is not needed, while keeping the live branches that are. In practice, the bundler examines imports and exports, determines which exported values are reachable from the entry point through actual uses, and excludes exports that are never imported, or imported but never used, from the output. The result is a smaller bundle that contains only the code the app actually runs.
Tree shaking works best with modules that use named exports, because named exports make it clear, by name, which pieces are used and which are not. If a module exports several helpers by name, and the app imports only one of them, the bundler can see that the other helpers are unused and remove them, provided it can be sure that removing them has no side effects. That proviso matters. If a module's top-level code does work when it is imported, such as registering a handler or changing a global, removing the module entirely could change behaviour, even if none of its exports are used. Bundlers therefore treat modules with side effects cautiously, and they rely on configuration or on hints in the project to know which modules are safe to shake and which are not. A module that only defines functions and constants, without side effects, is the easiest to shake safely.
The benefit of tree shaking is most visible with libraries. A library might offer many features, but a typical app uses only a few of them. Without tree shaking, the bundle would include the whole library, including features the app never calls, wasting download time and parsing time. With tree shaking, the bundle includes only the features the app uses, which can make a substantial difference for large libraries. That benefit, however, depends on how the library is written and how the app imports it. Importing a whole library as a single object, rather than importing specific named pieces, can prevent tree shaking, because the bundler cannot tell which pieces are actually used. Precise imports, as advocated in the modules chapter, therefore pay off again in production, by making tree shaking possible.
For a developer, the lesson is to write modules in a shakeable way: use named exports for helpers, keep top-level side effects to a minimum, and import only what you use. Then, when you build for production, check whether the bundle is smaller than it would be without shaking, by comparing output sizes or by inspecting the bundle for code you know is unused. If unused code remains, the cause may be a side-effect import, a whole-library import, or a configuration issue, and investigating it will teach you how your bundler's shaking works in practice. Tree shaking is not automatic in the sense of working perfectly without thought. It is a collaboration between how you write modules and how the bundler analyses them.
Check: Why does tree shaking work better with named exports?
Section 13 — Code splitting: loading only what is needed
Code splitting is the practice of dividing a bundle into smaller pieces, called chunks, that can be loaded separately, so that a page loads only the code it needs at first, and loads additional code later, when it is actually needed. The alternative, a single bundle containing all code for all pages and all features, loads everything upfront, even code for pages the visitor never visits and features they never use. That upfront loading wastes time and bandwidth, especially for large sites, and it delays the moment when the page becomes usable, because the browser must download and parse all code before it can run any of it. Splitting breaks that all-or-nothing pattern into a more gradual, on-demand pattern.
Splitting can happen in several ways. One way is by page or route, where each page gets its own bundle, plus shared bundles for code used by many pages. A visitor to one page loads that page's bundle and the shared bundles, but not other pages' bundles. Another way is by feature, where code for a feature that is only needed after an interaction, such as opening a chart or entering edit mode, is split into a separate chunk and loaded dynamically when that interaction happens, using the dynamic import pattern from the modules chapter. A third way is by vendor, where third-party library code is split from app code, so that library code, which changes rarely, can be cached separately from app code, which changes more often. Each way serves a different purpose, and a project may use more than one.
The trade-off in splitting is between fewer, larger requests and more, smaller requests. Splitting too little leaves pages loading unnecessary code. Splitting too much creates many small requests, each with its own overhead, and a page that waits for many chunks can feel slower than a page that loads a few well-chosen bundles. The right balance depends on the project's size, its pages, its features, and how visitors actually use it. A small site with a few pages may need no splitting beyond a single bundle. A large site with many pages and features will benefit greatly from thoughtful splitting. The balance should be judged by loading measurements and by user experience, not by a rule about how many chunks a project should have.
For a beginner, the starting point is to understand splitting conceptually, and to use dynamic imports for clearly on-demand features, such as a chart that appears only on click. That single habit introduces splitting in a controlled, understandable way, where the split point is a feature boundary you chose, not an automatic division you do not understand. As you gain experience, you can explore page-based and vendor splitting, configured through the bundler, and you can measure their effects on loading. Splitting is a powerful optimisation, but it is also a complexity, and like all build complexities, it should be added when it solves a real loading problem, not before.
Check: When does splitting a bundle help, and when can it hurt?
Section 14 — Lazy loading and the build
Lazy loading is the strategy of delaying the loading of code, images, or other resources until they are actually needed, rather than loading everything upfront. In the context of build tools, lazy loading usually means code splitting combined with dynamic imports: a feature's code is placed in a separate chunk during the build, and that chunk is fetched only when the feature is requested, such as when a user clicks a button, scrolls to a section, or navigates to a page that needs it. The build tool's job is to create the chunk and to make the dynamic import work, by rewriting it to fetch the chunk's file from the output folder at runtime. The developer's job is to decide which features are lazy candidates and to handle the loading state gracefully.
Not every feature is a good lazy candidate. Code needed for the initial view, such as the main layout, the navigation, or the primary content, should load immediately, because delaying it would delay the page itself. Code for a secondary feature, such as a chart, a complex editor, or a rarely used settings panel, is a good candidate, because most visitors will not use it on a given visit, and those who do will accept a brief loading moment in exchange for a feature they explicitly requested. The decision should be based on how visitors actually use the site, not on a desire to make the initial bundle as small as possible at any cost. A page that loads quickly but then stutters every time a common feature is used has traded one poor experience for another.
Handling the loading state is part of lazy loading done well. When a chunk is being fetched, the page should show something sensible, such as a loading indicator, a placeholder, or a disabled state for the button that triggered the load, rather than appearing frozen or broken. If the fetch fails, because of a network problem or a missing chunk file, the page should show a helpful message and offer a way to retry, rather than leaving the user with a dead button and no explanation. Those handling details are not build tool concerns, in the sense that the build tool does not write them for you, but they are part of the feature's design, and a lazy feature without them is incomplete, however well the build tool splits its code.
Testing lazy loading requires testing both paths: the path where the feature is never used, to confirm that its code is not loaded upfront, and the path where it is used, to confirm that its code loads correctly, displays correctly, and handles failure gracefully. Browser network tools can show which chunks are loaded and when, which is a useful way to verify that splitting is working as intended. A chunk that loads upfront, when it was meant to be lazy, indicates a configuration or import problem, such as a static import where a dynamic import was intended. A chunk that fails to load on demand indicates a path or output problem, such as a chunk file that was not deployed or a reference that was not rewritten correctly. Both are build-related problems, and both are found by observing loading, not by reading source alone.
Check: What makes a feature a good candidate for lazy loading?
Section 15 — Development servers and live updates
A development server, introduced in the modules chapter as a way to serve files locally, becomes more powerful when combined with a build tool. Instead of serving source files directly, it serves built files, rebuilding them quickly whenever source changes, and updating the browser without requiring a manual refresh. The simplest form of this is live reload, where the browser refreshes the whole page when a file changes. A more refined form, often called hot replacement, updates only the changed module in the running page, preserving the page's state, such as form input or scroll position, while applying the new code. Both forms aim at the same goal: shortening the loop between editing code and seeing its effect, so that development feels immediate rather than stop-and-start.
The difference between reload and hot replacement matters for productivity. A full reload is simple and reliable, because it restarts the page from scratch, but it loses state, which can be annoying when testing a feature deep inside a workflow that requires several steps to reach. Hot replacement preserves state, which is valuable for testing those deep features, but it is more complex, because the tool must work out how to replace a module's code in a running page without breaking the modules that depend on it. Not every change can be hot-replaced cleanly, and in those cases the tool falls back to a full reload, which is the safe option. Understanding that fallback prevents confusion when a hot update suddenly becomes a reload: the tool is not broken, it is choosing reliability over state preservation for that particular change.
Development servers also handle other conveniences, such as serving the built output at a local address, showing build errors in the browser or in the terminal, and, in some setups, proxying requests to a separate data server during development, so that the front-end can be tested against realistic data without deploying a full backend. Those conveniences are not essential to understanding build tools, but they explain why developers prefer a build tool's development server to a simple static server, even for small projects. The development server is not just serving files. It is running the build continuously, in development mode, and feeding its output to the browser as fast as you can edit.
A caution for beginners is to remember that development server behaviour is not production behaviour. A page that works through a development server, with hot replacement and development-mode builds, may behave differently in production, where code is minified, split, and served without hot updates. That is why testing the production build, as emphasised earlier, remains essential, even when development feels smooth. The development server is a tool for writing code quickly. The production build is a tool for delivering code reliably. Both are part of a build setup, and both deserve attention, but they serve different purposes and should not be confused with each other.
Check: How does hot replacement differ from a full page reload?
Section 16 — Source maps: linking output back to source
When code is bundled, transpiled, or minified, the output looks very different from the source. A single output file may contain code from dozens of source files, with transformed syntax and shortened names, all on a few long lines. Debugging that output directly, by reading it and setting breakpoints in it, would be painful and often impossible, because the output's lines and names do not correspond to the source's lines and names. Source maps solve this problem by providing a mapping between output and source, so that browser developer tools can show you the original source, with its original names and line numbers, even while the browser is actually running the built output. You debug source, while the browser runs output, and the map bridges the two.
A source map is a separate file, or sometimes an inline comment, that records, for each part of the output, which source file it came from, which line and column in that source, and what the original name was, before minification or transformation. When you open developer tools on a built page and the browser finds a source map for a bundle, it uses that map to display the source files in its sources panel, to show stack traces in terms of source lines, and to let you set breakpoints in source, which the browser then translates to the corresponding output positions when running. That translation is what makes production debugging feasible, because without it, an error in minified output would point at a line that bears no resemblance to the code you wrote.
Source maps are most valuable in development, where they are usually generated in full detail, and in staging or testing environments, where built output is checked before deployment. In production, the decision to include source maps is more nuanced. Including them makes production debugging easier, because errors can be traced to source, but it also exposes source code to anyone who opens developer tools on the live site, which may be undesirable if the source contains comments or structure you prefer not to share, although, as noted in earlier chapters, front-end code is inherently visible to visitors in any case. Many projects therefore generate source maps for production but do not deploy them publicly, keeping them for internal debugging, or they deploy them with access controls. The choice depends on the project's needs, and it should be made deliberately, not by accident.
For a beginner, the key point is that source maps are a build output, configured like any other, and that their presence or absence explains why debugging sometimes shows readable source and sometimes shows minified output. If you open developer tools on a built page and see only minified code, check whether source maps were generated and whether the browser can find them. If you see readable source, the map is working, and you can debug as if you were running source directly, which is the intended experience. Source maps do not change what code does. They change how debuggable it is, and that change is essential for working with built output professionally.
Check: What problem does a source map solve?
Section 17 — Caching and fingerprints
Caching is the browser's habit of storing downloaded files locally, so that on a later visit it can reuse them instead of downloading them again. That habit makes repeat visits faster, because the browser does not need to fetch every file anew, but it creates a problem when a file changes. If a visitor's browser has cached an old version of a script, and the site deploys a new version under the same file name, the browser may keep serving the old version, and the visitor will see old behaviour, or a broken mix of old and new code, until the cache expires or is cleared. That stale-cache problem is a classic front-end bug, and it is frustrating because the site works correctly for new visitors, who have no cache, and incorrectly for returning visitors, who do.
Build tools solve this with fingerprints, sometimes called content hashes, which are short strings derived from a file's contents and included in its output file name. When a file's contents change, its fingerprint changes, and therefore its file name changes, so the browser treats the new file as a completely different file, fetches it fresh, and does not confuse it with the cached old version. When a file's contents do not change, its fingerprint stays the same, its file name stays the same, and the browser can safely reuse its cached copy, without downloading it again. Fingerprinting therefore gives the best of both worlds: long caching for unchanged files, and immediate freshness for changed files, without requiring visitors to clear their caches manually.
The fingerprint must be based on content, not on a random value or a build time, because a random fingerprint would change on every build, even when the file did not change, defeating the caching benefit. A content-based fingerprint changes only when the file's contents change, which is exactly the behaviour caching needs. It also means that a small change to one file changes only that file's fingerprint, while other files keep theirs, so returning visitors download only the changed files, not the whole site again. That selectivity is especially valuable when combined with code splitting, because a change to one feature's chunk changes only that chunk's fingerprint, while shared chunks and other features' chunks keep their cached versions.
For a developer, the practical implications are to let the build tool manage output file names, including fingerprints, and not to hard-code output file names in HTML or in other files that the build tool does not rewrite. The build tool, when it generates HTML or updates references, will use the fingerprinted names it produced, so that the built page points at the correct, current files. If you hard-code a name without a fingerprint, you bypass the caching benefit and risk stale-cache bugs. If you reference a fingerprinted name manually, you must update it on every build, which is error-prone. Let the tool own the names, check that built pages reference the fingerprinted files, and test a repeat visit, with a cache, to confirm that changed files update and unchanged files are reused. That test, more than any configuration detail, proves that caching is working as intended.
Check: How does a content-based file name prevent stale-cache problems?
Section 18 — Modes and environment differences
A build often needs to behave differently depending on where its output will run. A development build might talk to a local data server, show detailed error messages, and include debugging helpers. A production build might talk to the live data service, hide detailed errors from visitors, and omit debugging helpers. Those differences are environment differences, and build tools handle them through modes or environment settings, which are named configurations, such as development and production, that change how the build runs and what values are baked into the output. The same source, built in different modes, can therefore produce output that behaves differently in the ways that matter for each environment, without requiring separate source files for each environment.
Environment values are usually small pieces of configuration, such as the address of a data service, a feature flag that turns a new feature on or off, or a setting that controls logging. They are not secrets, and that distinction is critical. A value that is baked into front-end output, in any mode, is visible to anyone who opens the built files, as explained in earlier chapters about secrets. Environment settings in a build should therefore be limited to non-secret configuration, such as public addresses or feature flags, and secrets should remain on a server, never in built output. A build that bakes a secret into its output, even in a development mode, has created a secret exposure, because built files can be copied, shared, or accidentally deployed, and the secret will travel with them.
Configuration for environments should be explicit and reviewable. A build configuration that lists its modes and their values, in a file that is version-controlled, lets a reviewer see exactly what differs between development and production, and lets a future developer understand why a feature behaves differently in one environment from another. Hidden environment differences, such as values that come only from a developer's local machine and are not recorded anywhere, create a project that works on one machine and fails on another, for reasons that are hard to trace. Recording environment configuration, while keeping secrets out of it, is the balance to strike.
Testing across modes is the habit that makes environment differences safe. Build in development mode and test the features that depend on development settings. Build in production mode and test the same features against production settings, or against a staging environment that mirrors production. If a feature works in one mode but not the other, the difference is likely in the environment configuration, not in the feature's logic, and comparing the two builds' settings will reveal it. Modes are a powerful way to adapt one codebase to multiple contexts, but they are also a source of mode-specific bugs, and only testing both modes catches those bugs before visitors do.
Check: What kind of value should never be baked into built output, even in development mode?
Section 19 — Configuration: telling the tool what to do
A build tool needs instructions, and those instructions live in a configuration file, or in a set of configuration options, that describe the project to the tool. The configuration typically names the entry point or points, the output folder and file naming pattern, the transformations to apply, the plugins to use, and the mode-specific settings for development and production. Without configuration, a build tool would have to guess those things, and guesses would often be wrong, because projects differ in structure, in naming, and in needs. Configuration is how you tell the tool about your project, in a form it can follow reliably, every time it runs.
Good configuration is readable, commented, and as simple as the project allows. It should be possible for a new developer to open the configuration file and understand, at a glance, where the build starts, where its output goes, and what transformations it applies. Comments that explain why a particular setting exists, especially for settings that are not obvious, help future readers, including future you, understand decisions that would otherwise look arbitrary. A configuration file that is a copy of another project's configuration, with settings nobody understands and nobody dares to change, is a liability, because it will be wrong for this project in subtle ways, and no one will know how to fix it when it breaks.
Configuration also evolves with a project. A small project may need only a few settings, such as entry and output, while a larger project may need settings for splitting, for asset handling, for environment modes, and for plugins that handle specific file types. Adding settings one at a time, each for a named reason, keeps configuration understandable, in the same way that adding tools one at a time keeps a project understandable. Adding many settings at once, copied from a template or a tutorial without understanding them, creates a configuration that is hard to debug, because when the build fails, no one knows which setting caused the failure or whether a setting is even needed.
When a build fails or behaves unexpectedly, the configuration file is one of the first places to check, after the error message itself. Does the entry point match the actual main file? Does the output folder match where you are looking for output? Are transformations applied in the intended order, and are plugins listed that you actually need? Answering those questions, by reading the configuration, often reveals the problem faster than changing code at random. Configuration is not an advanced extra. It is the project's instructions to its build tool, and like any instructions, it deserves to be written clearly, read carefully, and updated whenever the project changes. A configuration that is clear today will still be clear after a break, when you return to the project and need to understand it quickly.
Check: What does a build configuration file tell the tool?
Section 20 — Plugins: extending a build tool
No single build tool can handle every kind of file, every transformation, and every project need out of the box, because projects vary too widely. Plugins solve this by extending a build tool with additional capabilities, each plugin handling a specific job, such as transforming a particular file type, optimising a particular asset, or adding a particular check to the build. A plugin is, in effect, a small tool that plugs into the larger build tool, runs at a defined point in the build pipeline, and does its job on the files passing through. By combining a core tool with a set of plugins, a project can build a pipeline tailored to its exact needs, without needing a different core tool for every need.
Plugins are powerful, but they also add complexity and risk, and they should be chosen with the same care as libraries. Each plugin is code written by someone else, running as part of your build, with access to your source files and, potentially, to your environment. A plugin that is unmaintained, poorly documented, or overly broad in what it does can break a build, slow it down, or introduce unexpected behaviour that is hard to trace, because the build's output is now the result of many plugins interacting, not just the core tool's actions. Before adding a plugin, ask whether its job is truly needed, whether the core tool already does it, and whether the plugin is well-maintained and well-documented. If the job is not needed, or the core tool already does it, adding the plugin only adds a dependency to maintain.
The order of plugins matters, because the build pipeline runs them in sequence, and the output of one plugin becomes the input of the next. A plugin that transforms syntax should usually run before a plugin that minifies, because minifying transformed code is safer than transforming minified code. A plugin that handles styles should run at the point where styles are processed, not after the bundle is written. Configuration usually lists plugins in order, and that order is part of the project's build design, not an arbitrary list. When a build produces unexpected output, checking plugin order, and whether a plugin is running at all, is a useful diagnostic step, alongside checking the core configuration.
For a beginner, the practical approach is to start with the core tool's built-in capabilities, add a plugin only when a specific need arises that the core cannot meet, and document why the plugin was added, in a comment or in the project's documentation. That documentation, a single sentence per plugin, such as "this plugin handles image optimisation for hero images", helps future readers understand the pipeline without reading every plugin's documentation. Plugins extend a build tool, but they do not replace understanding of it. A pipeline you understand, even if it is simple, is better than a pipeline that does more but that nobody can explain or fix when it breaks.
Check: When should you add a plugin to a build, and when should you not?
Section 21 — Transforms and loaders: handling different file types
A bundler's core job is handling JavaScript modules, but a real project contains many file types, and each type needs different handling before it can become part of a bundle or an output file. Transforms and loaders are the mechanisms that handle those types. A loader, in some tools, is a small program that takes a file of a particular type, such as a stylesheet, an image, or a file written in a syntax that needs converting, and returns something the bundler can use, such as JavaScript code that represents the file, or a reference to a copied output file. A transform is a similar idea, expressed as a step that converts a file from one form to another, before bundling continues. The names vary by tool, but the idea is the same: teach the bundler how to handle each file type it encounters, instead of assuming everything is JavaScript.
Without loaders or transforms, a bundler would fail on the first non-JavaScript import, because it would not know what to do with a stylesheet or an image. With them, an import of a stylesheet can become a stylesheet file in the output, linked from the built page, or, in some setups, code that injects the styles at runtime. An import of an image can become a copied, optimised image file in the output, with the import rewritten to point at it. An import of a file written in a newer syntax can become converted code that the bundler can then combine normally. Each loader or transform is therefore a bridge between a source file type and the bundler's world, and a project with many file types will have many bridges, each configured for its type.
Configuring loaders involves telling the tool which files each loader handles, usually by file extension or by a pattern, and what options that loader should use. A stylesheet loader might need to know whether to extract styles to a separate file or to inject them. An image loader might need to know a size threshold, above which images are copied as files and below which they are embedded. A syntax transform might need to know the target level of support, as discussed in the transpiling section. Those options are part of the build configuration, and they should be set deliberately, with an understanding of what each loader does, rather than copied blindly from an example that may have different needs.
Debugging loader problems often starts with the error message, which usually names the file that failed and the loader that tried to handle it, or the absence of a loader for that file type. If a build fails on a stylesheet import, the likely cause is a missing or misconfigured stylesheet loader. If an image appears broken in built output, the likely cause is an image loader that copied the file to an unexpected location or that failed to rewrite the reference correctly. Checking loaders, their patterns, and their options, in that order, resolves most file-type problems, and it teaches you how the bundler sees your project: not as a folder of files, but as a graph of typed files, each needing the right handler to become part of the output.
Check: Why does a bundler need loaders for non-JavaScript files?
Section 22 — Task runners versus bundlers
Task runners and bundlers are both build tools, but they solve different problems, and confusing them leads to choosing the wrong tool or to using one tool for a job it was not designed for. A task runner automates a sequence of tasks, such as copying files, optimising images, compiling styles, and starting a server, by running a list of steps in order, each step being a small program or script. It does not, by itself, understand imports or build a dependency graph. It runs tasks you define, in the order you define them, and it is good for projects where the build is a series of independent steps that do not need to be combined into bundles. A bundler, by contrast, understands imports, builds a graph, and combines code into bundles, as covered throughout this chapter. It is good for projects where modules and their dependencies are the central structure.
In practice, many projects use both, or use a bundler that also performs task-like steps through plugins. A bundler might optimise images as part of its pipeline, which is a task-runner-like job, while a task runner might invoke a bundler as one of its steps, to produce bundles as part of a larger sequence that also copies assets and deploys output. The distinction is therefore about primary purpose, not about exclusive capabilities. A task runner's primary purpose is orchestration: running steps in order. A bundler's primary purpose is packaging: following dependencies and producing bundles. Knowing a tool's primary purpose helps you judge whether it is the right main tool for your project, and whether its other capabilities are sufficient or whether a second tool is needed for the remaining jobs.
For a small project with a few tasks and no complex module graph, a task runner, or even a set of scripts, may be sufficient and simpler than a bundler. For a project with many modules, libraries, and dynamic imports, a bundler is the right main tool, because its graph understanding solves problems that a task runner would need to solve by hand, with fragile ordering and manual file lists. For a project in between, the choice depends on which problem is more central: orchestrating diverse tasks, or packaging modules. Starting with the simpler tool that solves the central problem, and adding the other only when its problem becomes real, follows the same principle as adding tools generally: add for a named reason, not for completeness.
Historically, task runners were more prominent when front-end builds were mainly about compiling styles, optimising images, and concatenating scripts by hand, before modules and bundlers became standard. As modules became the norm, bundlers became the central build tool for JavaScript-heavy projects, while task runners remained useful for projects where orchestration is the main need. Understanding that history, in conceptual terms without needing dates or versions, helps explain why tutorials from different eras recommend different tools: they were solving different central problems, at different times, with different project structures in mind. Your choice should be based on your project's central problem today, not on a tutorial's era.
Check: What is the main difference between a task runner and a bundler?
Section 23 — Choosing a bundler: questions, not brands
Choosing a build tool can feel overwhelming, because there are many tools, each with its own fans, documentation, and examples, and each claiming to be fast, simple, or powerful. A useful way to choose is to ask questions about your project, rather than starting from brand loyalty or from a tutorial's recommendation. The questions should cover what your project actually needs, what your team already knows, and what constraints you face, because a tool that is perfect for one project may be a poor fit for another, even if both are front-end projects. Brand names change in popularity over time, but the questions remain stable, because they are about your project, not about fashion.
Start with the project's structure. How many entry points does it have? How many modules, and how are they organised? Does it use libraries that need resolving from bare imports? Does it need transpiling, style handling, image optimisation, or code splitting, or would a simple bundling setup suffice? A project that needs only simple bundling should not adopt a complex tool with many features it will never use, because complexity without need is a maintenance cost. A project that needs many of those features should not adopt a minimal tool that will require many plugins and workarounds to meet its needs, because workarounds are also a maintenance cost. Match the tool's capabilities to the project's needs, as closely as you can, without over- or under-shooting by a large margin.
Next, consider the team. What tools does the team already know, and how much time is available to learn a new one? A tool the team already understands will be configured correctly, debugged quickly, and maintained well, while a tool nobody understands, however powerful, will be configured by copying examples, debugged by searching for answers, and maintained with fear. Documentation quality matters here too. A tool with clear, complete, beginner-friendly documentation is easier to learn and to troubleshoot than a tool whose documentation assumes deep prior knowledge, even if the second tool is technically more capable. The team's learning curve is a real cost, and it should be counted in the choice.
Finally, consider constraints such as deployment, performance goals, and long-term maintenance. Does the tool's output work with your hosting setup? Does it support the caching, splitting, and optimisation strategies your performance goals require? Is it actively maintained, with a community that answers questions and fixes problems, so that you are not stranded on an abandoned tool? Those questions do not have a single right answer for all projects, but answering them honestly, for your project, will narrow the choice to a small set of sensible options, from which you can pick one, try it on a small part of the project, and evaluate it by whether it makes building easier and output better, not by whether it is popular. A tool chosen by questions is a tool you can defend and maintain. A tool chosen by fashion is a tool you may regret when fashion changes.
Check: What three kinds of questions should guide choosing a build tool?
Section 24 — When no build is needed
Not every front-end project needs a build step. A small page with a few modules, a stylesheet, and a handful of images, served through a local server during development and deployed as static files, can work perfectly well without any bundling, transpiling, or optimisation. The browser can load its modules directly, apply its styles directly, and display its images directly, without a tool intervening. Adding a build step to such a project would add configuration to write, tools to install, errors to debug, and output to check, without making the page noticeably better for visitors, because the page is already small and simple. Simplicity is a virtue, and a project that does not need a build should not adopt one out of a feeling that serious projects must have one.
The signs that no build is needed are clear. The project has few files, so loading them separately is not a burden. It uses only language features and styles that the target browsers support directly, so no transpiling or preprocessing is required. It does not use bare imports, so no resolution step is needed, because all imports are relative paths to real files. It does not need code splitting, because there is no large, rarely used feature to split off. It does not need fingerprinting for caching, because its files change infrequently or its hosting handles caching sensibly without it. When all those signs point to simplicity, the honest conclusion is that a build would be overhead, not help, and the project should stay build-free until one of those signs changes.
Staying build-free does not mean staying tool-free. A local server is still needed for modules, as covered earlier. A linter and a formatter can still help, because they work on source without producing built output. Version control is still essential, because it tracks source history regardless of whether a build exists. Those tools support development without adding a build step, and they should not be confused with build tools. The distinction is between tools that help you write source and tools that transform source into output. The first kind is useful even for tiny projects. The second kind becomes useful when transformation solves a real problem.
Revisit the decision as the project grows. A page that starts simple may gain many modules, libraries, and features, until loading separately becomes slow, or bare imports become necessary, or transpiling becomes desirable. At that point, adding a build is no longer overhead. It is the solution to a problem that has actually appeared, and the reason for adding it will be clear to everyone, because the problem is visible in loading times or in failed imports. Adding a build then, for a named reason, is far better than adding it early, for a hypothetical reason, and then maintaining it through a period when it provided no benefit. Start without a build when you can, and add one when you must, with a reason you can state in one sentence.
Check: When is it fine to have no build step at all?
Section 25 — When a build becomes worth it
A build becomes worth it when it solves a problem that is actually hurting the project, not when it merely could solve a problem in theory. The most common such problem is loading performance. As a project grows to many modules, libraries, and assets, loading each file separately becomes slow, and visitors notice. Bundling, minification, and image optimisation, applied together, can make a tangible difference to how quickly a page becomes usable, and that difference is worth the complexity of a build, because it directly affects user experience. If loading measurements show that a page is slow because of many requests or large files, a build that addresses those causes is not overhead. It is a fix.
Another problem that makes a build worth it is the need for transformations that browsers cannot do on their own. If a project wants to use newer language features, or a style preprocessing approach, or bare imports for libraries, those choices require a build step to translate them into forms browsers can use. Without a build, the project must avoid those features, which may mean writing more verbose code, managing library files by hand, or structuring styles in a less maintainable way. With a build, the project can use the conveniences that make development easier, because the build translates them for delivery. The build's cost is justified by the development benefit, provided the conveniences are actually used and actually help, not merely adopted because they are fashionable.
A third problem is scale and team workflow. As a team grows, or as a project gains multiple pages, features, and environments, the need for consistent, repeatable packaging grows with it. A build that runs the same way on every developer's machine, and in deployment, ensures that what one developer tests is what another developer tests, and what visitors receive. Without a build, packaging becomes manual and inconsistent, with different developers producing different output by hand, or with deployment depending on who happened to package the site last. A build automates packaging, making it repeatable and reviewable, which is essential for team reliability, even if a solo developer on a small project could manage without it.
The decision to add a build should be made explicitly, with the problem stated and the expected benefit noted, so that later, if the build becomes a burden, the team can judge whether it is still solving the problem it was added for. A build added for loading performance should be judged by loading measurements. A build added for transformations should be judged by whether those transformations are still needed and still help. A build added for team consistency should be judged by whether it actually makes packaging more consistent. If a build no longer solves its original problem, or if its problem disappears, simplifying or removing the build is a reasonable option, not a failure. Tools serve projects, and when a tool stops serving, it should be reconsidered, however standard it may seem.
Check: Name three problems that make a build step worth its complexity.
Section 26 — The output folder: generated, not edited
The output folder is where a build tool writes its built files, and it should be treated as generated output, not as source. That means you do not edit files in it by hand, you do not commit it to version control, unless your deployment process specifically requires it, and you do not rely on it containing anything that cannot be rebuilt from source. The output folder is a product of the build, in the same way that a printed book is a product of its manuscript. Editing the printed book does not change the manuscript, and the next printing will overwrite your edit. Editing built output does not change source, and the next build will overwrite your edit, with the same frustrating result.
Not committing the output folder follows from the same logic. Version control should track source, which is human work, and configuration, which describes how to build, because those are the inputs that determine output. Tracking output as well would duplicate information, because output can be derived from source and configuration at any time, and it would clutter history with large, generated diffs on every build, even when source changed only slightly. A new developer cloning the project does not need the output folder, because they can run the build themselves and produce it. A deployment process does not need it from version control, because it can run the build as part of deployment, producing fresh output from the current source. In both cases, building from source is cleaner and more reliable than copying a committed output folder.
There are exceptions, and they should be deliberate. A project deployed to a static host that cannot run a build might need to commit or upload built output, because the host serves files as they are, without building them. In that case, the output folder is the deployment artefact, and it should be produced by a clean build, checked, and then deployed, with the understanding that it is still generated, not source, and that any fix should be made in source and rebuilt, not patched in output. The exception does not change the rule. It changes where the build runs, not whether output should be edited by hand.
Practically, this means keeping the output folder in the ignore file, so that status does not show it as untracked and broad staging commands do not add it accidentally. It also means cleaning the output folder occasionally, or letting the build tool clean it before each build, so that stale files from earlier builds, such as chunks that are no longer produced, do not linger and get deployed by mistake. A clean build, from clean source, to a clean output folder, is the most reliable way to produce output, because it eliminates the risk that an old file, left over from a previous configuration, will be mistaken for current output. Generated means rebuildable, and rebuildable means you should never be afraid to delete output and build again.
Check: Why should you not edit files in the output folder by hand?
Section 27 — Debugging a production build
Debugging a production build is different from debugging source, because the code running in the browser is transformed output, not the code you wrote. An error in production may point at a minified file, a bundled file, or a chunk file, with line numbers and names that do not match your source. Source maps, covered earlier, are the first tool for bridging that gap, because they let developer tools show you source while running output. If source maps are available, use them to find the source line that corresponds to the production error, and start your investigation there, as you would for a development error. If source maps are not available, you will need to work from the output itself, which is harder, and that difficulty is a reason to ensure source maps are generated for environments where debugging may be needed.
A production-only bug, one that appears in built output but not in development, deserves a particular kind of investigation. Start by asking what production transformations could cause it. Did minification rename something that should not have been renamed? Did tree shaking remove code that was actually used, perhaps through a side effect or a dynamic reference that the bundler could not see? Did code splitting load a chunk in the wrong order, or fail to load a chunk at all? Did a style optimisation change the cascade, or remove a rule that was needed at runtime? Each of those questions points at a different part of the build, and checking them in order, by inspecting the built output and the build configuration, often reveals the cause faster than changing source at random.
Reproducing a production bug locally is the next step. Build for production on your own machine, using the same configuration and mode as the deployed build, and open the built output through a local server, as you would for a deployed site. If the bug reproduces locally, you can debug it with developer tools, source maps, and the ability to rebuild quickly after each attempted fix. If it does not reproduce locally, the difference may be in environment settings, in deployed files that differ from local output, or in data that differs between environments, and those differences become the focus of investigation. A bug that cannot be reproduced is hard to fix, so making it reproducible, by matching the production build locally, is a valuable first goal.
Finally, remember that a production bug is still a bug in source or in configuration, not a mysterious property of production itself. The build does not create logic out of nothing. It transforms, combines, and optimises logic that already exists in source, or it applies configuration that you chose. Finding the source or configuration change that, when combined with production transformations, produces the bug, and fixing it there, is the correct approach. Patching built output directly, even if it fixes the immediate symptom, will be lost on the next build and will hide the real cause, which will reappear. Fix source, fix configuration, rebuild, and verify in production mode. That loop, repeated patiently, resolves production bugs reliably.
Check: What is the first thing to check when a bug appears only in production?
Section 28 — Reading build errors
Build errors are messages from the build tool explaining why it could not produce output, and reading them carefully is the fastest way to fix a failed build. A build error usually names the file that caused the problem, the kind of problem, and sometimes the line or the import involved. For example, it might say that a module could not be found, that a file type has no handler, that syntax could not be parsed, or that a configuration option is invalid. Each kind of error points at a different fix, and guessing between them wastes time. Read the message literally, note the file and the import it names, and check those first, before changing unrelated code or configuration.
Missing module errors are among the most common. They occur when an import points at a path that does not exist, at a bare name that the bundler cannot resolve, or at a file that was moved or renamed without updating its importers. The fix is to check the import path, starting from the importing file, as with browser module errors, and to check whether the library is actually installed, if the import is bare. A missing file error can also occur when a file is referenced from HTML or from styles, but not from JavaScript, because the bundler may only follow JavaScript imports by default, and other references may need separate handling. The error message usually makes clear which reference failed, and following that reference to its source reveals whether the file is missing, misnamed, or in an unexpected location.
Syntax and transformation errors occur when a file contains code the build tool cannot parse, or a transformation it cannot apply. The cause might be a typo, a feature the configured target does not support, or a file type that needs a loader that is not configured. The fix is to open the named file, check the named line, and ask whether the code is valid and whether the tool is configured to handle that file type. Configuration errors occur when an option is misspelled, a plugin is missing, or an entry point or output path is wrong. The fix is to read the configuration file, compare it with the tool's documentation for the options you are using, and correct the misspelling or the path.
A useful habit is to fix build errors one at a time, in the order they appear, rebuilding after each fix. Build tools often report many errors that all stem from a single root cause, such as a missing file that many other files import, and fixing the root cause clears the rest. Fixing errors out of order, or changing several things at once, makes it hard to tell which change fixed which error, and it can introduce new errors that mask the original. Work through errors in order, fix one, rebuild, and repeat. That patient, ordered approach turns build errors from a wall of messages into a checklist, and a checklist is a problem you can solve.
Check: A build fails with a missing module error. What do you check first?
Section 29 — Keeping builds fast
As a project grows, builds can become slow, and slow builds slow development, because every change requires waiting for a rebuild before it can be tested. Keeping builds fast is therefore a practical concern, not just a performance nicety, and it involves understanding what makes a build slow and what can be done about it, without sacrificing correctness. The most common cause of slow builds is doing unnecessary work: rebuilding everything, from scratch, on every change, even when only one file changed. A build tool that caches intermediate results, and reuses them for unchanged files, can rebuild only what changed, which is far faster than rebuilding the whole project for a small edit. That incremental approach, where the tool remembers previous results and reuses them where possible, is the single most effective way to keep development builds fast.
Another cause of slow builds is processing files that do not need processing. A build that transforms every file in the project, including files that are never imported, or that optimises images on every development rebuild, even though images rarely change, wastes time on work that does not affect the current test. Configuring the build to process only what the entry points need, and to optimise expensive assets only in production mode, not on every development rebuild, removes that waste. Similarly, a build that includes large libraries or large assets in every development rebuild, when those libraries and assets rarely change, can benefit from splitting them into separate, cached chunks, so that development rebuilds focus on app code, which changes frequently, while library code is reused from cache.
Measuring build speed, before optimising it, is essential, because guesses about what is slow are often wrong. Most build tools report how long a build took, and some report how long each step or each file took, which lets you identify the bottleneck, whether it is a particular transformation, a particular plugin, or a particular large file. Optimising a step that is not the bottleneck wastes effort and can introduce new problems, while optimising the actual bottleneck, even slightly, can make a noticeable difference. Measure first, optimise the bottleneck, measure again, and stop when builds are fast enough for comfortable development, rather than pursuing maximum speed for its own sake.
Finally, remember that build speed and output quality are sometimes in tension. A production build that applies many optimisations will naturally take longer than a development build that skips them, and that is acceptable, because production builds run infrequently and their output is used many times. A development build should prioritise speed and debuggability, even if its output is larger or less optimised, because it runs frequently and its output is used only for testing. Keeping those priorities separate, by configuring development and production builds differently, is the simplest way to keep development fast without sacrificing production quality. Fast development builds and thorough production builds are not contradictory goals. They are different modes, each optimised for its purpose.
Check: What is the most effective way to keep development rebuilds fast?
Section 30 — How bundles handle dependencies
When a project uses a library, that library becomes a dependency, and the bundler must decide how to include it in the output. The simplest approach is to bundle the library's code directly into the app's bundle, so that the browser loads one file containing both app code and library code. That approach is straightforward and reliable, because the library code travels with the app code, and there is no separate request or version mismatch to worry about. It is the default for many projects, especially smaller ones, where the library code is not large and where the simplicity of a single bundle outweighs the benefits of separating library code.
A more refined approach is to split library code from app code, as mentioned in the code splitting section. Library code changes rarely, because libraries update less often than app code, and it is often shared by many pages, so placing it in a separate, long-cached chunk means that returning visitors, who already have the library chunk cached, need only download the changed app chunk on a later visit, not the whole bundle again. That caching benefit is valuable for large libraries and for sites with frequent app updates, but it adds complexity, because the build must decide which code counts as library code, how to split it, and how to ensure that the app chunk and the library chunk load in the correct order and version. That complexity should be justified by measured caching benefits, not assumed.
Dependencies also have their own dependencies, which the bundler must resolve. A library may import other libraries, or parts of libraries, and the bundler follows those imports just as it follows app imports, building a complete graph that includes transitive dependencies, which are dependencies of dependencies. That completeness is essential, because an app that uses a library feature that itself uses another library will fail if the second library is missing. The bundler's graph, built from all imports, ensures that transitive dependencies are included, and its warnings can reveal cases where two versions of the same transitive dependency are pulled in through different paths, which can cause duplication or version conflicts that need resolving through configuration.
For a developer, the practical lesson is to understand that dependencies are not magic. They are code, with imports, like your own code, and the bundler treats them the same way, following their imports and including their reachable code. When a library does not work as expected in a bundle, check whether its code was included, whether its dependencies were included, and whether a version conflict or a side-effect issue, as discussed in earlier sections, is interfering. Dependencies make development faster by reusing others' work, but they also make the bundle's graph larger and more complex, and understanding that graph is part of understanding your built output.
Check: Why might splitting library code from app code help caching?
Section 31 — CSS bundling pitfalls
Bundling styles seems straightforward, just combine the files, but it contains pitfalls that are easy to overlook and hard to debug, because they manifest as visual differences, not as errors. The first pitfall is order, already discussed, but worth repeating in the context of bundling specifically. When styles from many files are combined, the order in which they appear in the output determines the cascade, and therefore which rule wins when rules conflict. If the bundler orders files alphabetically, or by discovery order, rather than by import order, the output cascade may differ from the source cascade, and a page that looked correct in development may look wrong in production. Checking built styles' order, by inspecting the output stylesheet, is a necessary step when a built page looks different from its source page.
The second pitfall is global scope. Unlike JavaScript modules, which have their own scope, styles are global by default: a rule for a heading, or for a class, applies to every matching element on the page, regardless of which file or feature defined it. When styles from many features are bundled into one file, a rule from one feature can unintentionally affect another feature, if both use the same class name or the same element selector. That collision is not a bundler bug. It is a consequence of styles being global, made more visible by bundling, because bundling brings all styles into one place where collisions become apparent. The solution is in source, not in the bundler: use distinctive class names, scope styles to their feature's container where possible, and avoid broad element selectors that affect the whole page unless that is truly intended.
The third pitfall is unused styles, which are harder to remove safely than unused JavaScript, as noted earlier. A style might appear unused because no HTML file in the project references its class, but it might be applied by JavaScript at runtime, or by HTML generated dynamically from data, or by a page that is not part of the current build's entry points. Removing it based on a static analysis of source files would break the feature that uses it dynamically, and the breakage might only appear in a specific user flow, making it hard to catch in testing. For that reason, style optimisation should be conservative, and any removal of styles should be tested across the site's features, not just on the pages that were checked during development.
A related pitfall is duplication. When the same style file is imported by multiple entry points, a bundler might include it multiple times, once per entry point, unless it is configured to share it. Duplication wastes download size and can cause cascade confusion, because the same rule appears twice in the output, and later copies override earlier ones in ways that are hard to predict. Checking built output for duplicated rules, and configuring the bundler to deduplicate shared styles, are part of maintaining a clean style bundle. Styles are not a secondary concern in a build. They are a first-class part of the output, with their own ordering, scoping, and optimisation challenges, and they deserve the same attention as JavaScript when a build is configured and tested.
Check: Why can bundling styles cause a rule from one feature to affect another?
Section 32 — Working with third-party code in a build
Third-party code, code written by others and used as a library, enters a build through dependencies, and it brings both benefits and challenges. The benefit is reuse: a library solves a problem that many projects share, such as drawing charts, handling dates, or managing complex interactions, and using it saves the time and risk of writing that solution from scratch. The challenge is that third-party code was not written for your project specifically. It may assume a particular environment, use a particular module format, rely on side effects, or include code that your project does not need. A build tool must handle those assumptions, and a developer must check that it does so correctly, rather than assuming that any library will bundle cleanly just because it is popular.
Module format is a common source of friction. Some libraries are written as modules, with named exports that tree shaking can analyse. Others are written in older formats, where the whole library is a single object, or where exports are not clearly named. A bundler can usually handle both, by converting older formats into a form it can bundle, but the conversion may prevent tree shaking, because the bundler cannot tell which parts of the library are used and which are not. The result is a larger bundle than expected, containing the whole library even though the app uses only a small part of it. Understanding a library's format, by reading its documentation or its package information, helps you predict whether tree shaking will work and whether the bundle size is reasonable for the features you actually use.
Side effects are another friction point. A library might set up global state when it is imported, or it might expect to be imported for its side effects, rather than for a value it exports. A bundler that shakes aggressively, removing imports it thinks are unused, might remove a side-effect import that was actually needed, breaking the library's setup. Configuration can mark a library, or a particular file, as having side effects, so that the bundler preserves it, but that configuration must be set deliberately, based on knowledge of the library, not guessed. When a library works in development, where shaking may be disabled, but fails in production, where shaking is enabled, a side-effect issue is a likely cause, and checking the library's side-effect behaviour is a useful diagnostic step.
Finally, third-party code should be evaluated, not just installed. Before adding a library, check its documentation, its maintenance, its size, and whether it actually solves your problem without adding new problems, such as a large bundle size for a small feature, or a dependency on many other libraries that you do not need. A build tool makes using libraries easy, but ease of installation is not a reason to install. Each library added to a project becomes part of its bundle, its graph, and its maintenance burden, and it should earn that place by solving a real problem well. Choose libraries deliberately, understand how they bundle, and test their behaviour in built output, not just in source, to ensure that third-party code helps your project rather than complicating it.
Check: Why might a library work in development but fail after production tree shaking?
Section 33 — Splitting shared code
Shared code is code used by many pages or many features, such as a formatting helper, a data fetching helper, or a common layout component. When a project has multiple bundles, one per page or per feature, shared code presents a choice: include a copy in each bundle that uses it, or place it in a separate shared bundle that all bundles load. Including a copy in each bundle is simple, because each bundle is self-contained, but it duplicates code, increasing total download size for visitors who visit multiple pages, because they download the same helper again with each page's bundle. Placing shared code in a separate bundle avoids duplication, because visitors download the shared bundle once and reuse its cached copy on later pages, but it adds a dependency between bundles, because each page's bundle now requires the shared bundle to be loaded first, in the correct order.
The bundler can often make this decision automatically, by analysing which modules are used by multiple entry points and extracting them into a shared chunk. That automatic extraction is convenient, but it should be understood, not blindly trusted, because its result depends on configuration, such as thresholds for how many entry points must share a module before it is extracted, and on the project's structure, such as whether shared code is genuinely shared or only coincidentally used by two pages for different reasons. A module used by two pages for unrelated reasons might be extracted into a shared chunk that both pages load, even though each page uses it differently, which is still correct, but which may not be the most efficient arrangement if the shared chunk is large and each page uses only a small part of it.
Manual control over shared code is also possible, by organising source so that shared helpers live in a clearly marked shared folder, and by configuring the bundler to treat that folder's code as shared. That organisation makes the sharing explicit in source, not just in build output, which helps developers understand which code is shared and which is page-specific. It also helps with maintenance, because a change to shared code is understood to affect all pages that use it, and it should be tested across those pages, not just on the page where the change was made. Shared code is a commitment to multiple consumers, and changing it requires the same care as changing a public interface.
For a beginner, the key insight is that sharing is a trade-off, not a pure win. Sharing reduces duplication and improves caching for repeat visitors, but it creates a dependency that must be loaded correctly and a change that affects multiple pages. Duplication, by contrast, is wasteful for repeat visitors, but it keeps each bundle independent and each page's change isolated. The right balance depends on how visitors use the site, whether they visit many pages in one session or typically visit one, and on how often shared code changes. Measure, or at least reason about, visitor behaviour, and let that reasoning guide whether shared code is split or duplicated, rather than adopting a default without understanding its consequences.
Check: What is the trade-off when shared code is placed in its own bundle?
Section 34 — Preprocessing styles
Preprocessing styles means writing styles in a form that is more convenient for developers, and then converting that form into standard styles that browsers can use directly, as part of the build. The convenient form might offer features that standard styles do not, such as variables for colours and sizes, nesting to show hierarchy, or reusable blocks for common patterns, such as a button style used in many places. Those features make styles easier to write, easier to maintain, and easier to change consistently, because a colour used in many places can be defined once as a variable and changed in one place, rather than searched for and replaced in many files. The build step converts the convenient form into standard styles, so that browsers, which understand only standard styles, can apply them.
The benefit of preprocessing is maintainability, especially for large style codebases. A project with many pages, many components, and a design system with consistent colours, spacing, and typography, benefits greatly from variables and reusable blocks, because they enforce consistency and make global changes safe. A project with a small stylesheet, on the other hand, may not benefit enough to justify the added build step, the new syntax to learn, and the extra tool to configure and maintain. As with other build features, preprocessing should be adopted when it solves a real maintainability problem, not because it is fashionable or because a tutorial uses it. A small stylesheet written directly in standard styles, well organised, is often clearer than a small stylesheet written in a preprocessed form that adds a layer of indirection for little gain.
Preprocessing also introduces a debugging consideration. When a style problem appears in the browser, developer tools will show the built, standard styles, not the preprocessed source, unless source maps for styles are generated and available. Without those maps, tracing a built rule back to its preprocessed source, to the variable or block that produced it, requires searching the source by hand, which is slower than debugging standard styles directly. With source maps, the trace is automatic, and debugging feels as if you were working with source directly. Configuring style source maps, for development at least, is therefore an important part of adopting preprocessing, because it preserves the debuggability that preprocessing might otherwise obscure.
Finally, preprocessing does not change the fundamental rules of styles, such as the cascade, specificity, and the box model. It only provides a more convenient way to write styles that follow those rules. A preprocessed stylesheet that produces a confusing cascade, or overly specific selectors, will still produce confusing, hard-to-maintain output, however convenient its source looked. Preprocessing is a writing aid, not a substitute for understanding styles. Learn standard styles well first, use preprocessing to make well-understood styles easier to maintain, and check built output to ensure that the convenience of the source has not hidden a problem in the output.
Check: What benefit does style preprocessing offer, and what does it not change?
Section 35 — Static assets and the public folder
Not every file in a project should go through the build pipeline. Some files are already in their final form, ready for browsers to use directly, and transforming them would add no value and might even break them. Examples include a site icon, a manifest file for installation, a plain text file for verification, or an image that is already optimised and referenced by a fixed path. For those files, build tools often provide a public folder, sometimes called a static folder, whose contents are copied directly to the output folder, unchanged, during the build, without transformation, optimisation, or fingerprinting. Files in the public folder keep their names and paths, so that references to them, from HTML or from other files, continue to work in output exactly as they worked in source.
The public folder is useful, but it should be used deliberately, not as a dumping ground for files that should have gone through the pipeline. A file placed in the public folder bypasses optimisation, fingerprinting, and reference rewriting, which means it will not benefit from caching fingerprints, it will not be optimised, and its reference will not be updated if the output structure changes. For files that benefit from those pipeline features, such as images that should be optimised and fingerprinted, importing them through JavaScript or styles, so that the bundler handles them, is the better approach. The public folder is for files that genuinely need a fixed path or that are already final, not for files that could benefit from pipeline handling but were placed in public out of convenience.
A common mistake is to place a file in both places, or to reference a file inconsistently, sometimes through the pipeline and sometimes through the public folder, leading to duplicates in output, one fingerprinted and one not, or to broken references when one copy is updated and the other is not. To avoid that, decide, for each asset, whether it belongs in the pipeline or in the public folder, and stick to that decision consistently across the project. Document the decision, if the project is large, so that new developers do not guess and place files in the wrong place. Consistency in asset handling, like consistency in other build decisions, prevents a class of bugs that are easy to create and hard to trace, because they manifest as missing or outdated files in built output, not as errors in source.
When testing a build, check the public folder's output explicitly. Open the built site, check that public files are present in the output folder, under their expected names and paths, and that references to them work. A public file that is missing from output, or present under a wrong path, indicates a configuration problem, such as a public folder that is not configured, or a reference that assumed a different output structure. Public files are simple, but their simplicity depends on the build copying them correctly, and that copying deserves the same verification as any other build step.
Check: When should a file go in the public folder instead of through the pipeline?
Section 36 — Fonts in a build
Fonts are assets, but they have particular handling needs in a build, because they affect both appearance and loading, and because they are often large files that are used across many pages. A font file, in source, might be referenced from a stylesheet, or imported through the build pipeline, and the build tool must copy it to output, rewrite its reference to point at the output location, and, ideally, fingerprint it for caching, just as it does for images. If a font reference is not rewritten correctly, the built page will fall back to a different font, changing its appearance in ways that may be subtle, such as slightly different spacing or letter shapes, or obvious, such as a completely different typeface. Checking built pages for correct fonts, by looking at them, is therefore part of testing a build, not just a design check.
Loading behaviour matters for fonts as well. A browser that must download a font before it can display text may delay text rendering, showing a blank space or a fallback font until the font arrives, which can be jarring for visitors. Build and loading strategies can mitigate this, such as by ensuring fonts are loaded early, by using a fallback font that is similar in size to the intended font, so that text does not shift noticeably when the intended font arrives, or by splitting fonts into subsets for different character sets, so that only the needed subset is downloaded. Those strategies involve both build configuration, such as how fonts are copied and referenced, and HTML and style decisions, such as how fonts are declared and loaded. They should be considered together, because a build that copies fonts correctly but loads them late still produces a poor experience.
Font licensing and source are also considerations, although they are not build tool concerns in the strict sense. A font used in a project must be licensed for web use, and its source files must be obtained legally, whether from a font service or from a licensed file. A build tool does not check licensing, and using an unlicensed font in a build does not make it licensed. Developers should ensure that fonts in the source folder are properly licensed, and that any font service's terms are followed, such as attributing the service or using its recommended loading method, rather than downloading its files in a way that violates its terms. That diligence is part of professional front-end work, and it applies to fonts just as it applies to images, icons, and other third-party assets.
For a beginner, the practical lesson is to treat fonts as built assets, not as an afterthought. Place font files where the build expects them, whether in the pipeline or in the public folder, depending on whether they need fingerprinting and reference rewriting, and check built output for correct font loading and appearance. If a built page's fonts look wrong, check the built stylesheet's font references, the output folder for font files, and the browser's network tools for failed font requests, in that order. Those checks, like the asset checks in earlier sections, turn a vague "fonts look wrong" into a concrete build or reference problem that can be fixed in source or configuration, and then verified by rebuilding.
Check: Why should you check fonts specifically in built output, not just in source?
Section 37 — Optimising images in a build
Images are often the largest part of a page's download, and optimising them in a build can improve loading more than optimising code, because code, even unoptimised, is usually smaller than images. Image optimisation in a build can take several forms, each with its own trade-offs. It can convert an image to a more efficient format, which preserves visual quality while reducing file size, provided the target browsers support that format, or it can provide multiple formats and let the browser choose the most efficient one it supports. It can resize an image to match its display size, so that a large original, intended for a large display, is not downloaded in full for a small display where most of its detail would be wasted. It can compress an image, reducing file size at the cost of some quality, with the balance chosen to preserve visual acceptability while saving download time.
Each optimisation should be judged visually, not just by file size. An image that is much smaller but visibly degraded, with artefacts, banding, or loss of detail that visitors notice, is not an optimisation. It is a quality failure, however impressive its size reduction looks in a build report. A good build setup therefore optimises conservatively, preserving quality for prominent images, such as hero images or product images, and compressing more aggressively only for small, decorative images where degradation is less noticeable. Checking built images visually, by opening the built page and looking at them, is essential, because a build report's size numbers do not tell you whether an image still looks good. The eye is the final judge of image optimisation, not the byte count.
Responsive images add another layer to build optimisation. A page displayed on a small phone and on a large desktop does not need the same image file. A build can generate multiple sizes of the same image, and HTML can let the browser choose the most appropriate size for the current viewport and device, so that phones download smaller files and desktops download larger ones, each appropriate to its display. That generation of multiple sizes is a build task, because it must be done for each source image, consistently, and the resulting files must be referenced correctly in the built HTML. Without a build, generating multiple sizes by hand for every image is tedious and error-prone, and it is often skipped, leaving phones to download desktop-sized images unnecessarily. With a build, it becomes automatic, and loading improves for mobile visitors in particular.
Finally, image optimisation should be balanced against build time. Optimising many large images on every development rebuild would slow development unnecessarily, because images rarely change during a typical editing session, while code changes frequently. A sensible setup optimises images fully in production builds, where output quality matters most, and either skips image optimisation or does it lightly in development builds, where speed matters most. That mode-based difference, like other development versus production differences, reflects the different priorities of the two contexts, and it keeps development fast without sacrificing production quality. Optimise images for visitors, in production, and keep development builds focused on code.
Check: Why should image optimisation be judged visually, not only by file size?
Section 38 — Accessibility and the build
A build step should not break accessibility, but it can, if its transformations or optimisations are applied carelessly, and checking built output for accessibility is therefore part of testing a build, not just a design concern. Accessibility, in front-end terms, means that people with different abilities can use a page, through real headings, labelled form fields, keyboard navigation, sufficient contrast, and alternative text for meaningful images. Those features are present in source, in HTML and styles, and a build that transforms HTML, styles, or code must preserve them in output. A build that removes a label, changes a heading level, or strips alternative text, whether through a faulty transformation or through an over-aggressive optimisation, has broken accessibility, even if the page still looks correct to a sighted mouse user.
Minification and bundling, in themselves, do not usually break accessibility, because they do not change HTML structure or style semantics, only code size and packaging. The risks come from transformations that do change structure or semantics, such as a tool that rewrites HTML, inlines styles, or removes supposedly unused code that is actually needed for keyboard navigation or for screen reader announcements. A script that is only triggered by keyboard interaction, for example, might appear unused to a static analysis that only looks for mouse events, and removing it would break keyboard use. A style rule that only affects focus indicators, which are visible only when navigating by keyboard, might appear unused in a visual check that only uses a mouse, and removing it would make keyboard navigation invisible. Those are the kinds of accessibility regressions that build optimisations can cause, and they are easy to miss if testing only uses a mouse and a visual check.
Testing built output for accessibility therefore requires the same checks as testing source, applied to the built page. Open the built page, navigate it by keyboard, check that focus is visible and logical, check that form fields have labels, check that headings are in order, and, where possible, test with a screen reader or with an accessibility checking tool, to catch problems that visual inspection alone would miss. Those checks should be part of the production build test, not just the development test, because a build-specific regression will only appear in built output. If a check fails in built output but passes in source, the build is the culprit, and its configuration or transformations deserve investigation, starting with the most recent optimisation added.
The broader lesson is that a build is not neutral with respect to user experience. It can improve loading and efficiency, which benefit all users, including users with slower connections or older devices, who often benefit most from optimisation. But it can also harm accessibility, if optimisations are applied without considering how users actually interact with the page, through keyboards, screen readers, and other assistive tools. A responsible build setup optimises for loading while preserving accessibility, and it verifies both, by testing built output for loading performance and for accessibility, not just for visual correctness. Optimisation that helps one group of users while harming another is not a net improvement, and it should be reconsidered.
Check: How could a build optimisation accidentally harm keyboard users?
Section 39 — Testing the built output, not just source
Testing source is essential, but testing built output is also essential, because built output is what visitors actually receive, and it can differ from source in ways that testing source alone will not reveal. A feature that works in development, with source files loaded directly, may fail in production, after bundling, minification, tree shaking, splitting, or asset optimisation, for reasons covered in earlier sections. Testing built output catches those production-only failures before deployment, while they are still cheap to fix, rather than after deployment, when visitors have already encountered them. The habit of building for production locally and testing the built output, for every significant change, is one of the most valuable habits a front-end developer can adopt.
Testing built output should cover the same ground as testing source, but with attention to build-specific risks. Open the built pages, not just the source pages, and check that they look correct, that their styles apply, that their images and fonts load, and that their interactive features work, including features that are lazy-loaded or split into chunks. Check that lazy features load on demand, that shared code loads correctly, and that cached files update when they should, by testing a repeat visit with a cache, as discussed in the caching section. Check the browser console for errors in built output, because an error that appears only in built output points at a build problem, such as a missing chunk or a broken reference, rather than at a source logic problem.
Automated tests, if a project has them, should also be run against built output where possible, or at least against a production-like build, because tests run only against source may pass while built output fails, for the same production-only reasons. That does not mean every test must be run against built output on every change, which would be slow, but it does mean that a final check, before deployment, should include running key tests or key manual checks against the built output that will actually be deployed. That final check is the last opportunity to catch a build problem before visitors do, and it should not be skipped because source tests passed.
Finally, testing built output builds familiarity with what production output looks like, which makes production debugging faster when it is needed. A developer who has opened built files, inspected their structure, and seen their source maps in action, will be less intimidated by a production error than a developer who has only ever seen source. That familiarity is a professional asset, and it comes from the habit of testing built output regularly, not just when a production bug forces it. Build, open, test, and verify, as a routine, and built output will feel like a known quantity, not a mystery that only appears at deployment time.
Check: Why is testing source alone not enough before deployment?
Section 40 — Deploying built output
Deploying is the process of placing built output where visitors can reach it, on a server or a hosting service, with the correct configuration for serving, caching, and security. It is distinct from building, although the two are often run together in a deployment pipeline, where a build is triggered, its output is checked, and then that output is uploaded or copied to the hosting service. Understanding deployment as a separate step from building helps you debug problems that occur at deployment time, such as files that build correctly locally but fail to appear on the live site, or files that appear but are served with wrong caching or wrong content types. Those are deployment problems, not build problems, and they require checking the hosting configuration and the deployed files, not just the build configuration.
The deployed files should be exactly the built output, from a clean production build, not a mixture of built files and hand-edited files, or files from different builds. Mixing outputs from different builds can create a site where some files are from one version and others are from another, leading to mismatched code, broken references, or stale chunks that do not match the current HTML. A clean deployment, where the output folder is built fresh, checked, and then deployed as a whole, avoids that mixing and ensures that the live site corresponds to a single, known build. If a deployment process uploads files incrementally, it should do so in a way that preserves that wholeness, such as by deploying to a new location and switching to it atomically, rather than overwriting files in place while visitors are using the site.
Caching configuration at deployment time is also critical, and it interacts with the fingerprinting discussed earlier. Files with fingerprints, which change names when their contents change, can be cached for a long time, because a changed file will have a new name and will be fetched fresh. Files without fingerprints, such as the main HTML file, which references the fingerprinted files, should be cached briefly or not at all, because it must always point at the current fingerprinted files, not at an old set. Getting that caching configuration wrong can cause a live site to serve old HTML pointing at old files, or new HTML pointing at files that have not yet been deployed, both of which break the site for visitors. Deployment configuration should therefore be reviewed alongside build configuration, because the two work together to deliver a correct, cached, and up-to-date site.
After deploying, verify the live site, by opening it as a visitor would, checking key pages and features, and confirming that the deployed files match the built output you tested locally. That live verification is the final check in the build-and-deploy chain, and it catches problems that local testing cannot, such as hosting configuration errors, missing files that were not uploaded, or environment differences between local and live servers. A deployment is not complete when files are uploaded. It is complete when the live site is verified to work, and that verification should be part of every deployment, however routine it feels.
Check: Why should a deployment use a single clean build, not a mixture of files?
Section 41 — Rolling back a bad deployment
Even with careful testing, a deployment can go wrong, and a site can break for visitors after an update. Rolling back means returning the live site to the previous, working version, quickly, while the problem is investigated, rather than leaving a broken site live while a fix is developed. A rollback is not a fix. It is a safety measure that restores service, buys time, and prevents a bad deployment from affecting more visitors than necessary. Having a rollback plan, before it is needed, is part of professional deployment, because a plan made during an outage, under pressure, is likely to be incomplete or mistaken.
The simplest rollback is to redeploy the previous built output, from the previous clean build, which is why keeping previous builds, or keeping the source and configuration needed to rebuild them, matters. If the previous output is available, redeploying it restores the site to its previous state, with its previous files, fingerprints, and references, all consistent, because they came from a single build. If the previous output is not available, rebuilding it from the previous source, by checking out the previous commit and running a clean production build, achieves the same result, provided the build is reproducible, as discussed in the next section. Either way, the rollback should be a whole, consistent version, not a patchwork of old and new files, for the same reason that a deployment should be a whole build.
Rollback also requires care with caching and with data. If the broken deployment changed file names through fingerprinting, rolling back to old file names should work, because old files, if still present on the host, will be served, or, if they were removed, they will be redeployed with the rollback. If the broken deployment changed data, or a data format, rolling back code alone may not be sufficient, because the new data may not work with old code, or old code may not understand new data. Those data considerations are beyond the scope of a front-end build chapter in detail, but they are worth noting, because a front-end rollback that ignores data changes can restore a site that still fails, for a different reason. A rollback plan should consider both code and data, even if the front-end part is only code.
After rolling back, investigate the problem calmly, using the built output from the bad deployment, which should be preserved for debugging, not deleted in the rush to restore service. Compare the bad build with the previous good build, look at what changed in source, in configuration, and in dependencies, and reproduce the problem locally with a production build, as described in the debugging section. Fix the problem in source or configuration, test the fix in a production build locally, and then deploy the fixed version, with the same verification steps as any deployment. A rollback is not a failure. It is a controlled response to a failure, and a team that can roll back quickly and investigate calmly is a team that can deploy confidently, because it knows that a bad deployment is recoverable, not catastrophic.
Check: What is the difference between rolling back and fixing a deployment?
Section 42 — Reproducible builds
A reproducible build is a build that produces the same output, from the same source and configuration, every time it runs, on any machine. That sameness matters for several reasons. It means that a build tested locally will match the build deployed live, because both were produced from the same inputs by the same process, without hidden differences from a developer's local machine, such as a different tool version, a different environment setting, or a file that exists locally but not in version control. It means that a previous build can be recreated exactly, for rollback or for debugging, by checking out its source and running the build again. And it means that a build problem can be reproduced by another developer, on another machine, which is essential for collaborative debugging, because a problem that only occurs on one machine is hard to fix for anyone else.
Reproducibility requires discipline in what the build depends on. It depends on source files, which should be version-controlled and complete, so that checking out a commit gives every file the build needs, with no missing files that only exist on one developer's machine. It depends on configuration, which should also be version-controlled, so that the build's settings are the same for everyone. It depends on dependencies, which should be recorded precisely, so that the same library versions are installed for every build, rather than the latest versions at build time, which could differ from one build to the next. And it depends on environment, which should be controlled, so that build-time values, such as timestamps or random values, do not leak into output and make it differ from build to build, unless those values are deliberately part of the output, such as a build identifier that is expected to change.
Achieving perfect reproducibility can be difficult, and for many projects, approximate reproducibility, where output is functionally the same even if a few bytes differ, such as a timestamp comment, is sufficient. The goal is not byte-for-byte sameness for its own sake, but functional sameness: the same code, the same assets, the same behaviour, produced from the same source, regardless of who builds it or when. If a build's output differs functionally between machines, that difference indicates a hidden dependency, such as a local file, an environment setting, or an unrecorded library version, and finding and recording that dependency improves the project, because it makes the build more reliable for everyone.
For a beginner, the practical steps toward reproducibility are simple: commit all source and configuration, record dependencies precisely, avoid local-only files that the build needs, and test builds on a clean checkout, not just in a long-lived working folder that may contain extra files. A clean checkout test, where you clone the project fresh, install dependencies, build, and check the output, reveals hidden dependencies quickly, because any file that was only on your original machine will be missing, and the build will fail or differ in a way that points at the missing piece. That test, run occasionally, keeps builds reproducible and deployments reliable, without requiring deep knowledge of build theory.
Check: Why does it matter that the same source produces the same build on any machine?
Section 43 — Keeping build configuration readable
Build configuration, like code, is read more often than it is written, and it should be written for readers, not just for the tool. A readable configuration file uses clear names, consistent structure, comments that explain why a setting exists, and grouping that reflects the build's logic, such as grouping entry and output together, transformations together, and plugins in the order they run. A configuration file that is a wall of unexplained options, copied from another project, is hard to maintain, because no one knows which options are essential, which are optional, and which are leftover from a previous need that no longer exists. When the build breaks, that unreadable configuration becomes a barrier to fixing it, because the fixer must first understand a file that was never written to be understood.
Readability also means minimalism. A configuration should contain only the settings the project actually needs, with defaults relied on where they are sensible, rather than listing every possible option with its default value, which obscures the settings that actually matter. When a setting is changed from its default, a comment explaining why it was changed helps future readers understand the decision and judge whether it is still needed. When a setting is added for a specific feature, such as code splitting or image optimisation, a comment naming that feature helps future readers connect the setting to its purpose. Those small documentation habits, applied to configuration, have the same value as documentation in code: they preserve reasoning that would otherwise be lost.
Configuration should also be reviewed, like code, when it changes. A pull request that changes build configuration should be reviewed by someone who understands the build, with attention to whether the change is necessary, whether it is correctly scoped, and whether it might affect output in unexpected ways, such as changing caching, splitting, or optimisation behaviour. A configuration change that looks trivial, such as changing an output folder name, can break deployment if the deployment process expects the old name, or can break caching if it changes fingerprinting behaviour. Reviewing configuration changes with the same care as code changes prevents those surprises, and it spreads knowledge about the build across the team, so that configuration is not the private knowledge of a single developer.
Finally, configuration should evolve with the project, and obsolete settings should be removed, not left in place out of caution. A setting for a feature that was removed, or for a file type that is no longer used, is clutter that confuses readers and can cause errors if it references files or plugins that no longer exist. Removing obsolete settings, in a commit with a clear message, keeps configuration honest and current, just as removing obsolete code keeps a codebase honest. A readable, minimal, reviewed, and current configuration is a build asset in its own right, and it deserves the same care as the source code it configures.
Check: Why should build configuration be reviewed like code?
Section 44 — Common build mistakes
Build mistakes, like module mistakes, tend to fall into a short list, and learning that list in advance will save hours of confusion. The first is editing built output by hand, instead of editing source and rebuilding. The edit appears to work, because the built file changes, but it vanishes on the next build, and the developer is left wondering why a fix that worked yesterday no longer works today. The fix for this mistake is a habit, not a command: always edit source, never output, and if you find yourself editing output, stop and find the source file that produced it. That habit, once formed, prevents a whole class of mysterious regressions.
The second common mistake is testing only in development and never building for production locally before deploying. A site that works in development, with unminified, unsplit, unoptimised code, may fail in production, after transformations that expose a hidden assumption, such as a reliance on a function name that minification shortens, or on a style order that bundling changes. The fix is to build for production locally, as part of testing, and to open the built output and check key features, before deploying. That check takes minutes and catches production-only problems while they are still cheap to fix, rather than after visitors have encountered them.
The third mistake is hard-coding output file names, especially fingerprinted names, in HTML or in other files, instead of letting the build tool generate and reference them. A hard-coded fingerprinted name will be correct for one build and wrong for the next, when the fingerprint changes, leading to broken references and missing files in later deployments. The fix is to let the build tool own output names and references, by generating HTML from the build or by using the tool's reference rewriting, so that names and references stay in step automatically. Similarly, hard-coding a development server address, or a local path, in built output will break in production, where the address and path differ, and environment configuration, covered earlier, is the correct way to handle such differences.
A fourth mistake, worth adding, is adding build complexity before it is needed, such as configuring code splitting, multiple plugins, and complex optimisation for a small project that would work fine with a simple build, or with no build at all. That premature complexity makes the build harder to understand, harder to debug, and harder to maintain, without providing a benefit that justifies it. The fix is the principle repeated throughout this chapter: add build features one at a time, for named reasons, when a real problem appears, and keep the build as simple as the project allows. A simple build that works reliably is better than a complex build that does more but that nobody understands.
Check: Name three common build mistakes.
Section 45 — Build tools and team workflow
In a team, a build tool is not just a personal productivity aid. It is part of the shared workflow, and its configuration, its output, and its errors affect everyone who works on the project. For that reason, build configuration should be version-controlled, shared, and understood by the team, not kept as a personal setup on one developer's machine. When configuration is shared, every developer builds the same way, produces the same output, and encounters the same errors, which makes debugging collaborative rather than individual. When configuration is personal, builds differ between machines, errors differ, and a problem that one developer fixes may reappear for another, because their builds were never the same in the first place.
Builds should also be part of the team's review and integration process. A pull request that changes source should be checked not only for its source changes, but for whether it builds cleanly, whether its built output behaves correctly, and whether it changes the build in a way that affects others, such as adding a dependency, changing a configuration setting, or altering bundle size significantly. Automated checks, run on pull requests, can verify that the project builds, that its tests pass, and that its output meets basic criteria, before a human reviewer spends time on it. Those checks do not replace human review, but they catch mechanical problems early, so that human review can focus on design, logic, and user experience, rather than on whether the project builds at all.
Documentation for the build is also a team asset. A short guide, in the project's documentation, explaining how to run a development build, how to run a production build, where output goes, and how to debug common build errors, helps new developers get started quickly, without needing to ask a teammate for every step. That guide should be kept up to date as the build evolves, because an outdated guide is worse than no guide, as it misleads with confidence. Updating the guide in the same change that updates the build, so that the two never drift apart, is a habit that keeps team knowledge current and reduces the burden on experienced developers to repeatedly explain the build to newcomers.
Finally, build ownership should be shared, not concentrated in one person. If only one developer understands the build, that developer becomes a bottleneck, and the project becomes vulnerable if they are unavailable. Sharing knowledge through documentation, through review of configuration changes, and through pairing on build problems, spreads understanding and makes the build a team capability, not an individual one. A build tool, like any tool, is most valuable when the whole team can use it, debug it, and improve it, not when it is the private expertise of a single member. Invest in shared build knowledge, and the team's workflow will be smoother, more reliable, and more resilient.
Check: Why should build configuration be shared by the whole team, not kept personal?
Section 46 — Security in the build
A build process touches source code, dependencies, and environment settings, and it therefore has security implications that deserve attention, even though a build tool is not primarily a security tool. The first implication is dependencies. A project that uses many libraries, each with its own dependencies, has a large supply chain: code written by many people, in many places, all bundled into the site's output. If any of that code contains a vulnerability, or is malicious, it can affect visitors, because it runs in their browsers as part of the site. Checking dependencies for known vulnerabilities, using the package manager's auditing features or a dedicated checking tool, and updating vulnerable libraries promptly, is therefore part of maintaining a secure build, not an optional extra. A build that bundles a vulnerable library without checking is a build that silently ships a known problem to visitors.
The second implication is secrets, already emphasised, but worth repeating in the build context, because builds are a common place where secrets leak. A secret baked into built output, through an environment value, a configuration file, or a hard-coded value in source, is exposed to anyone who can download the built files, which is anyone who can visit the site. A secret used during the build, such as a token for deploying or for fetching a private dependency, should be kept in the build environment's protected configuration, never in source, never in build configuration that is version-controlled, and never in built output. Reviewing built output for secrets, before deploying, is a useful safeguard, because a secret that appears in output, even once, should be treated as exposed and revoked, not just removed from the next build.
The third implication is build integrity. A build that runs on a shared or automated system should run from trusted source, with trusted dependencies, and with a configuration that has been reviewed, because a compromised build could inject malicious code into output, even if source itself is clean. That risk is low for most small projects, but it grows with project size and with the number of people and systems involved in building and deploying. Simple safeguards, such as building from version-controlled source, recording dependencies precisely, reviewing configuration changes, and verifying built output before deploying, reduce that risk without requiring deep security expertise. They are the same habits that make builds reliable, applied with security in mind.
Security in a build is not about paranoia. It is about recognising that a build is a point where many inputs, source, dependencies, configuration, and environment, combine into output that will run in visitors' browsers, and that each input deserves a basic check. Check dependencies for vulnerabilities. Keep secrets out of source, configuration, and output. Build from trusted, reviewed inputs. Verify output before deploying. Those four habits, practised consistently, make a build not only efficient and reliable, but also responsible, because they protect visitors from problems that a careless build could otherwise introduce silently.
Check: What are two security checks a build process should include?
Section 47 — Performance budgets: setting limits
A performance budget is a set of limits a project sets for itself, to prevent loading performance from degrading as features are added. Without a budget, each new feature adds a little code, a little style, and a few more assets, and each addition seems harmless on its own, but together they make the site slower over time, until visitors notice and the team must undertake a large optimisation effort to recover performance that was lost incrementally. With a budget, each addition is judged against a limit, such as a maximum total size for scripts on a page, a maximum number of requests, or a maximum time for a page to become usable, and an addition that would exceed the budget must either be optimised, split, or reconsidered, before it is merged.
Setting a budget starts with understanding the project's visitors and their context. A site whose visitors mostly use fast connections and recent devices can afford a larger budget than a site whose visitors use slower connections or older devices, where every byte and every request matters more. A site whose core purpose is quick information, such as a news page or a reference page, needs a tighter budget than a site whose purpose is a rich, interactive application, where visitors expect more code and more assets as part of the experience. The budget should therefore be set based on visitor needs and on the site's purpose, not copied from another project whose visitors and purpose differ. A budget that is too loose will fail to prevent gradual degradation. The right budget is one that protects user experience while allowing the site to do its job.
Enforcing a budget requires measurement, not just intention. A build can report the size of its output, the number of bundles, and other metrics, and those reports can be checked against the budget on each build or on each pull request, with a warning or a failure if the budget is exceeded. That automated check makes the budget real, because it prevents a budget-breaking change from merging silently, and it forces a conversation about whether the change is worth its cost, or whether it can be optimised to fit. Without enforcement, a budget is just a document that is ignored when deadlines press, and performance will degrade despite good intentions. With enforcement, performance becomes part of the definition of done, alongside correctness and design.
Budgets should also be revisited periodically, as a project evolves, as visitor behaviour changes, and as network conditions improve or worsen for the target audience. A budget for a small site may need to grow as the site gains features, provided the growth is deliberate and justified by user value, not merely by accumulation. A budget that is never revisited becomes either irrelevant, because it no longer matches the site's reality, or obstructive, because it blocks necessary growth. Reviewing the budget, with data about actual loading performance and visitor experience, keeps it a useful tool rather than a fossil. Performance budgets are not about making sites as small as possible. They are about making deliberate trade-offs between features and loading, in favour of visitors, and recording those trade-offs as limits that the team respects.
Check: Why does a performance budget need enforcement, not just intention?
Section 48 — Measuring what visitors actually experience
Bundle size and request counts are useful build metrics, but they are not the same as visitor experience. A site with a small bundle can still feel slow, if its code blocks rendering, if its images load late, if its fonts cause text to shift, or if its interactive features respond slowly to input. Conversely, a site with a larger bundle can feel fast, if its critical code loads early, its images are optimised and appropriately sized, and its interactions respond promptly. Measuring what visitors actually experience, through loading times, interactivity, and visual stability, therefore matters more than measuring bundle size alone, because visitors judge a site by how it feels, not by how its build report looks.
There are several aspects of experience worth measuring, in conceptual terms. Loading time measures how long it takes for a page to become visible and usable, not just for its files to download, because a page can download quickly but still take time to render if its code is complex or its styles are heavy. Interactivity measures how quickly a page responds to input, such as a click or a key press, after it has loaded, because a page that looks ready but does not respond feels broken, even if its loading time was good. Visual stability measures whether content shifts after it appears, such as text moving when a font loads or an image loads late, because shifting content is disorienting and can cause a visitor to click the wrong thing. Each of these aspects can be affected by build decisions, such as splitting, optimisation, font loading, and image handling, and each should be checked in built output, not just in source.
Measuring experience requires testing in conditions that resemble visitors' conditions, not just on a fast developer machine with a fast connection. A site tested only on a powerful laptop, with a local server and no network delay, will appear faster than it will for a visitor on a slower device, with a slower connection, and with a cache that is cold on first visit. Testing with throttled network conditions, with a cold cache, and on a representative device, gives a more honest picture of visitor experience, and it often reveals problems that fast-machine testing hides, such as a large bundle that is trivial locally but slow on a real connection, or a font that loads instantly locally but delays text for real visitors.
Finally, measurement should guide optimisation, not replace judgement. If measurements show that a page is slow to become interactive, the cause might be a large bundle that needs splitting, or it might be a script that does too much work on load, which is a source problem, not a build problem. If measurements show visual shifting, the cause might be images without dimensions, or fonts without fallbacks, which are HTML and style problems, not build problems. The build is one part of performance, and measuring experience helps you tell whether a performance problem is a build problem, a source problem, or a deployment problem, so that you fix the right thing, rather than optimising a bundle that was never the bottleneck. Measure experience, find the real bottleneck, and fix that, whether it lives in source, in build, or in deployment.
Check: Why is bundle size alone not a good measure of visitor experience?
Section 49 — How build tools are changing
Build tools change over time, as browsers gain new capabilities, as language features become more widely supported, and as developers' needs evolve. Understanding the direction of that change, in conceptual terms, helps you make build decisions that will age well, without needing to predict specific tools or to follow every new release. One direction is toward using browser capabilities more directly. As browsers support modules natively, with imports, dynamic imports, and import maps, the need for bundling every project diminishes for smaller projects, because the browser itself can now do some of the work that bundlers used to do exclusively. That does not make bundlers obsolete, because bundlers still offer optimisation, transformation, and handling of non-JavaScript assets that browsers do not, but it does mean that simple projects can increasingly rely on native browser features, with a build only when optimisation or transformation is genuinely needed.
Another direction is toward faster builds, through tools written for speed, through incremental rebuilding that reuses previous work, and through caching that avoids repeating work that has already been done. Faster builds matter because they shorten the feedback loop between editing and testing, which makes development more pleasant and more productive, especially for large projects where a slow build can interrupt flow. Speed alone, however, is not a reason to switch tools, because a fast tool that produces incorrect or hard-to-debug output is worse than a slower tool that produces correct output. Speed should be judged alongside correctness, debuggability, and maintainability, as one factor among several, not as the sole criterion.
A third direction is toward simpler configuration, with tools that work well with sensible defaults, requiring explicit configuration only for projects that need it. That direction reflects a lesson learned from years of complex build setups: a tool that requires extensive configuration for a simple project is a barrier to adoption, and a tool that works out of the box for common cases, while allowing customisation for uncommon ones, is more likely to be used correctly and maintained well. When choosing or configuring a tool, favouring sensible defaults, and adding configuration only for named reasons, aligns with that direction and keeps builds understandable, regardless of which specific tool you use.
The lesson from these directions is not to chase the newest tool, but to understand the principles that endure: package code for browsers efficiently, transform only when needed, keep configuration readable, test built output, and measure visitor experience. Tools that embody those principles well, whether they are established or new, will serve a project well, and tools that violate them, however fashionable, will cause problems. Build tools will continue to change, but the questions to ask of any tool, what problem does it solve, what does it output, how debuggable is that output, and how does it affect visitors, will remain the same, and answering those questions well is the skill that matters, more than knowing any single tool's details.
Check: How might native browser modules change when a build is needed?
Section 50 — Chapter 26 in one paragraph
Build tools take source written for humans and produce output shaped for browsers, by bundling modules, transforming syntax and styles, handling assets, optimising for loading, and managing caching, so that visitors receive a site that loads quickly and reliably. That summary captures the chapter, but it is worth expanding, because each part of it represents a decision that affects visitors, developers, and the project as a whole, and those decisions deserve to be made deliberately, not by default.
The first decision is whether a build is needed at all, and if so, what it should do. A small project may need no build, or only a simple bundling step, while a large project may need bundling, transpiling, splitting, optimisation, and fingerprinting, all configured to work together. The second decision is how to configure the build, with entry points, output, transformations, plugins, and modes, in a readable, reviewed, and shared configuration that the whole team understands. The third decision is how to test the build, by building for production locally, opening built output, checking its appearance, its assets, its lazy features, and its accessibility, and by measuring visitor experience, not just bundle size. The fourth decision is how to deploy and maintain the build, by deploying clean builds as a whole, by keeping previous builds for rollback, by making builds reproducible, and by keeping dependencies and secrets handled safely.
Together, those decisions turn a build from a mysterious step that happens before deployment into a understood part of development, with a clear purpose, a readable configuration, and a tested output. A project with a well-understood build is a project whose developers can debug production problems, whose visitors receive efficient, reliable pages, and whose team can evolve the build as the project grows, without fear that a change will break something invisible. That understanding is the real goal of this chapter, more than any particular tool or technique, because tools change, but the understanding of what a build does, why it does it, and how to check it, endures across tools and across projects.
Carry forward two habits from this chapter. First, always distinguish source from output, edit source, build output, and test output, not just source, before deploying. Second, add build complexity only for named reasons, when a real problem appears, and keep the build as simple as the project allows, while still meeting its loading, caching, and reliability needs. Those habits, practised on small projects, will scale to large ones, and they will keep builds helpful rather than burdensome. The next chapters in this book will build on this foundation, showing how testing, performance, and deployment practices use built output, but the build itself, as a bridge between human source and browser delivery, remains a central idea in front-end development.
Check: In your own words, summarise what build tools do for a front-end project.
Practical task
Set up a small project with a build step, using a bundler of your choice, and keep the setup conceptual and simple. Create a source folder with an entry module, a helper module it imports, a stylesheet, and an image reference. Configure the build with one entry point and one output folder, run a development build and a production build, and compare them: check that the production output is bundled, that its styles apply, that its image loads, and that its file names support caching. Then add a lazy feature using a dynamic import, rebuild, and verify in the browser's network tools that the lazy chunk loads only when the feature is requested, not on initial load. Finally, open the built pages and check them visually and by keyboard, to confirm the build did not break appearance or accessibility.
Chapter questions
- Explain, in plain words, the difference between source files and built output, and why you should edit source, not output.
- A site works in development but breaks in production after minification and tree shaking. What build-related causes would you investigate, and in what order?
- Why does fingerprinting output file names help caching, and why should the main HTML file be cached differently from fingerprinted files?
- When would you choose code splitting, and when would splitting hurt loading? Give an example of each.
- A teammate proposes adding several build plugins and complex splitting to a small three-page site at once. What would you advise, and why?