Skip to content

    Frontend System Design

    How a Browser Actually Works

    Trace a page from URL to pixels through a browser's processes and threads, then use that model to keep the main thread free and isolate third-party widgets.

    By Published 20 min read

    • frontend-system-design
    • browser-architecture
    • site-isolation
    • main-thread
    • rendering
    Share

    This lesson is part of Frontend System Design Foundations: How the Web Runs. Enroll for free, then sign in, to read it in full.

    What you'll learn: By the end of this lesson you will be able to name the processes and threads a browser uses and what each is responsible for, trace the path from typing a URL to an interactive page, explain how a click reaches your event handler and why a busy main thread makes a page feel frozen, say what the same-origin policy and site isolation protect, and turn that model into concrete front-end design decisions.

    Think of an office building. At the front desk, a receptionist checks visitors in and routes parcels. Each company rents its own locked office, so a fire in one office does not burn the paperwork of another. A mailroom handles everything that comes and goes, and a print room with special equipment produces the finished posters. Nobody walks into another company's office just because they are in the same building.

    A modern browser is organised the same way. The front desk is the browser process. The locked offices are renderer processes, where web pages live. The mailroom is the network service and the print room is the GPU process. This layout explains why a crashed tab does not take down your browser, why a third-party iframe cannot read your page, and why one slow script can make a whole page feel stuck while the rest of the browser stays alive.

    The running example is a product page with three parts: an image carousel, a search box with suggestions, and a third-party chat widget loaded in an iframe from another company's domain. Each part touches a different corner of the browser's design.

    This lesson describes Chromium, the open-source engine behind Chrome and Edge, because it has the most thorough public documentation. Other engines appear only where their own project documentation says something specific. Companion lessons (The Critical Rendering Path, The Event Loop and the Main Thread, HTTP Caching for the Front End, Rendering Strategies, Loading Performance and Core Web Vitals) go deeper on single stages. This lesson is the map.

    The core idea

    A browser downloads code from strangers and runs it on your machine, so it is built like a small operating system: it isolates untrusted work, schedules it and shares the screen. Its main pieces are the user interface (address bar, tabs), the networking layer (DNS, connections, TLS, HTTP, caching), the rendering engine that turns HTML, CSS and images into pixels (Blink in Chromium, per the Chromium multi-process architecture page), the JavaScript engine (V8 in Chromium) and storage and platform glue such as cookies and file access.

    How the pieces are split across operating system processes (programs with private memory) and threads (parallel workers inside a process) is an implementation choice. The Chrome team says there is no standard specification for how to build a browser (Inside look at modern web browser, part 1). Standards define what pages can do; process layouts are what one browser does.

    Three goals drive the split into processes:

    1. Stability. If a renderer hangs or crashes, other tabs keep working. Chromium shows a "sad tab" or "sad frame" for the affected pages and creates a new renderer on reload.
    2. Security. A renderer runs in a sandbox, a restricted environment with little access to files, display, input devices or network. Anything privileged goes through the browser process.
    3. Performance. Slow work in one page does not block the browser's interface, and drawing can run in parallel on other cores.

    The processes in Chromium

    Figure 1. Chromium's process layout for the product page

    Chromium processes while the product page is openBrowser process to Renderer: shop page (IPC); Browser process to Renderer: chat iframe (IPC); Browser process to Network service (requests); Renderer: shop page to GPU process (frames); Renderer: chat iframe to GPU process (frames)SANDBOXED RENDERERSBrowser process: UI, coordinationBrowserprocessUI, coordinationNetwork service: utility processNetworkserviceutility processRenderer: shop page: page scriptsRenderer: shoppagepage scriptsRenderer: chat iframe: other site's codeRenderer: chatiframeother site's codeGPU process: raster, drawGPU processraster, drawIPCIPCrequestsframesframes

    How to read this: the browser process is the hub. Each renderer holds one site's code and talks to the browser process over IPC (inter-process communication, messages between processes). Renderers hand finished frames to the GPU process. The chat iframe sits in its own renderer because it comes from a different site.

    Process What it does Notes from the sources
    Browser Runs the browser's own interface (address bar, bookmarks, back and forward), manages other processes, and does privileged work such as network requests and file access One per browser. It is the "broker" that sandboxed processes ask for permitted actions.
    Renderer Runs everything inside a tab's web page: parsing, scripts, style, layout, painting, input handling Several. In Chromium's RenderingNG documentation, one renderer handles one site and tab combination (RenderingNG architecture).
    GPU Aggregates frames from renderers and the browser UI, rasterises and draws using the graphics hardware The RenderingNG documentation calls this role Viz and says there is one for all of Chromium. The older "Inside look" series calls it the GPU process.
    Network service Low-level networking: HTTP, sockets, WebSockets A utility process by default on most platforms. On Android it runs inside the browser process (network service README).

    How many processes? It varies

    There is no fixed answer. The "Inside look" article says Chrome caps the number of processes it starts, depending on the device's memory and CPU, and past the cap runs several tabs from the same site in one process. Chromium's process model document is more specific, at the time of writing (Process Model and Site Isolation):

    • Desktop uses full site isolation with a soft process limit based on available memory, which can be exceeded when site isolation needs more processes.
    • Android with 2 GB of RAM or more uses partial site isolation (only some sites, chosen by heuristics such as login or password entry) and relies on the operating system to discard processes.
    • Low-memory Android, Android WebView and Chrome for iOS have no site isolation.

    So "every cross-site iframe is in its own process" is the desktop Chrome case, not a guarantee.

    How other engines differ

    The same goals led other engines to the same broad shape, with differences in detail. The table lists only what each project's own documentation states, at the time of writing.

    Engine What the documentation says
    WebKit (Safari) A UI process (the application), WebContent processes that load pages and are tightly sandboxed (no file system access unless the user grants it), and a shared Networking process that handles network access, the disk cache and storage APIs (WebKit2 architecture). It says web pages are loaded in their own WebContent process but does not spell out the exact rule for tabs and sites.
    Gecko (Firefox) A parent process (not sandboxed) and sandboxed content processes, plus helpers such as a GPU process that is "not used on all platforms". With Fission enabled, content from each site goes to its own webIsolated process; without it, most web content shares a pool of web processes (Gecko process model).

    All three keep web code in sandboxed processes apart from a privileged one. How finely they split sites, and on which platforms, differs and changes between versions.

    From URL to interactive page

    DNS, TCP, TLS and HTTP are covered in the request lifecycle. Here the focus is what the browser itself does around that exchange, following Chrome's "Inside look" part 2.

    Figure 2. Navigation: from address bar to load event

    sequenceDiagram accTitle: Navigation lifecycle in Chromium accDescr: The browser process sends a request to the server, checks the response, picks a renderer process, commits the navigation, and the renderer parses, renders and reports that loading finished. participant U as You participant B as Browser process participant S as Server participant R as Renderer process U->>B: type text, press Enter B->>B: search query or URL? B->>S: DNS, connect, TLS, request B-->>R: start a renderer early (in parallel) S-->>B: response headers and first bytes B->>B: sniff type, safe-browsing check B->>R: commit navigation (IPC, data stream) R-->>B: commit confirmed Note over B: address bar, history updated R->>R: parse HTML, find subresources R->>B: ask for CSS, JS, images R->>R: style, layout, paint, composite R-->>B: load event done (IPC) Note over B: loading spinner stops

    How to read this: time runs downward. The browser process (its UI and network threads) owns the network and the decision of where a page runs; the renderer owns everything after the first bytes of HTML.

    In steps:

    1. Handle input. The browser process decides whether what you typed is a search query or a URL.
    2. Start the request. A network thread does the DNS lookup and TLS setup and sends the request. If the server answers with a redirect such as 301, the browser starts another request.
    3. Read the response. The browser checks the first bytes to work out the content type, because the Content-Type header can be missing or wrong. HTML goes to a renderer; a zip file goes to the download manager. A Safe Browsing check can show a warning page, and Cross-Origin Read Blocking (the term in Chrome's 2018 article; Chromium's newer mechanism is called Opaque Response Blocking) keeps some sensitive cross-site data out of the renderer.
    4. Pick a renderer. The browser already knows the target site, so it can start a renderer in parallel with the request. The spare may go unused if the navigation redirects to another site.
    5. Commit. The browser process tells the renderer over IPC to commit the navigation and hands it the response stream. Once the renderer confirms, the address bar, security indicator and history update.
    6. Parse and render. The renderer parses HTML into the DOM (the in-memory tree of the page that scripts can read and change). A preload scanner looks ahead for images and stylesheets and requests them early. A synchronous <script> can pause parsing; async and defer avoid that. The Critical Rendering Path lesson covers this stage.
    7. Finish. After onload has fired in every frame, the renderer tells the browser process and the spinner stops. Scripts can still load more afterwards, so "loaded" is not "finished".

    Two more details. Service workers (scripts between the page and the network) are consulted on navigation; if a scope matches, the browser starts a renderer to run the worker, which may answer from a cache (HTTP Caching for the Front End covers this). And navigating to a different site leaves the old renderer alive to run beforeunload and unload handlers while a new renderer takes the new page; the article warns against unconditional beforeunload handlers.

    Inside one renderer: threads

    A renderer has several threads (RenderingNG architecture):

    • The main thread runs your scripts, the rendering event loop, the document lifecycle (style, layout, paint), hit testing, event dispatch and HTML parsing.
    • The compositor thread processes input, performs scrolling and animations, decides how the page is split into layers, and coordinates raster work. There is one per renderer.
    • Raster work turns drawing instructions into GPU texture tiles. The compositor coordinates it and the GPU process executes it.
    • Worker threads run web workers and service workers.

    The crucial fact: your JavaScript, style, layout and paint share one main thread, which does one task at a time. While it is busy it cannot run your next handler or produce the next animation frame.

    Figure 3. Who does what between the click and the screen

    Renderer threads and the GPU processBrowser process to Compositor thread (input); Compositor thread to Main thread (events to handle); Main thread to Compositor thread (commit); Compositor thread to GPU process (frames); GPU process to Screen (draw)RENDERER PROCESSBrowser process: routes inputBrowserprocessroutes inputCompositor thread: scroll, animateCompositorthreadscroll, animateMain thread: JS, style, layout, paintMain threadJS, style,layout, paintGPU process: raster, aggregate, drawGPU processraster,aggregate, drawScreenScreeninputevents to handlecommitframesdraw

    How to read this: input lands on the compositor thread first. Anything that needs your code goes down to the main thread, and the results are committed back up. The compositor can still produce frames for work it handles itself, such as scrolling, without waiting for the main thread.

    Where each stage runs, per the same documentation:

    Stage Where it runs
    Run scripts, parse HTML Main thread
    Style, layout, pre-paint, paint, commit Main thread
    Animate, scroll Main thread or compositor thread
    Layerize (split into layers) Compositor thread
    Raster (make pixel tiles) Coordinated by the compositor, executed in the GPU process
    Aggregate frames, draw GPU process

    Why compositor-friendly animation matters

    The main thread produces paint records (ordered drawing instructions). The compositor splits the page into layers and tiles, raster turns tiles into bitmaps in GPU memory, and the compositor gathers them into a compositor frame.

    If an effect can skip layout and paint, it can run entirely on the compositor thread without the main thread (RenderingNG). web.dev puts it practically: the only two properties that avoid both are transform and opacity, and the element needs its own layer, which will-change can request. Each layer costs memory and GPU bandwidth, so promote only what you animate (web.dev).

    For the carousel, compare two ways to slide the images:

    /* Layout-triggering: changes geometry, so the main thread
       re-runs layout and paint on every frame. */
    .track-slow {
      position: relative;
      left: 0;
      transition: left 300ms ease;
    }
    .track-slow.next { left: -100%; }
    
    /* Compositor-friendly: transform changes need no layout or paint,
       so the compositor can move the layer by itself. */
    .track-fast {
      will-change: transform;
      transition: transform 300ms ease;
    }
    .track-fast.next { transform: translateX(-100%); }

    Both look the same, but only the second can keep moving while the main thread is busy. Confirm in the DevTools Performance panel on a throttled device; thread assignment can differ by browser and version.

    Figure 4. Where does this visual change run?

    flowchart TD accTitle: Where a visual change is processed accDescr: A decision tree. Changes limited to transform or opacity on a promoted element run on the compositor. Changes to geometry need layout and paint on the main thread. Others need paint only. A["I change an element's appearance"] --> B{"Only transform or opacity?"} B -->|yes| C{"Element has its own layer?"} C -->|yes| D["Compositor thread
    no layout, no paint"] C -->|no| E["Main thread may still paint
    (check in DevTools)"] B -->|no| F{"Does it change size or position?"} F -->|yes| G["Main thread: layout,
    paint, then compositor"] F -->|no| H["Main thread: paint,
    then compositor"]

    How to read this: the further left and up you stay, the less main-thread work you create. Layout-affecting changes cost the most because each later stage depends on the previous one.

    From click to handler, and the frozen page

    Now the search box. A user taps it and types. What happens?

    Figure 5. A click travels from the OS to your handler

    sequenceDiagram accTitle: Input event path from operating system to event handler accDescr: The operating system gives the click to the browser process, which sends the event type and position to the renderer. The compositor thread forwards it to the main thread, which hit tests and runs the handler. If the main thread is busy, the event waits. participant OS as Operating system participant B as Browser process participant C as Compositor thread participant M as Main thread OS->>B: pointer event for the window Note over B: knows only where it happened B->>C: event type and coordinates (IPC) C->>M: forward event for handling Note over M: if a long task is running,
    the event waits in the queue M->>M: hit test, find target, run listeners M->>C: commit updated page C-->>OS: frame shown via the GPU process

    How to read this: the browser process only knows the position of a gesture. The renderer finds what is under it and runs your listeners, and that step is on the main thread.

    Chrome's part 4 explains the details that matter:

    • The browser process sends the event's type (for example touchstart) and coordinates to the renderer, which finds the target and runs the listeners.
    • The compositor marks regions with event handlers as the non-fast scrollable region. Input outside it can be handled without waiting for the main thread; input inside it goes to the main thread first.
    • One listener on document.body (event delegation) marks the whole page non-fast scrollable. { passive: true } tells the browser the listener will not block scrolling.
    • Continuous events (wheel, mousemove, pointermove, touchmove) are coalesced and dispatched just before the next requestAnimationFrame; discrete events (keydown, mousedown, touchstart) are dispatched immediately.
    // Passive: the compositor can keep scrolling the carousel while this runs.
    carousel.addEventListener("touchmove", onDrag, { passive: true });
    
    // Delegation is fine for clicks. Avoid one listener on document.body
    // for scroll-blocking events such as touchmove or wheel.
    carousel.addEventListener("click", (event) => {
      const dot = event.target.closest("[data-slide]");
      if (dot) showSlide(Number(dot.dataset.slide));
    });

    Why a busy main thread feels like a frozen page

    The compositor can scroll, but a click, a keystroke or a JavaScript-driven animation needs the main thread, and a long task makes them wait. web.dev defines a long task as one running longer than 50 milliseconds and notes the browser cannot respond to interactions while a task runs (Optimize long tasks). The delay before the handler starts is input delay, one component of Interaction to Next Paint (INP), covered in the Loading Performance and Core Web Vitals lesson.

    Here is a deliberately bad search handler and a way to see the problem:

    const input = document.querySelector("#search");
    
    input.addEventListener("input", () => {
      const start = performance.now();
      while (performance.now() - start < 300) {
        // simulates 300 ms of synchronous work (illustrative)
      }
    });
    
    // Chromium-based browsers can report tasks of 50 ms or more.
    // Check MDN's compatibility table before relying on it.
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        console.log(`long task: ${Math.round(entry.duration)} ms`);
      }
    }).observe({ type: "longtask", buffered: true });

    Each keystroke starts a task of about 300 ms, so the observer logs a long task of roughly that length per keystroke. Meanwhile the typed letter does not appear, a JavaScript-driven animation stalls and a click on a carousel dot waits, while a CSS transform transition on a promoted layer can continue. A large JSON parse, a big list render or a heavy third-party script has the same effect.

    The fix is to split the work and yield so input is handled between pieces:

    // scheduler.yield() is available in Chrome and Edge 129+ and Firefox 142+
    // at the time of writing; not in Safari. Fall back to a timer.
    function yieldToMain() {
      if (globalThis.scheduler?.yield) return scheduler.yield();
      return new Promise((resolve) => setTimeout(resolve, 0));
    }
    
    async function filterSuggestions(items, query) {
      const results = [];
      let last = performance.now();
      for (const item of items) {
        if (item.name.toLowerCase().includes(query)) results.push(item);
        if (performance.now() - last > 50) { // yield about every 50 ms
          await yieldToMain();
          last = performance.now();
        }
      }
      return results;
    }

    The Event Loop and the Main Thread lesson covers tasks, microtasks and workers. The architectural principle: everything on the main thread competes with the user's input.

    What the same-origin policy and site isolation protect

    Two protections sit on top of the process layout, and they are different things.

    The same-origin policy is a rule of the web platform. An origin is the combination of scheme, host and port, so https://shop.example and https://chat.example are different origins. MDN summarises the policy: cross-origin writes (links, form posts) are typically allowed, embedding (images, scripts, iframes) is typically allowed, and reads are typically disallowed (MDN: Same-origin policy). For a cross-origin iframe, the parent gets only a very small set of window properties and methods, among them postMessage. The parent cannot read the iframe's DOM, and the iframe cannot read the parent's.

    Site isolation is Chromium's defence in depth for that policy. A site is the scheme plus the registered domain (so https://a.shop.example:8080 is in the site https://shop.example). Site isolation puts pages from different sites in different processes, including cross-site iframes ("out-of-process iframes"). The reason is that Chrome assumes an attacker might compromise a renderer, or read its memory with a side-channel attack such as Spectre. If another site's data is not in that process, there is less to steal (Chromium: Site Isolation).

    Details worth remembering:

    • Process boundaries follow sites, not origins. shop.example and chat.shop.example are the same site and can share a process, so a widget on a subdomain of your site is not isolated from your page.
    • It was enabled by default on desktop from Chrome 67. The Chromium page reports roughly 10 to 13 percent more memory use on desktop (Chrome 67, many tabs) and 3 to 5 percent on Android (Chrome 77): figures for those versions, not a general rule.
    • It is not available everywhere (see the platform list above). The same-origin policy and server-side controls remain the base layer.

    Figure 6. Should this third-party code run in my page or in a frame?

    flowchart TD accTitle: Choosing how to embed third-party code accDescr: A decision tree for embedding third-party code. If it needs your page's DOM, a script in your page gets the same trust as your own code. Otherwise a cross-origin sandboxed iframe on a different site is preferred, with postMessage for communication. A["Third-party widget to embed"] --> B{"Does it need your page's DOM
    or your users' cookies?"} B -->|yes| C["Script in your page
    = full trust, same main thread"] B -->|no| D["Cross-origin iframe
    on a different site"] D --> E["Add sandbox and allow lists"] E --> F["Talk through postMessage
    and check event.origin"] C --> G["Review, pin versions,
    measure its long tasks"]

    How to read this: embedding by script gives the vendor everything your own code has, and shares your main thread. An iframe on another site gets its own origin, usually its own process on desktop, and only the access you grant.

    Here is the chat widget embedded as a cross-site, sandboxed iframe, with a safe message exchange:

    <iframe
      src="https://chat.vendor.example/widget"
      title="Customer chat"
      sandbox="allow-scripts allow-forms allow-popups"
      loading="lazy"
      width="360"
      height="480"
    ></iframe>
    const chat = document.querySelector('iframe[title="Customer chat"]');
    
    // Parent to widget: name the exact target origin.
    chat.contentWindow.postMessage(
      { type: "set-product", sku: "A-100" },
      "https://chat.vendor.example"
    );
    
    // Widget to parent: always check who sent it.
    window.addEventListener("message", (event) => {
      if (event.origin !== "https://chat.vendor.example") return;
      if (event.data?.type === "unread") updateBadge(event.data.count);
    });

    Notes on this snippet, from MDN's iframe reference: an empty sandbox applies all restrictions and each token lifts one. Without allow-same-origin the frame is treated as a special origin that fails the same-origin check, which can block the frame's access to its own cookies and storage. If your widget needs its own storage, add allow-same-origin, which is safe here only because the frame's origin differs from yours. MDN strongly discourages combining allow-scripts and allow-same-origin for a same-origin embed, because the frame can then remove its own sandbox. loading="lazy" defers the frame until it nears the viewport. Add an allow attribute only for features the widget needs; it sets a Permissions Policy on top of any header.

    WARNING

    An iframe protects trust, not speed. The frame's memory and processing still cost something ("Every <iframe> in a page requires increased memory and other computing resources", MDN). And many chat vendors also ask you to add a loader <script> to your page. That script runs on your main thread with your page's privileges, even if the chat UI lives in the frame.

    For deeper treatment of identity, cookies and defences on the server, see System Design: Security.

    What this means for design decisions

    Decision Why the browser's design says so What to do
    Keep the main thread free Scripts, style, layout, paint and input handling share it Break up work over 50 ms; use workers; yield between chunks
    Animate transform and opacity They can run on the compositor without layout or paint Use them for the carousel; promote only what you animate
    Avoid broad scroll-blocking listeners One listener on body makes the whole page non-fast scrollable Use { passive: true }
    Treat third-party iframes as separate trust zones Different origins and (often) different processes Cross-site host, sandbox, allow, postMessage with origin checks
    Treat third-party scripts as your own code Same main thread, same privileges Budget them, load them late, measure their long tasks
    Do not rely on one process per iframe Process counts vary by platform and memory Make correctness and security independent of it
    NOTE

    This is a realistic scenario, not a case history. It is a composite built from common front-end patterns. It does not describe any specific company, and all numbers are illustrative assumptions.

    The situation

    A retail product page has an image carousel, a search box with live suggestions and a third-party chat widget. The vendor's guide says to paste a <script> tag that injects an iframe. A small front-end team records long tasks and INP with real-user monitoring. After a vendor update, tickets say "the page freezes when I start typing", mostly from budget Android phones.

    Starting architecture

    Figure 7. The product page with its trust boundary

    Product page with a first-party area and a third-party chat widgetPage scripts to Shop API and CDN (fetch); Image carousel to Chat loader script (same thread); Chat loader script to Chat iframe (injects, postMessage); Chat iframe to Vendor servers (its own requests)YOUR ORIGINTHIRD-PARTY ORIGINPage scripts: first-partyPage scriptsfirst-partyImage carousel: CSS transformImage carouselCSS transformShop API and CDN: your serversShop API andCDNyour serversChat loader script: runs in your pageChat loaderscriptruns in your pageChat iframe: vendor originChat iframevendor originVendor serversVendor serversfetchsame threadinjects, postMessageits own requests

    How to read this: the iframe is isolated, but the loader script is not. It sits inside your zone and shares the main thread with your search box and carousel logic. The red dashed edge marks that shared thread as the weak point.

    Options considered

    Option What it fixes Cost Risk
    Keep the script, ask the vendor to fix it Nothing until they ship None You depend on their schedule
    Load the widget on first click (a "facade" button) Removes its cost from page load and typing A small component; a delayed first chat Chat starts a moment later
    Embed the vendor's iframe URL directly, no loader Keeps vendor code off your main thread Needs vendor support Fewer features, such as page-aware greetings

    The decision

    Field Decision record
    Context A third-party loader adds long tasks on low-end phones, hurting typing and carousel swipes.
    Decision Load chat on first click of a "Chat" button (facade), in a cross-site sandboxed iframe where the vendor supports direct embedding. Keep the loader off the initial load.
    Consequences, good Typing no longer competes with vendor code at load; a clear trust boundary.
    Consequences, bad First chat opens slightly later; some vendor features unavailable.
    Revisit when The vendor offers a direct iframe embed, or field data shows no widget long tasks for several releases.

    Rollout and verification

    1. Record a baseline of long tasks per page view and INP by device class (see observability and SLOs for alerting on such measures).
    2. Ship the facade behind a flag to a small share of visitors, then widen it.
    3. Compare long-task attribution (entries identify the frame behind a task) and INP between flag groups, by device class, not only on average.
    4. Test on a throttled profile in DevTools and on a real low-end phone.

    The 2 a.m. failure

    At 02:00 the vendor releases a loader build that does heavy initialisation on load, while your facade flag covers only a small share of visitors. The alert is a rise in page views with long tasks, then in INP for the low-end mobile bucket. First checks: does the rise start at the vendor's release time, which long tasks are attributed to the loader or chat frame, and how do flag groups compare. Mitigation: raise the facade flag to 100 percent or use the tag manager's kill switch. Follow-ups: pin the vendor's script version where allowed, add a canary page that fails on long tasks from any third-party script, and send the vendor the long-task evidence.

    Architect's notes

    • Draw the thread map before the component map. What runs on the main thread, in a worker, in an iframe and on the server finds most performance and security problems early.
    • Decide trust boundaries early. Which third parties get a frame and which a script is hard to change once other teams depend on them.
    • Budget the main thread. Treat long tasks as a metric with an owner.
    • Do not design to one engine's internals. Chromium's process counts and thread layout change between versions; the shared main thread does not.
    • Defer layer tuning. Use transform and opacity by default; add will-change only when a profile shows a need.
    • Measure on real low-end devices, where memory pressure and missing site isolation are worst.

    Common mistakes

    • Saying "one process per tab". In Chromium the count depends on memory, platform and site isolation, and one tab can use several processes.
    • Believing an iframe protects your main thread. It protects data. A loader script in your page still runs on your thread.
    • Animating layout properties. Sliding a carousel with left or width forces layout and paint every frame.
    • One big scroll-blocking listener on document.body. The whole page waits for the main thread.
    • Trusting postMessage without checking event.origin. Any window can message yours.
    • Hosting a widget on your own subdomain and calling it isolated. Site isolation works on sites, so it can share your page's process.

    Key takeaways

    • A browser is a set of cooperating processes. The browser process coordinates; sandboxed renderer processes run web pages; the GPU process draws; a network service fetches.
    • Process layouts are an implementation choice. This lesson describes Chromium; WebKit and Gecko share the broad shape but differ in detail, and counts vary by platform and device memory.
    • Navigation is a hand-off. The browser process fetches, picks a renderer, commits the navigation, and the renderer parses and renders.
    • One main thread runs your scripts, style, layout, paint and event handling. A task longer than 50 ms makes input wait.
    • The compositor thread can scroll and animate on its own. transform and opacity on a promoted layer are the compositor-friendly properties.
    • The same-origin policy is a rule; site isolation is a process boundary behind it. Both work on different units: origins and sites.
    • Treat third parties as separate trust zones. Prefer a cross-site, sandboxed iframe, postMessage with origin checks, and a measured, late-loaded script.
    • Measure long tasks and INP in the field, by device class.

    Practice exercises

    Exercise 1 — Starter (30 min): Open a page you work on, record a trace in the DevTools Performance panel, and find the main thread's longest task. Write down what ran in it and which of the stages (script, style, layout, paint) dominated.

    Exercise 2 — Intermediate (1 hour): Build a small page with the two carousel CSS variants from this lesson. Add a button that blocks the main thread for 300 ms (illustrative). Click it while each slide animation is running and record what keeps moving. Explain the result using Figure 3.

    Exercise 3 — Intermediate (1 hour): Draw the trust map of a page you own: which iframes are cross-site, which scripts are third-party, and what each can read.

    Exercise 4 — Advanced (2 hours): Embed a cross-origin iframe on a local test page using two hostnames. Write the postMessage exchange with origin checks, then try to read the iframe's DOM from the parent and record the error.

    In this course:

    Elsewhere on the site:

    Official documentation

    Enroll to keep reading

    Lesson 3 of 8 · Frontend System Design Foundations: How the Web Runs

    Trace a page from URL to pixels through a browser's processes and threads, then use that model to keep the main thread free and isolate third-party widgets.

    In the rest of this lesson

    • From click to handler, and the frozen page
    • What the same-origin policy and site isolation protect
    • What this means for design decisions
    • In practice: a product page whose chat widget slows search
    • Architect's notes
    • Common mistakes

    Enrolling is free. It unlocks every lesson in Frontend System Design Foundations: How the Web Runs, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments