#Browser#HTTP

Web Fundamentals: Deep Notes

How the browser turns a URL into pixels: the rendering pipeline, the critical rendering path, the event loop, the CSS cascade and stacking contexts, and HTTP from caching to CORS to HTTP/3 — explained as mental models rather than definitions.

By Pratik Rai

Every frontend developer can describe what the browser does. Far fewer can explain why it does it in that order — why CSS blocks rendering but images don't, why a <script> tag in the wrong place costs you a second of load time, why a z-index: 9999 modal still hides behind a header.

This is the layer underneath the frameworks: the rendering pipeline, the critical rendering path, the event loop, the CSS cascade, and HTTP. Understanding it is what separates knowing React from knowing the web.


1. From URL to Pixels

Type a URL, press enter, and a remarkable amount happens before a single pixel appears.

The network phase

Four steps, each one a round trip you pay for:

  1. DNS resolution. The domain resolves to an IP address. The browser checks its own cache first, then the OS cache, then asks a resolver — which walks the root, TLD, and authoritative name servers if nobody has the answer.
  2. TCP handshake. A connection opens via the three-way handshake: SYN, SYN-ACK, ACK.
  3. TLS handshake. For HTTPS, certificates are exchanged and a session key negotiated so the traffic is encrypted.
  4. HTTP request. The browser asks for the HTML document. The server responds with bytes.

Every one of these is latency before the browser has seen a single byte of content — which is why preconnect to a third-party origin is worth so much. It pays these costs early, in parallel with other work.

The rendering phase

Once HTML bytes start arriving, the browser begins constructing the page:

HTML bytes → tokens → nodes → DOM tree
CSS bytes  → tokens → nodes → CSSOM tree
DOM + CSSOM → Render Tree → Layout → Paint → Composite

DOM construction turns bytes into tokens, tokens into nodes, and nodes into a tree — the structural representation of the document.

CSSOM construction does the same for stylesheets, producing a tree of computed styles that cascades down.

The render tree combines the two, and contains only what will actually be visible. Elements with display: none are excluded entirely. Elements with visibility: hidden are included — they occupy space, they just aren't painted. That distinction catches people out constantly.

Layout, sometimes called reflow, computes the exact geometry of every node: its size and its position on the page.

Paint fills in pixels — text, colours, borders, shadows, images — producing paint records organised into layers.

Composite draws those layers to the screen in the right order. This happens on the compositor thread, on the GPU, which is the single most important fact in the whole pipeline for anyone who cares about animation.

Reflow versus repaint

These two words get used interchangeably and they should not be.

Reflow recalculates geometry. It is expensive, and it cascades — changing one element's size can force recalculation of its children and its ancestors. It's triggered by changing width, height, margin, padding, top, left, or font size; by adding or removing DOM nodes; and — less obviously — by reading layout properties like offsetHeight. A reflow always implies a repaint afterwards.

Repaint redraws pixels without changing geometry. Triggered by color, background-color, visibility, box-shadow. Cheaper than reflow, but not free.

And then there are transform and opacity, which skip both. They're handled entirely by the compositor, on the GPU, without touching layout or paint. This is the whole answer to "how do you build a smooth 60fps animation": animate the two properties that never touch the main thread.

Layout thrashing

The classic performance anti-pattern is interleaving reads and writes, which forces the browser to reflow on every iteration:

JSfile.js
1// Bad — each read forces a synchronous layout 2for (let i = 0; i < boxes.length; i++) { 3 boxes[i].style.width = boxes[i].offsetWidth + 10 + 'px'; 4} 5 6// Good — batch all reads, then all writes 7const widths = boxes.map(b => b.offsetWidth); 8boxes.forEach((b, i) => { 9 b.style.width = widths[i] + 10 + 'px'; 10});

The browser queues style changes and applies them in a batch. But the moment you read a layout property, it must flush that queue to give you an accurate answer. Read-write-read-write in a loop defeats the batching entirely.


2. The Critical Rendering Path

The critical rendering path is the sequence the browser must complete before it can render the initial view. Shortening it is the most direct way to make a page feel fast.

Two things block it, and they block it for different reasons.

CSS blocks painting. The browser refuses to paint until the CSSOM is complete, because painting early would show unstyled content and then repaint it once styles arrived. Blocking is the lesser evil. So critical CSS should be small and arrive fast, and everything else should be deferred.

Scripts block parsing. A plain <script> stops DOM construction, fetches, executes, and only then lets parsing resume — it has to, because the script might call document.write. Worse, a script waits for any in-progress CSSOM construction before it runs, since it can query computed styles, so a stylesheet in the head can delay a script that has nothing to do with it.

That is what async and defer exist to fix:

HTMLindex.html
1<!-- Blocks parsing during both fetch and execution --> 2<script src="app.js"></script> 3 4<!-- async: fetched in parallel, runs as soon as it arrives — may interrupt 5 parsing, order not guaranteed. Right for analytics and similar. --> 6<script async src="analytics.js"></script> 7 8<!-- defer: fetched in parallel, runs after parsing, in document order. 9 The best default for application code that touches the DOM. --> 10<script defer src="app.js"></script>

Optimising the path itself comes down to three levers: fewer critical resources, fewer critical bytes, and fewer round trips.

That is the working summary. The six stages the browser actually runs — DOM, CSSOM, render tree, layout, paint, composite — and how each one maps onto Core Web Vitals are covered in Understanding the Critical Rendering Path, which is worth reading in full before an interview that is likely to go near performance.


3. The Event Loop

JavaScript runs on a single thread, yet handles asynchronous work without blocking. The event loop is how.

The moving parts

  • The call stack — where synchronous code executes, LIFO, one frame at a time.
  • Web APIs — timers, fetch, DOM events. These are provided by the browser and run off the main thread. When they finish, they hand a callback to a queue.
  • The macrotask queue — callbacks from setTimeout, setInterval, I/O, UI events.
  • The microtask queue — callbacks from resolved promises, queueMicrotask, and MutationObserver.

The rule that explains everything

After the current synchronous code finishes, and after each macrotask, the event loop drains the entire microtask queue before picking up the next macrotask.

Microtasks always jump ahead of the next timer. That single rule predicts the output of almost every event-loop puzzle:

JSfile.js
1console.log('1: sync start'); 2 3setTimeout(() => console.log('2: setTimeout'), 0); 4 5Promise.resolve() 6 .then(() => console.log('3: promise then')) 7 .then(() => console.log('4: promise then 2')); 8 9console.log('5: sync end');
1: sync start
5: sync end
3: promise then
4: promise then 2
2: setTimeout

The synchronous logs run first and empty the stack. Then the microtask queue drains completely — both .then callbacks — before control returns to the event loop. Only then does the setTimeout callback run, despite its zero delay.

async/await follows the same rule, because everything after an await is a microtask:

JSfile.js
1async function foo() { 2 console.log('A'); 3 await null; 4 console.log('B'); // queued as a microtask 5} 6console.log('start'); 7foo(); 8console.log('end'); 9// start, A, end, B

4. CSS That Actually Gets Asked About

CSS gets dismissed as the easy part, which is precisely why gaps in it are so visible.

The box model

Every element is a box: content, then padding, then border, then margin.

The default box-sizing: content-box means width sets only the content width — padding and border are added on top, so a 200px box with 20px padding is actually 240px wide. Almost every codebase resets this:

CSSstyles.css
1*, *::before, *::after { 2 box-sizing: border-box; 3}

With border-box, width includes padding and border, which is what most people intuitively expect.

Specificity

When several rules target the same element, specificity decides. Score it as a tuple — inline, IDs, classes/attributes/pseudo-classes, elements/pseudo-elements:

SelectorScore
div0,0,0,1
.btn0,0,1,0
#header0,1,0,0
style="..."1,0,0,0
!importantoverrides everything

Higher tuple wins, compared left to right. Ties break by source order — last one wins. !important sits outside the system entirely, which is why it's a trap: it wins today and makes tomorrow's override impossible.

Flexbox and Grid

Flexbox is one-dimensional — a row or a column. It's the right tool for distributing items along a single axis: navigation bars, button groups, centring.

Grid is two-dimensional — rows and columns simultaneously. It's the right tool for page layout and anything with an explicit structure.

CSSstyles.css
1.center { 2 display: flex; 3 justify-content: center; /* main axis */ 4 align-items: center; /* cross axis */ 5}

Positioning

  • static — the default; participates in normal flow.
  • relative — offset from its normal position, but still occupies the original space. Establishes a positioning context for descendants.
  • absolute — removed from flow, positioned relative to the nearest positioned ancestor.
  • fixed — positioned relative to the viewport; stays put during scroll.
  • sticky — a hybrid: behaves as relative until a scroll threshold, then sticks.

Stacking contexts

This is the source of the most frustrating CSS bug there is.

z-index only applies to positioned elements and flex/grid children. More importantly, a new stacking context is created by a positioned element with a z-index other than auto — but also by opacity less than 1, by transform, by filter, and by will-change.

Children stack within their parent's context. So a modal with z-index: 9999 will still sit behind a header if the modal's parent has a lower stacking context than the header's. The 9999 is competing with its siblings, not with the header.

This is why "why won't my modal come to the front" is almost never solved by raising the z-index, and almost always solved by moving the element — usually with a portal.


5. HTTP

Methods

MethodPurposeSafeIdempotent
GETReadYesYes
POSTCreateNoNo
PUTReplaceNoYes
PATCHPartial updateNoNo
DELETERemoveNoYes

Safe means no side effects. Idempotent means repeating the request produces the same result — which is exactly why POST is the one you cannot blindly retry.

Status codes

  • 2xx200 OK, 201 Created, 204 No Content
  • 3xx301 Moved Permanently, 302 Found, 304 Not Modified
  • 4xx400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests
  • 5xx500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable

Two worth being precise about. 401 means not authenticated; 403 means authenticated but not permitted — despite 401's misleading name. And 304 means your cached copy is still valid, which is the basis of revalidation.

Caching

Cache-Control is the primary mechanism. max-age=3600 caches for an hour. no-cache means revalidate before using — not "don't cache", which is a genuinely common misreading. no-store is the one that means never cache.

ETag is a fingerprint of the resource. The browser sends it back as If-None-Match, and if nothing changed the server returns 304 with no body.

Last-Modified is the timestamp equivalent, paired with If-Modified-Since. ETags are more precise and take precedence when both are present.

HTTPfile.http
1# Server response 2Cache-Control: max-age=86400 3ETag: "abc123" 4 5# Browser revalidates on the next visit 6GET /style.css 7If-None-Match: "abc123" 8 9# Unchanged — no body re-sent 10HTTP/1.1 304 Not Modified

CORS and the same-origin policy

The same-origin policy prevents a page from reading responses from a different origin, where origin means scheme, host, and port — all three must match. This is the actual security mechanism.

CORS is the controlled relaxation of it. For "non-simple" requests — custom headers, methods like PUT or DELETE, certain content types — the browser first sends an automatic OPTIONS preflight asking what the server permits:

HTTPfile.http
1# Preflight, sent automatically by the browser 2OPTIONS /api/data 3Origin: https://app.example.com 4Access-Control-Request-Method: PUT 5Access-Control-Request-Headers: Authorization 6 7# The server grants permission 8Access-Control-Allow-Origin: https://app.example.com 9Access-Control-Allow-Methods: GET, PUT, POST 10Access-Control-Allow-Headers: Authorization

Only if the response approves does the real request follow.

The framing that matters: CORS is enforced by the browser, not the server. The server receives and processes the request regardless; the browser simply refuses to hand the response to your JavaScript if the headers don't permit it. CORS is not a protection for your API — it's a protection for your users' browsers, and it's permission you grant rather than a wall you build.

HTTP/1.1, /2, and /3

HTTP/1.1 handles one request at a time per connection, with head-of-line blocking. Browsers open around six parallel connections per origin to compensate. Headers are plain text.

HTTP/2 introduced multiplexing — many requests and responses share one connection, interleaved as streams. It added header compression via HPACK. This solved application-layer head-of-line blocking, though TCP-level blocking remained.

HTTP/3 runs over QUIC, built on UDP rather than TCP. That eliminates TCP head-of-line blocking entirely: a lost packet on one stream no longer stalls every other stream. Connection setup is faster too, with the TLS handshake folded in.


The Two Questions Everything Above Answers

"What happens when you type a URL and press enter?"

DNS, TCP, TLS, HTTP request, server responds with HTML. The parser builds the DOM while CSS builds the CSSOM. They combine into the render tree, which goes through layout, paint, and composite. Scripts can pause parsing; CSS blocks rendering. defer and async and the critical rendering path are how you make that sequence shorter.

"What's the difference between reflow and repaint?"

Reflow recalculates geometry, is expensive, and cascades through the tree. Repaint just redraws pixels and is cheaper. transform and opacity skip both entirely and run on the compositor — which is how you get animation that holds 60fps. And you avoid layout thrashing by batching every read before every write.

Goal: Build crisp, interview-ready mental models for the browser rendering pipeline, critical rendering path, the event loop, CSS fundamentals, and HTTP — including caching, CORS, and HTTP/1 vs HTTP/2 vs HTTP/3.

Related Articles

Continue learning with these related challenges

View All
Blogs

Frontend Roadmap, Tier 1: Beginner Deep Notes

The mental models and edge cases that separate someone who has used HTML, CSS, JS, forms, responsive design, and HTTP from someone who actually understands them. Part 1 of the Frontend Roadmap series.

HTML · CSS · JavaScript · HTTPPratik Rai ·

Blogs

Web Security: Deep Notes

XSS (stored, reflected, DOM-based), CSRF, CORS and the Same-Origin Policy, auth token storage tradeoffs (HttpOnly cookies vs localStorage), and a set of quick-hit defenses — explained as layered mental models, not a checklist.

JavaScript · HTTP · Browser · AuthenticationPratik Rai ·

Blogs

Performance Optimisation from First Principles

Understanding frontend performance optimization from the ground up - learn how browsers work and optimize your code accordingly.

Performance · Frontend · JavaScript · BrowserPratik Rai ·