Web Development Basics — How the Web Works

Status: Active Last Updated: 2026-08-29 Category: Web — Fundamentals Prerequisites: None — this is the entry point for web development Tags: web, http, browser, html, dom, css, javascript

Summary

Before you write HTML, CSS, or JavaScript, understand how browsers and servers communicate. This article explains HTTP, the browser rendering pipeline, the DOM, and how a frontend application (like fogserv.cloud's TanStack Router app) communicates with a backend server.

What You'll Learn

Table of Contents

  1. Architectural Overview & Core Schema
  2. Deep Dive & Implementation
  3. Anti-Patterns & Common Pitfalls
  4. Independent Challenge
  5. Consolidation & Key Invariants
  6. Next Steps

1. Architectural Overview & Core Schema

Foundational theory, system mechanics, and high-level design.

How the Web Works

When you visit a URL in a browser, the browser sends an HTTP request to a server. The server sends back an HTTP response. The browser parses that response (usually HTML), builds a DOM tree, applies CSS styles, creates a render tree, lays out elements, paints pixels, and displays the page. This entire pipeline happens in milliseconds.


The Browser Rendering Pipeline

The browser converts your HTML file into a visible page through these steps:

  1. Parse HTML → Build the DOM (Document Object Model)
  2. Parse CSS → Build CSSOM (CSS Object Model)
  3. Combine DOM + CSSOM → Create a Render Tree
  4. Layout — Calculate positions and sizes
  5. Paint — Fill in pixels
  6. Composite — Combine layers

Understanding this pipeline explains why CSS blocking and JavaScript async patterns matter for performance.


2. Deep Dive & Implementation

Technical implementation, fully commented code blocks, and step-by-step logic.

Deep Dive — HTTP Request and Response

An HTTP request has a method (GET, POST, PUT, DELETE, PATCH), URL, headers (metadata), and optionally a body. A response has a status code (200 OK, 404 Not Found, 500 Error, 301 Redirect), headers, and a body (HTML, JSON, etc.).

GET /index.html HTTP/1.1
Host: fogserv.cloud
Accept: text/html

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1234

Browsers set headers automatically. In JavaScript, fetch() gives you control:

const resp = await fetch('/api/users');
const data = await resp.json(); // parse JSON body
console.log(data);

Status codes: 2xx success, 3xx redirect, 4xx client error, 5xx server error. Always check resp.ok or the status code before parsing.

Deep Dive — The Browser Rendering Pipeline

After receiving HTML bytes, the browser decodes characters (per charset), tokenizes tags into tokens, and builds the DOM tree. Then it parses CSS into the CSSOM. It merges both trees into the Render Tree (display:none elements are excluded). Layout computes positions; Paint fills pixels. Composite assembles layers.

CSS and JS block this pipeline. <link rel="stylesheet"> in <head> pauses HTML parsing while CSS loads. defer and async attributes on <script> unblock HTML parsing:

<script src="app.js" defer></script>   <!-- runs after DOM, in order -->
<script src="analytics.js" async></script> <!-- runs ASAP, out of order -->

Inserting or removing DOM nodes, changing CSS properties, or resizing the window triggers reflows and repaints. requestAnimationFrame batches visual updates. Avoid layout thrashing (read then write DOM in a loop).

Deep Dive — The DOM

The DOM (Document Object Model) is a tree of nodes representing HTML. JavaScript accesses it via document:

const h1 = document.querySelector('h1');        // first match
const allButtons = document.querySelectorAll('button'); // NodeList
document.getElementById('myid');                // by ID
document.getElementsByClassName('card');         // live HTMLCollection

DOM nodes are mutable. Changes trigger re-renders:

h1.textContent = 'New Title';          // safe, escapes HTML
h1.innerHTML = '<em>Title</em>';       // parses HTML (XSS risk)
const div = document.createElement('div');
div.textContent = 'Added dynamically';
document.body.appendChild(div);

Events propagate (capture phase down, target, bubble phase up). addEventListener supports passive listeners for smooth scrolling and { once: true } for one-time handlers:

el.addEventListener('click', handler, { passive: true, once: true });

Deep Dive — How Frontend Talks to Backend

Single-page apps use fetch() or axios for HTTP calls. REST APIs use HTTP verbs: GET list resources, POST create, PUT/PATCH update, DELETE remove. Always handle errors:

async function fetchUsers() {
  try {
    const resp = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
    });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    return await resp.json();
  } catch (err) {
    console.error('Failed to fetch users:', err.message);
  }
}

CORS (Cross-Origin Resource Sharing) blocks requests to a different origin unless the server allows it via Access-Control-Allow-Origin. The browser enforces this. For local development, use a proxy or set appropriate CORS headers.

JWTs (JSON Web Tokens) are sent in the Authorization: Bearer <token> header. Store tokens in httpOnly cookies (preferred) or localStorage (vulnerable to XSS). Always use HTTPS in production.

3. Anti-Patterns & Common Pitfalls

Documented failure modes and how to detect/prevent them.

Not checking resp.ok before parsing the body

fetch() only rejects on network failure (no internet, DNS error, CORS block). A 404 or 500 HTTP response resolves successfully — await fetch() returns a Response with ok: false. If you skip if (!resp.ok) throw ... and call resp.json() anyway, the server may return an HTML error page instead of JSON, crashing your parser.

Detection: Search for await.*fetch without a corresponding resp.ok check. In the Network tab of DevTools, inspect the response body — if it's HTML instead of JSON, the server errored.

Fix:

const resp = await fetch('/api/data');
if (!resp.ok) {
  const body = await resp.text(); // read body so it can be consumed
  throw new Error(`HTTP ${resp.status}: ${body}`);
}
const data = await resp.json();

Blocking the render pipeline with synchronous scripts

<script> (no defer/async) in <head> pauses HTML parsing. The browser cannot build the DOM until the script executes and downloads if src= is present. If the script is large or served slowly, the page is blank.

Fix: Always put <script src="app.js" defer></script> in <head> or at the end of <body>. defer downloads in parallel and runs after DOM is ready, in order.

Blocking CSS with render-blocking <link> in <body>

Placing <link rel="stylesheet"> inside <body> forces the browser to re-parse and re-render the document, causing a flash of unstyled content (FOUC). Stylesheets belong in <head>.

Fix: Move all <link rel="stylesheet"> tags to <head>. Use media="print" or rel="preload" + onload for non-critical styles.

Layout thrashing: interleaving DOM reads and writes

Reading element.offsetHeight forces the browser to compute layout synchronously. If you then write to the DOM, the browser must compute layout again. A loop that alternates reads and writes (e.g., read all heights, then write all widths) causes N reflows instead of 1.

Fix: Batch all reads first, then all writes. Use requestAnimationFrame to decouple from the frame cycle:

// BAD — N reflows
elements.forEach(el => {
  const h = el.offsetHeight;          // read
  el.style.height = (h * 2) + 'px';  // write → reflow
});

// GOOD — 1 reflow
const heights = elements.map(el => el.offsetHeight); // read all
requestAnimationFrame(() => {
  elements.forEach((el, i) => el.style.height = heights[i] * 2 + 'px'); // write all
});

Missing CORS preflight causing silent failures

Cross-origin fetch() requests with custom headers (e.g., Authorization) trigger a CORS preflight OPTIONS request. If the server doesn't respond with appropriate Access-Control-Allow-* headers, the request is silently blocked. No error is thrown — the promise never resolves.

Fix: In development, use Vite's proxy (server.proxy in vite.config.ts) to route /api to the backend. In production, configure the backend to emit the correct CORS headers.

4. Independent Challenge

A problem to solve without step-by-step guidance; promotes synthesis.

Build a small page that demonstrates the full browser rendering pipeline. Requirements:

  1. Create an HTML page with a <header>, <main>, and <footer>
  2. Add a <script defer> that fetches a list of items from /api/items (mock with a Response object or a simple JSON file served via the Vite dev proxy)
  3. Render the items into a <ul> using DOM APIs (no innerHTML)
  4. Show a loading state before the fetch completes
  5. Handle the error state if the fetch fails (display an error message, not a silent failure)
  6. Verify with Chrome DevTools: confirm no layout thrashing in the Performance panel during render

Constraints: No frameworks. Pure HTML + browser JS only. Do not look at existing solutions until you have tried.

5. Consolidation & Key Invariants

Bullet-list summary of the must-remember rules.

6. Next Steps

Sequenced links to dependent lessons, deeper dives, or production guides.


Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse