The Age of Darkness: Web Development Before Build Tools
In the beginning, there was the `<script>` tag.
It was a simpler time, though those of us who lived through it rarely recognized its simplicity as a virtue. We were too busy drowning in the chaos that our own hands had created. The year was 1999, or perhaps 2003, or maybe 2008—depending on which corner of the web you inhabited. The details matter less than the fundamental truth: before the bundling revolution reshaped our craft, frontend development existed in a state of managed anarchy, where every project was a delicate house of cards built upon the twin foundations of global namespaces and alphabetical file ordering.
This is the story of that darkness. This is the chronicle of the problems that drove us toward the solutions we now take for granted. And understanding this history is essential—not merely as an exercise in nostalgia, but because the decisions baked into our modern tools still carry the fingerprints of the problems they were designed to solve. To truly master the tools of today, we must understand the ghosts of yesterday.
The World Before: HTTP/1.1 and the Request Economy
To appreciate the primitive state of early frontend development, we must first understand the technological context in which it operated. The web of the early 2000s ran on HTTP/1.1, a protocol designed in an era when a web page was a document, not an application. HTTP/1.1 had no built-in concept of modules, of dependencies, of scope. It had one job: fetch a resource from a URL and deliver it to the browser.
```http GET /index.html HTTP/1.1 Host: www.example.com
HTTP/1.1 200 OK Content-Type: text/html
<!DOCTYPE html> <html> <head> <title>My Web Application</title> </head> <body> <script src="/js/utils.js"></script> <script src="/js/main.js"></script> </body> </html> ```
This simple request-response model was elegant in its simplicity, but it placed the entire burden of dependency management onto the developer. The browser would fetch `index.html`, parse the HTML, encounter the first `<script>` tag, fetch `utils.js`, execute it, then move to `main.js`, fetch it, and execute that. There was no mechanism for `utils.js` to declare what it needed, and no mechanism for `main.js` to declare what it provided. Everything ran in a single, shared global namespace—typically the `window` object in browsers—and the only way developers could coordinate was through careful manual ordering and the strictest discipline.
The HTTP/1.1 protocol compounded these challenges with its notorious head-of-line blocking behavior. Each connection could handle only one request-response pair at a time, and establishing a new TCP connection required the infamous three-way handshake: SYN, SYN-ACK, ACK. For a typical web page loading a dozen script files, this meant a dozen round trips to the server, each waiting in line behind the others. The response times were brutal, the user experience often miserable, and the developer's only tools for optimization were concatenation and minification—manual processes that were error-prone and difficult to maintain.
Consider what this meant in practice. When a developer wanted to use a utility function—say, a date formatter—in their application, they didn't import it from a module. They found a library that provided it, downloaded the library's JavaScript file, placed it in their project directory, and added a `<script>` tag somewhere in their HTML. The key word here is "somewhere." Where exactly the script tag went determined whether the function would be available when their code ran. Put it after the code that needed it, and the browser would throw a `ReferenceError`. Put it before, and everything would work—until someone else on the team accidentally moved the script tags during a refactoring, or until a library update changed the function name, or until a different library overwrote the function with its own implementation of the same name.
This was the daily reality of frontend development in the pre-build-tool era: an endless series of fragile conventions, maintained through discipline and prayer.
The Global Namespace: A Shared Bathroom Without Locks
JavaScript's original design included a critical flaw that would shape—and haunted—frontend development for over a decade: the global namespace. In JavaScript, any variable declared outside of a function automatically becomes a property of the global object. In browsers, that global object is `window`. This meant that every script on a page shared the same namespace, the same toilet, if you will, with no locks on the doors.
```javascript // utils.js - Version 1.0 var formatDate = function(date) { return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0'); }; ```
```javascript // feature.js - Third-party library we just integrated var formatDate = function(d) { // Different signature, different behavior // Completely overwrites the original return (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear(); }; ```
```javascript // main.js - Our application code // We expect: "2024-01-15" // We get: "1/15/2024" // And we have no idea why console.log(formatDate(new Date())); ```
The consequences of this design extended far beyond naming collisions. They fundamentally shaped how code was written, how libraries were distributed, and how teams coordinated their efforts. Every variable name became a precious, finite resource that had to be carefully managed across an entire codebase. The JavaScript community developed elaborate naming conventions—namespaces prefixed with company initials, module namespacing patterns, Hungarian notation resurrected from ancient Visual Basic code—all in service of avoiding the inevitable collisions that occurred when two pieces of code claimed the same global variable name.
The industry standard became Immediately Invoked Function Expressions (IIFEs), a pattern that created private scopes and prevented pollution of the global namespace:
```javascript var MyApp = MyApp || {};
MyApp.Utils = (function() { 'use strict';
// Private variables var privateCounter = 0;
// Private function function privateMethod() { privateCounter++; }
// Public API return { publicMethod: function() { privateMethod(); return privateCounter; } }; })(); ```
While IIFEs provided a valuable escaping mechanism, they were fundamentally a workaround, not a solution. They required developers to manually manage the module pattern, to remember to expose only the necessary APIs, and to carefully distinguish between public and private interfaces. They added ceremony to every module, boilerplate that obscured the actual logic. And they still didn't solve the core problem: the global namespace remained the only mechanism for code sharing between modules, even if that sharing was now more controlled.
The jQuery ecosystem attempted to address this through a convention-based approach. Libraries would attach themselves to `jQuery` and its alias `$`, and developers were encouraged to use jQuery's `$.fn.extend()` for plugins. But this was a social contract, not a technical enforcement. Violate it—either through ignorance or carelessness—and you would find your plugin's functionality silently overwritten by another developer who had chosen the same name.
```javascript // A jQuery plugin - fragile, easily overwritten (function($) { $.fn.myPlugin = function() { // Plugin implementation return this.each(function() { // Do something }); }; })(jQuery); ```
The Dependency Graph: A Manual Art
In a module system, dependencies are explicit. A module declares what it needs, and the system resolves those needs, ensuring that dependencies are loaded in the correct order. In the script-tag era, dependency management was a purely manual process, docume