Cover image for How Web Browsers Work

How Web Browsers Work

Liviu Lupei

Liviu Lupei

Founder & Solutions Architect, Endtest · May 29, 2026

A web browser looks simple from the outside.

You type a URL, press Enter, and a page appears. You click a button, submit a form, watch a video, upload a file, open a menu, drag an item, or complete a checkout flow. It feels instant, natural, and predictable.

Under the surface, the browser is doing a surprising amount of work.

It downloads files from the network, parses HTML, builds the DOM, downloads CSS, calculates styles, executes JavaScript, lays out elements, paints pixels, decodes images, manages memory, isolates tabs, protects users from malicious websites, stores cookies, handles permissions, communicates with the operating system, and keeps the page responsive while all of that is happening.

That complexity is one reason web application testing is harder than it looks. A bug is not always caused by your application code alone. Sometimes it appears because of how a browser interprets that code, how the operating system renders it, how a graphics driver handles compositing, how fonts are installed, how permissions are implemented, or how a real browser behaves compared to a browser running in a Linux container.

To understand why cross-browser and cross-platform testing still matters, it helps to understand how browsers actually work.

What is a web browser?

A web browser is an application that retrieves, interprets, executes, and displays web content.

At a high level, the browser receives resources such as HTML, CSS, JavaScript, images, fonts, videos, and JSON responses. It then turns those resources into an interactive visual interface.

The most common browser features are familiar:

  • Address bar
  • Tabs
  • Back and forward navigation
  • Bookmarks
  • Downloads
  • Password storage
  • Developer tools
  • Extensions
  • Privacy and security controls

But those visible features are only the outer shell. The core job of the browser is to transform web code into something users can see and interact with.

A modern browser is a runtime environment for web applications, well beyond an HTML viewer.

That runtime includes rendering, JavaScript execution, networking, storage, security, media handling, accessibility support, GPU acceleration, process isolation, permission handling, and integration with the operating system.

When people say "it works in Chrome," they often mean "it works in the specific Chrome setup I tested." Chrome on Windows, Chrome on macOS, and Chrome on Linux are not always identical in real-world behavior.

The main parts of a browser

Modern browsers are built from several major components. The names vary between browsers, but the responsibilities are broadly similar.

The user interface includes the address bar, tabs, menus, bookmarks, settings, and browser controls. This is the part users directly see outside the web page.

The browser engine coordinates the work between the user interface and the rendering engine. It manages navigation, page loading, session history, and communication between major browser components.

The rendering engine is responsible for turning HTML and CSS into pixels on the screen. Chromium-based browsers use Blink. Safari uses WebKit. Firefox uses Gecko.

The JavaScript engine executes JavaScript. Chrome and other Chromium-based browsers use V8. Safari uses JavaScriptCore. Firefox uses SpiderMonkey.

The networking layer handles HTTP, HTTPS, DNS, caching, redirects, cookies, certificates, compression, and resource fetching.

The storage layer manages cookies, localStorage, sessionStorage, IndexedDB, Cache API data, service worker storage, and other browser-side data.

The graphics and compositing layer works with the CPU, GPU, and operating system to draw the page efficiently.

The security sandbox isolates websites and browser processes so that one malicious or broken page cannot easily compromise the entire system.

The operating system integration layer handles fonts, file pickers, input devices, clipboard access, camera and microphone permissions, notifications, downloads, native dialogs, accessibility APIs, and graphics APIs.

That last part is especially important for testing. Browsers are not floating in isolation. They sit on top of real operating systems.

What happens when you enter a URL?

When you type a URL into the address bar, the browser starts a chain of events.

First, it decides what the input means. If you type a complete URL, it navigates there. If you type words, it may perform a search using your default search engine. If the protocol is missing, it may infer HTTPS.

Next, the browser checks caches and internal data. It may look for existing DNS entries, cached resources, HSTS rules, service workers, cookies, and stored credentials.

Then DNS resolution happens. The browser needs to convert a domain name such as example.com into an IP address. This can involve the browser cache, operating system cache, router, ISP resolver, or a configured DNS provider.

After that, the browser establishes a connection. For HTTPS pages, it performs a TLS handshake to create an encrypted connection and verify the server certificate.

Then the browser sends an HTTP request. The server responds with HTML or another resource. The browser starts reading the response before the full page has necessarily downloaded.

This is where rendering begins.

The browser does not wait for every file before doing anything. It streams, parses, downloads, executes, lays out, and paints incrementally. This is why you can often see parts of a page before everything has finished loading.

HTML parsing and the DOM

HTML is the structure of the page.

When the browser receives HTML, it parses it into a tree-like structure called the DOM, or Document Object Model.

The DOM represents elements, attributes, text nodes, and relationships between elements. For example, a page might contain a header, navigation menu, main content area, form, buttons, and footer. In the DOM, those are represented as nodes in a tree.

The DOM is important because JavaScript can read and modify it.

When a script runs document.querySelector(), adds a class, changes text, inserts an element, removes a node, or attaches an event listener, it is interacting with the DOM.

The DOM is not the same thing as the original HTML source.

The original HTML is the text sent by the server. The DOM is the browser's live representation of the page after parsing, corrections, script execution, and dynamic updates.

This matters for testing because the DOM can change after the page loads. A test that tries to click an element too early may fail because the element has not been inserted yet. A test that relies on a fragile selector may fail when the frontend framework re-renders part of the page.

Many modern web apps generate most of their visible interface through JavaScript. In those apps, the initial HTML may be minimal, while the final DOM is built dynamically in the browser.

CSS parsing and the CSSOM

CSS describes how the page should look.

The browser parses CSS into another tree-like structure called the CSSOM, or CSS Object Model. The CSSOM contains rules, selectors, computed styles, media queries, animations, and other styling information.

To display a page, the browser combines the DOM and CSSOM.

It needs to determine which CSS rules apply to each element, which rules override other rules, which styles are inherited, which media queries match, and what each element's final computed style should be.

This can become complex quickly.

A button's final appearance might depend on browser defaults, a global reset stylesheet, a design system, component styles, hover states, focus states, disabled states, media queries, dark mode preferences, and inline styles added by JavaScript.

CSS also has browser-specific behavior around form controls, scrollbars, fonts, focus outlines, input rendering, date pickers, select dropdowns, and native controls.

That is one reason visual and functional testing should not assume that a page will behave exactly the same everywhere.

The render tree

After building the DOM and CSSOM, the browser creates a render tree.

The render tree contains the visual elements that need to be displayed. Not every DOM node appears in the render tree. For example, elements with display: none are not rendered. Metadata, script tags, and hidden elements may exist in the DOM but not produce visible boxes.

The render tree is a simplified representation of what needs to be drawn.

It includes visible elements and their computed styles. From there, the browser can calculate layout.

This distinction matters because tests often interact with DOM elements, but users interact with visible, rendered elements.

An element can exist in the DOM and still not be clickable. It may be hidden, covered by another element, outside the viewport, disabled, transparent, offscreen, or waiting for an animation to finish.

Good test automation has to account for the difference between "the element exists" and "the user can actually interact with it."

Layout: calculating positions and sizes

Layout is the process of calculating where elements appear and how large they are.

The browser considers the viewport size, CSS rules, fonts, content, images, flexbox, grid, positioning, margins, padding, borders, transforms, writing modes, and many other details.

A layout calculation answers questions such as:

  • How wide is this paragraph?
  • Where does this button appear?
  • How tall is this modal?
  • Does this item wrap to the next line?
  • Is this element inside the viewport?
  • Does the page need a scrollbar?

Layout is one of the places where small differences can create visible changes.

A font may render slightly wider on one operating system. A scrollbar may consume space on one platform and overlay content on another. A native input may have different default dimensions. A fractional pixel calculation may round differently. A browser zoom level or display scaling setting may affect the final result.

These differences can cause real bugs: clipped text, broken buttons, misaligned dropdowns, overlapping modals, hidden checkout controls, or mobile navigation that fails at a specific viewport width.

Paint and compositing

After layout, the browser paints visual elements.

Painting means drawing backgrounds, text, borders, shadows, images, gradients, and other visual details.

But modern browsers do not simply paint the whole page from top to bottom every time something changes. That would be too slow.

Instead, browsers use layers and compositing.

Some parts of a page may be promoted to separate layers, especially when using transforms, opacity, fixed positioning, video, canvas, or animations. The browser can then move or update those layers more efficiently, often using the GPU.

This is why some animations are smooth and others are janky. Changing transform or opacity is often cheaper than changing layout-affecting properties such as width, height, margin, or top.

Compositing is also another reason real environments matter.

Graphics APIs, GPU drivers, operating system rendering pipelines, display scaling, font smoothing, and hardware acceleration can all affect how a page appears and performs.

A visual issue that does not show up in a Linux container may still appear on a real Windows or macOS browser.

JavaScript execution

JavaScript makes web pages interactive.

It can modify the DOM, fetch data, validate forms, handle clicks, update routes, render components, show modals, store data, manage state, communicate with APIs, and run business logic in the browser.

JavaScript is executed by the browser's JavaScript engine. The engine parses the code, compiles it, optimizes it, executes it, and manages memory.

Modern JavaScript engines are extremely advanced. They use just-in-time compilation, optimization heuristics, garbage collection, hidden classes, inline caching, and other techniques to make JavaScript fast.

But JavaScript execution is not isolated from the rest of the browser.

A script can trigger layout changes. It can block rendering. It can delay user interaction. It can add event listeners. It can schedule asynchronous work. It can create memory leaks. It can depend on APIs that behave differently across browsers.

For testing, JavaScript timing is one of the biggest sources of flaky behavior.

A test may fail because an API response took longer than usual, a component re-rendered after the test found an element, an animation had not finished, or a button became clickable only after asynchronous validation completed.

This is why reliable web testing requires more than "find element and click." It requires waiting for the right user-visible state.

The event loop

The browser has to remain responsive while doing many things at once.

It needs to run JavaScript, respond to user input, process timers, handle network responses, update animations, perform layout, paint the screen, and execute background tasks.

The event loop coordinates this work.

JavaScript in the browser is mostly single-threaded from the perspective of page code. That means long-running JavaScript can block the main thread and make the page feel frozen.

When a user clicks a button, the browser adds an event to be handled. When a timer completes, its callback is scheduled. When a promise resolves, its continuation is queued. When a network response arrives, JavaScript may be scheduled to process it.

The browser constantly moves between tasks, microtasks, rendering opportunities, and user input.

Understanding this explains many frontend bugs.

A page can look loaded but still be processing JavaScript. A button can be visible but not yet wired to its event handler. A spinner can disappear before data is fully rendered. A route can change while a test is trying to interact with a stale element.

For real users, this creates frustration. For automated tests, it creates flakiness.

Browser networking

Every modern web app depends heavily on browser networking.

The browser fetches HTML, CSS, JavaScript, images, fonts, API responses, analytics scripts, feature flags, videos, and third-party resources.

It also manages:

  • HTTP caching
  • Cookies
  • Redirects
  • Compression
  • TLS certificates
  • CORS rules
  • Preflight requests
  • Connection reuse
  • Service workers
  • Request prioritization
  • Mixed content blocking
  • Content Security Policy

The browser does not download files randomly. It prioritizes resources based on what is needed for rendering and interactivity.

CSS needed for the page may block rendering. JavaScript may block parsing depending on how it is loaded. Images may be lazy-loaded. Fonts may swap after text is first displayed. API responses may arrive in different orders.

This has direct testing implications.

A page may pass when the network is fast and fail when the network is slower. A modal may open before tracking scripts finish. A font may load late and shift layout. A cached resource may hide a bug that appears for first-time users.

Realistic browser testing should account for the fact that the browser is a networked runtime, not a static page renderer.

Browser storage and state

Browsers store a lot of data.

Cookies are often used for sessions, authentication, tracking, preferences, and experiments.

localStorage and sessionStorage store key-value data in the browser.

IndexedDB can store larger structured data, often used by offline-capable apps.

The Cache API stores network responses, especially for progressive web apps.

Service workers can intercept requests and serve cached content, even when the network is unavailable.

Browsers also store permissions, autofill data, passwords, history, downloads, and site settings.

This makes testing more complicated because browser state can change the behavior of the application.

A first-time visitor may see a cookie banner. A returning user may not. A logged-in user may bypass the login page. A user with stale localStorage may see an old experiment. A service worker may serve an outdated file. A permission prompt may appear only once.

Reliable test automation should control browser state deliberately. Sometimes tests should start with a clean browser profile. Sometimes they should preserve state to simulate returning users. The important part is to be intentional.

Security and sandboxing

Browsers are one of the most attacked pieces of software in the world because they run untrusted code from the internet.

To reduce risk, modern browsers use multiple security layers.

Same-origin policy prevents one website from freely reading data from another website.

CORS controls which cross-origin requests are allowed.

Content Security Policy helps prevent certain types of injection attacks.

Site isolation and process sandboxing limit the damage a malicious page can cause.

HTTPS protects data in transit and verifies server identity.

Permission prompts protect access to sensitive capabilities such as camera, microphone, location, clipboard, notifications, and file system features.

Download protections warn users about dangerous files.

Mixed content blocking prevents secure pages from loading insecure resources in risky ways.

For developers and testers, security features affect real application behavior.

A cross-origin API request may fail in one environment. A permission prompt may block a test. A third-party iframe may be restricted. A file upload flow may behave differently depending on the operating system's native file picker. A clipboard operation may work only after a user gesture.

Browsers enforce rules that application code cannot simply ignore.

The browser engine matters because different engines can interpret and implement web standards differently.

Chrome, Edge, Brave, Opera, and many other browsers are based on Chromium, which uses Blink for rendering and V8 for JavaScript.

Safari uses WebKit and JavaScriptCore.

Firefox uses Gecko and SpiderMonkey.

Web standards reduce differences, but they do not eliminate them. Each engine has its own implementation details, bugs, performance characteristics, release schedule, and platform integrations.

Even when two browsers support the same feature, edge cases can differ.

A CSS layout may render differently. A JavaScript API may have a subtle behavior difference. A media format may be supported in one browser and not another. A form control may look and behave differently. A focus state may be handled differently. A drag-and-drop flow may work in one browser and fail in another.

This is why testing only in one browser is risky.

It is also why testing only in Chromium is not the same as testing the web application properly. Chrome compatibility does not automatically mean Safari compatibility or Firefox compatibility.

Why Chrome can behave differently across operating systems

Even within the same browser family, the operating system matters.

Chrome on Windows, Chrome on macOS, and Chrome on Linux share a lot of code, but they are not the same complete environment.

The browser depends on the operating system for many things:

  • Font rendering
  • Available system fonts
  • Emoji rendering
  • Scrollbar behavior
  • Display scaling
  • GPU acceleration
  • Video decoding
  • Audio handling
  • File picker dialogs
  • Clipboard access
  • Permission prompts
  • Window management
  • Touchpad and mouse input
  • Keyboard shortcuts
  • Accessibility APIs
  • Notification behavior
  • Certificate stores
  • Network configuration

A web app may look and behave almost identical across platforms most of the time. But "almost" is where many production bugs live.

A text label may wrap on Windows but not on macOS. A font fallback may change the size of a button. A date input may behave differently. A download flow may trigger a different native dialog. A file upload test may need to interact with a real operating system file picker. A video may decode differently. A permission prompt may appear in a different way. A keyboard shortcut may conflict with an OS-level shortcut.

This is why serious cross-platform testing should include real browsers on real Windows and macOS machines.

Linux containers are useful for many automation workloads. They are fast, cheap, and convenient. But they do not perfectly represent how real users experience Chrome, Edge, Firefox, or Safari on desktop operating systems.

If your customers use Windows and macOS, testing only inside Linux containers can leave important gaps.

Headless browsers vs real browsers

Headless browsers are browsers that run without a visible graphical interface.

They are useful for fast checks, scraping, simple automation, and CI pipelines. Many teams use headless Chromium because it is easy to run in containers and scales well.

But headless execution is not always the same as real user execution.

Historically, headless modes had differences in rendering, layout, media handling, permissions, downloads, GPU behavior, and browser APIs. Modern headless browsers have improved a lot, but the core issue remains: a simplified execution environment may not expose every real-world problem.

A test can pass in headless Chromium on Linux and still fail for a real user on Chrome on Windows or Safari on macOS.

That does not mean headless testing is bad. It means teams should understand what headless testing is good for.

Headless testing is useful for speed and broad regression coverage. Real browser testing is important for confidence in user-facing flows, visual behavior, cross-browser compatibility, and platform-specific issues.

The strongest testing strategy often combines both.

Why browser testing is harder than it looks

Many teams underestimate browser testing because they think of a browser as a predictable interpreter of web standards.

In reality, a browser is a complex runtime with many moving parts.

Your app may depend on:

  • Browser engine behavior
  • JavaScript timing
  • DOM updates
  • CSS layout rules
  • Network responses
  • Authentication state
  • Cookies
  • Browser storage
  • Third-party scripts
  • Service workers
  • Fonts
  • Media codecs
  • Permissions
  • Native OS dialogs
  • Viewport size
  • Device pixel ratio
  • GPU compositing
  • Accessibility APIs
  • Browser extensions
  • User settings

That complexity creates failure modes that are hard to reproduce.

A bug may happen only after login, only after a service worker update, only on Safari, only when the network is slow, only when a browser has cached an old script, only on Windows because a font renders differently, or only on macOS because of native scrolling behavior.

Testing one happy path in one browser is rarely enough for important applications.

How browsers affect test automation

Automated tests interact with browsers in ways that are similar to users, but not identical.

A test automation tool needs to locate elements, wait for them, click them, type into them, upload files, read text, verify results, handle tabs, manage downloads, deal with alerts, and recover from timing issues.

The browser can make any of those actions tricky.

An element might be in the DOM but not visible, or visible but covered by a sticky header. It might move during an animation, become stale after a frontend framework re-renders it, sit inside an iframe, require scrolling, require a real user gesture, or trigger a native permission prompt.

This is why reliable test automation requires browser-aware behavior.

It is not enough to record a click and replay it blindly. The automation layer needs to understand waiting, visibility, actionability, navigation, page state, browser differences, and recovery from UI changes.

This is also where no-code and AI-assisted testing platforms can help, especially when they generate editable test steps instead of opaque scripts. Teams can create coverage faster while still keeping tests understandable and maintainable.

For teams that need broad browser and operating system coverage without maintaining their own grid, Endtest provides cross-browser testing on real machines, including major browsers on Windows and macOS.

Why real Windows and macOS testing still matters

Most web applications are built on developer machines, tested in CI, and used by customers across many environments.

Those environments are not identical.

A developer may build on macOS, run CI in Linux, and have customers using Chrome and Edge on Windows. Another team may test mainly in Chrome but have a large number of Safari users. Another may rely on containerized browser testing and miss native desktop issues.

Real Windows and macOS testing is important because desktop browsers are browser engines plus operating system integration.

That integration affects the user experience.

Windows users may see different scrollbars, font rendering, file dialogs, download behavior, certificate handling, and display scaling. macOS users may see different trackpad behavior, font smoothing, permission prompts, keyboard shortcuts, and Safari-specific behavior.

Safari is especially important because it is not a Chromium browser. It uses WebKit, and WebKit has its own implementation details. Testing WebKit in a Linux environment is not the same as testing real Safari on macOS.

If your web app is business-critical, revenue-generating, or used by customers across different platforms, testing should reflect that reality.

The browser rendering pipeline in plain English

The browser rendering pipeline can be summarized like this:

  1. Download the HTML.
  2. Parse HTML into the DOM.
  3. Download and parse CSS into the CSSOM.
  4. Combine DOM and CSSOM into a render tree.
  5. Calculate layout.
  6. Paint visual elements.
  7. Composite layers onto the screen.
  8. Run JavaScript that may change everything.
  9. Repeat parts of the process as the page updates.

The important part is that this pipeline is not a one-time process.

Modern web apps constantly update. JavaScript changes state, API responses arrive, components re-render, CSS classes change, animations run, users interact, and layout shifts.

Every meaningful user flow is a moving target.

A login page, checkout form, dashboard, admin panel, or onboarding flow may go through many browser states before the user reaches the final result.

Good testing needs to validate the whole flow, not only the initial page load.

Some bugs are clearly application logic bugs. Others are browser-related or browser-specific.

Here are common examples.

  • A layout bug appears only on one browser because flexbox or grid behaves differently in an edge case.
  • A button is hidden behind a sticky footer on one viewport size.
  • A modal cannot be closed because focus is trapped incorrectly.
  • A form works in Chrome but fails in Safari because of a date input, autofill behavior, or JavaScript API difference.
  • A file upload flow fails because the test environment does not handle native file dialogs like a real machine.
  • A checkout page breaks because a third-party iframe behaves differently under stricter browser privacy rules.
  • A drag-and-drop feature works with a mouse but not with a trackpad.
  • A font loads differently on Windows, causing text to wrap and push a button below the fold.
  • A download verification passes in one browser but fails in another because file handling differs.
  • A permission prompt blocks the page only in a real browser profile.
  • A service worker serves stale assets after deployment.
  • A test passes in Linux headless Chrome but fails in real Chrome on Windows.

These bugs are common once web apps become complex and widely used.

How to think about browser compatibility

Browser compatibility should not mean testing every possible combination equally.

That would be expensive and unnecessary for most teams.

A practical strategy starts with user risk.

Which browsers do your customers use? Which operating systems matter? Which flows generate revenue? Which flows create support tickets when they fail? Which features depend on browser APIs? Which parts of the app have the most frontend complexity?

From there, you can prioritize.

Critical flows should run across major browsers and operating systems. Login, signup, checkout, payments, billing, onboarding, account settings, file uploads, dashboards, and core workflows deserve broader coverage.

Less critical flows can run in fewer environments or be tested periodically.

The goal is to catch the bugs that would matter most if they reached users, not to test everything everywhere.

What developers should know about browsers

Developers do not need to understand every internal browser optimization, but they should understand the basic model.

HTML creates structure. CSS creates presentation. JavaScript creates behavior. The browser turns those into a live, interactive interface.

Performance problems often come from blocking the main thread, triggering too much layout, loading too much JavaScript, using inefficient selectors, or forcing the browser to recalculate styles repeatedly.

Compatibility problems often come from assuming all browsers and operating systems behave the same.

Testing problems often come from assuming that the DOM is stable, that visibility equals clickability, or that a browser in CI represents a real user environment.

When developers understand how browsers work, they write better frontend code and better tests.

They become more careful with layout, loading states, accessibility, focus management, browser APIs, and state cleanup.

What QA teams should know about browsers

QA teams should think of the browser as part of the system under test.

The application is the complete user experience inside a browser, on a real platform, under real conditions, not only backend logic and frontend code.

That means QA should pay attention to:

  • Browser and OS combinations
  • Viewport sizes
  • Login state
  • Browser storage
  • Permission prompts
  • File upload and download behavior
  • Cross-origin content
  • Third-party scripts
  • Network variability
  • Visual layout differences
  • Accessibility behavior
  • Timing and async UI updates

A test strategy that ignores the browser environment will miss important classes of bugs.

This is especially true for teams that rely heavily on automated testing. Automation should make coverage broader and more repeatable, but it should not create false confidence by running only in simplified environments.

The future of browsers and testing

Browsers keep getting more powerful.

Web apps now use APIs for video calls, local databases, offline behavior, push notifications, payments, passkeys, file system access, WebAssembly, WebGL, WebGPU, machine learning, and advanced media processing.

The browser is becoming more like an operating system for applications.

That makes testing even more important.

As web applications become richer, the gap between "the page loaded" and "the user experience works correctly" becomes larger.

AI can help teams create, maintain, and understand tests faster. But AI does not remove the need to test in real browsers. In many cases, it makes browser testing more important because development moves faster, more code changes are shipped, and teams need faster feedback across more environments.

More automation alone does not cover that. It has to run in realistic environments.

Final thoughts

A web browser is one of the most complex pieces of software that most people use every day.

It downloads resources, parses code, builds the DOM, applies CSS, executes JavaScript, calculates layout, paints pixels, manages storage, enforces security, handles permissions, and integrates deeply with the operating system.

That complexity is why web applications can behave differently across browsers, and even across the same browser on different operating systems.

Chrome on Windows is not exactly the same real-world environment as Chrome on macOS or Chrome on Linux. Safari is not the same as Chromium. A headless browser in a Linux container is not the same as a real browser used by a real customer.

For low-risk checks, simplified browser environments can be useful. For critical user journeys, teams should test on the browsers and operating systems their customers actually use.

That means all major browsers, real Windows and macOS machines, and realistic execution environments.

Get started with Endtest today!

Create your first test in minutes, with nothing to install. Build fast, maintainable test suites without writing a line of code.