How Browsers Work: URL to Pixels

Plain-language notes on how browsers fetch, interpret, and render web pages.


A browser starts by fetching bytes and ends by drawing pixels.

Most frontend bugs live somewhere in the middle: networking, parsing, runtime execution, security rules, layout, paint, or compositing. If I can place a bug in that pipeline, debugging gets faster.

End-to-end flow

Rendering diagram...

Key terms in simple language

URL is the address you open. DNS turns a domain name into a server address. TLS encrypts traffic between browser and server. HTTP is the request/response protocol used for web content. Content-Type tells the browser how to treat incoming bytes.

DOM is the in-memory page structure. CSSOM is the in-memory style rule structure. The render tree combines the visible parts of the page with styles. Layout calculates position and size. Paint turns layout into drawing instructions. The compositor assembles layers into frames. The event loop coordinates what runs next on the main thread.

window and document

window is the global runtime container for one tab or frame. document is the page structure currently loaded in that container. document is available as window.document.

Think of window as the room and document as the whiteboard inside it.

Each tab gets its own room and whiteboard. Each iframe gets its own smaller room and whiteboard.

Where HTML, CSS, JS, and WASM fit

HTML creates the page structure. CSS controls how that structure looks. JavaScript updates behavior, state, and interactions. WebAssembly is useful for compute-heavy work and usually communicates with the page through JavaScript and browser APIs.

V8 and ownership boundaries

V8 runs JavaScript and WebAssembly code. Page parsing, DOM implementation, layout, and painting are handled by the browser engine layer. Network work is handled by browser networking components. Final pixel composition is handled by compositor and graphics components.

In Chromium browsers, V8 handles script execution. Blink handles the page model, web platform APIs, and layout/paint pipeline.

Other browsers use their own engines: Firefox uses Gecko and SpiderMonkey; Safari uses WebKit and JavaScriptCore.

Node.js also uses V8, but it is not a browser host, so it does not provide a page model like window and document by default.

Debugging checklist by layer

  1. Nothing renders: verify status code and Content-Type first.
  2. Script problems: verify script type, load mode, policy restrictions, and cross-origin rules.
  3. Style problems: verify CSS path, response type, and load order.
  4. State mismatch across tabs: verify storage scope and navigation cache behavior.
  5. Jank: profile long main-thread tasks and expensive layout/paint work.

Example: blank page investigation

When a page is blank, I try not to start with React, CSS, or the framework. I start with the pipeline:

  1. Did the navigation return a 200 HTML response?
  2. Did the browser receive text/html and not JSON, XML, or an error page?
  3. Did required CSS and JS files load with the right status and MIME type?
  4. Did JavaScript throw before hydration or rendering?
  5. Did the DOM exist but styles made it invisible?
  6. Did a client-side redirect or auth guard replace the page?

This prevents a common trap: debugging application code when the browser never received the resources it needed.

Example: slow interaction investigation

For jank, the page may load correctly but user actions feel slow. In that case, I usually check for long JavaScript tasks on the main thread, repeated layout calculations caused by measuring and mutating DOM in a loop, heavy paint areas, unnecessary hydration for static content, and expensive event handlers attached to scroll, resize, or input events.

The fix might be code splitting, memoization, CSS containment, virtualized lists, or moving work off the main thread. The right fix depends on which layer is actually congested.

Why the layer model helps teams

The browser is a system with boundaries. Network, parsing, runtime execution, layout, paint, and compositing each have different failure modes. Shared language makes bug reports sharper:

  • "The API returned 200, but the JS bundle has the wrong MIME type."
  • "Hydration succeeds, but layout thrashes after every filter change."
  • "The DOM is present; the issue is paint/compositing cost."

That language shortens the path from symptom to owner.

When debugging browser issues, I try to name the layer first: network, parser, runtime, policy, layout, paint, or compositor. That one step usually cuts down a lot of wandering.


Friendly Copyright & Sharing Reminder by Tushar Mohan.

Hey there! I’m thrilled you stopped by and hope my posts spark ideas of your own.

Feel free to quote short excerpts for commentary, reviews, or academic purposes—but please don’t copy, republish, or remix substantial portions without first getting my written okay.

Need permission? It’s easy—just drop me a note on my email or connect with me on any of the social media platforms I have linked here, with a quick outline of what you’d like to use, and we’ll sort it out fast. Thanks for respecting the work that goes into each post, and for helping keep the internet a place where creators and readers both thrive.

Unless I’ve credited someone else, all articles, code snippets, images, and other goodies on this site are

© Tushar Mohan, 2026.