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
- How a browser sends HTTP requests and receives responses
- The rendering pipeline: HTML → DOM → CSSOM → Render → Paint
- What the DOM (Document Object Model) is
- How
fetch()sends requests from JavaScript - What CORS (Cross-Origin Resource Sharing) means
Table of Contents
- Architectural Overview & Core Schema
- Deep Dive & Implementation
- Anti-Patterns & Common Pitfalls
- Independent Challenge
- Consolidation & Key Invariants
- 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:
- Parse HTML → Build the DOM (Document Object Model)
- Parse CSS → Build CSSOM (CSS Object Model)
- Combine DOM + CSSOM → Create a Render Tree
- Layout — Calculate positions and sizes
- Paint — Fill in pixels
- 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:
- Create an HTML page with a
<header>,<main>, and<footer> - Add a
<script defer>that fetches a list of items from/api/items(mock with aResponseobject or a simple JSON file served via the Vite dev proxy) - Render the items into a
<ul>using DOM APIs (no innerHTML) - Show a loading state before the fetch completes
- Handle the error state if the fetch fails (display an error message, not a silent failure)
- 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.
- Always check
resp.ok(orresp.status) before calling.json()/.text()—fetch()does not reject on HTTP errors. - Place
<link rel="stylesheet">in<head>— stylesheets in<body>cause FOUC and extra reflows. - Use
deferon<script>in<head>— never use blocking synchronous scripts unless absolutely necessary. - Batch DOM reads, then DOM writes — interleaving reads and writes causes layout thrashing.
- Use Vite's dev proxy for cross-origin API calls — avoids CORS in development without server changes.
- The render pipeline is ordered: Parse HTML → CSSOM → Render Tree → Layout → Paint → Composite — understanding this order explains every performance and rendering issue.
6. Next Steps
Sequenced links to dependent lessons, deeper dives, or production guides.
- Read
kb/web/html-dom.mdto learn semantic HTML and DOM manipulation - Read
kb/web/css-basics.mdfor layout, selectors, and responsive design - Explore
kb/containers/docker-concepts.mdto understand how the backend runs - Build a simple
fetch()-based app with a mock REST API
Change Log
- 2026-08-29: Migrated to 6-section canonical template (added sections 3, 4, 5; updated ToC; added Change Log entry)
- 2026-08-29: Expanded from stub to full entry-level lesson (deep dive sections, code examples, Next Steps, Change Log)