Contents
  1. What the Browser Needs
  2. What You Manage Manually
  3. ES Modules in the Browser
  4. When the Manual Approach Is Appropriate
  5. What to Do Now
← All posts

From Source to Browser: Building Without a Framework

Before a framework existed, a website was a folder of files. Understanding what the browser actually needs makes every tool introduced after this point make sense.

Before a framework existed, before a build tool was involved, a website was a folder of files. Understanding what the browser actually needs, and what problem every tool introduced after this point was solving, starts here.

What the Browser Needs

A browser can load three types of files directly: HTML for structure, CSS for presentation, and JavaScript for behaviour. Nothing else. Every framework, every bundler, every compiler exists to produce these three file types from something that is more convenient to write but that the browser cannot read directly.

Without any tooling, a project looks like this:

project/
  index.html
  style.css
  script.js
  images/

The HTML file links the other files explicitly:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My Site</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="app"></div>
  <script src="script.js"></script>
</body>
</html>

When the browser opens index.html, it reads the file top to bottom. It finds the <link> tag, makes a separate HTTP request for style.css, and applies it. It finds the <script> tag, makes another request for script.js, and executes it. That is the entire loading model. No build step, no compiler, no configuration file.

What You Manage Manually

Without a build pipeline, every responsibility that tools normally automate falls on the developer.

File order matters. If script.js depends on a function defined in utils.js, utils.js must be loaded first. You manage this by controlling the order of <script> tags.

<script src="utils.js"></script>
<script src="script.js"></script>

Each script tag is a separate HTTP request. Ten scripts mean ten requests. In development this is acceptable. In production, the number of requests directly affects load time.

There is no module system by default. Variables declared in one script file are global and accessible from every other script on the page, which means naming collisions are your responsibility to prevent.

CSS has the same problem. All stylesheets share a single global scope. A class named .button in one file will affect every element with that class across the entire page, regardless of which file defined it.

ES Modules in the Browser

Modern browsers support ES modules natively. Adding type="module" to a script tag enables import and export syntax without any build step:

<script type="module" src="main.js"></script>
// utils.js
export function add(a, b) {
  return a + b;
}

// main.js
import { add } from './utils.js';
console.log(add(2, 3)); // 5

The browser resolves the import, makes a request for utils.js, and executes it. This works in all modern browsers as of 2021 without any tooling. The limitation is that each import triggers its own network request, which becomes a performance issue as the number of files grows.

When the Manual Approach Is Appropriate

Building without a framework or build tool is appropriate for small, focused sites: landing pages, documentation pages, single-page utilities, and prototypes where the overhead of a build pipeline does not justify itself. The output is exactly what you write. There is no transformation layer between your source and what the browser runs.

The manual approach also makes the problem that build tools solve concrete and visible. When you feel the friction of managing script order, preventing global scope collisions, and handling many network requests, you understand precisely what webpack, Vite and the Vue and Angular CLIs are automating.

What to Do Now

Create a folder with three files: index.html, style.css and script.js. Link them manually. Open the HTML file in a browser. Open the browser’s Network tab in DevTools and observe each file loading as a separate request. Then add a second JavaScript file and import a function from it using type="module". Watch the network tab again and note that the browser resolves the dependency itself, without any build step involved.

← All posts