TIL 2026-03-27 - How browsers run JavaScript

2026-03-27

Part of a series of “Today I Learned”s

How Browsers Run JavaScript

This came up when trying to add a ‘Randomise Quote’ button to this website. I’ll rewind down to browsers and explain up from first principles β€” then build back up to the actual code at the end.

Browsers, servers, etc.

  • A server is a program that listens for requests and sends back responses.
    • It’s a role, not a device - any program that sits and waits can be a server -
    • The Flask app running this website is a server: it waits for someone to visit a page, then sends back an HTML file as a response
      • Technical note: In practice, this Flask app lives in a VPS (a Virtual Private Server). I’m basically renting a slice of a physical machine in a data centre somewhere, running 24.7.
        • It’s called virtual because the single physical computer is actually shared between multiple tenants. Each VPS behaves like an independent machine. It runs its own operating system, memory, resources, etc.
        • Mine runs Linux, and the Flask app runs a process on it.
  • When you open a browser and type in a URL, the browser is acting like a client.
    • It makes a request across the internet to that VPS. It waits for a response - which arrives as an HTML file**. Finally, it has to do something with that file.

More on HTML

  • HTML is just a text file with a specific structure
    • It describes what should exist on a page, and how elements relate to each other
    • The browser treats it as a set of instructions written in tags:
<h1>This is a heading</h1>
<p>This is a paragraph.</p>
<button id="refresh-quote">πŸ”„ Refresh Quote</button>
  • The browser reads top to bottom, drawing each element via its rendering engine
    • The rendering engine translates HTML tags into pixels on screen
    • So we’d first draw the heading, then the paragraph, then the button
    • That <button> at the bottom is the one we want to bring to life
      • Right now, it renders on the screen, and you can click it. But clicking it still does nothing
      • This is what the rest of this post is about.

The DOM

  • After reading the HTML file, the browser doesn’t discard the instructions
  • It builds a live tree of objects in memory
    • The tree has one object (node) per HTML element
    • Elements are nested in the tree exactly as they’re nested in the file
    • This is the DOM: the Document Object Model
  • Our snippet above becomes this DOM:
document
└── <body>
      β”œβ”€β”€ <h1>  "This is a heading"
      β”œβ”€β”€ <p>   "This is a paragraph."
      └── <button id="refresh-quote">  "πŸ”„ Refresh Quote"
  • Why keep this tree in memory at all?
    • Each element is a node in the tree, which can be read and modified.
    • Modifying a node causes the browser to immediately re-render just that part.
    • This means that when we press the refresh quote button, we can reload the quote text without losing the scroll position - nothing flickers, and the rest of the page stays untouched.

Javascript

  • Browsers also have a JavaScript engine. JavaScript lives inside your browser (which lives inside your machine).
    • The browser’s JS engine (Chrome uses one called V8) takes the JavaScript code and compiles it into machine instructions your CPU executes.
      • Worth noting: my server just sends the JavaScript file. After that, it’s your computer doing all the work.
      • This is why pages with heavy JavaScript can feel sluggish on older machines.
  • JavaScript also has automatic access to the DOM
  • The browser creates a global object called document. This is a live handle to the entire DOM tree. Every JavaScript file on the page can access document.
  • We can walk the tree and specifically access the refresh-quote button:

document.getElementById('refresh-quote')
- Anything we do to the above element immediately affects what’s on the screen

Event Listeners

  • Once we have a node, we can attach an Event Listener to it. This just waits at a node for a specific event to happen.
    document.getElementById(
            'refresh-quote'
        ).addEventListener(
            'click', 
            function() {
                // this runs when the button is clicked
            }
        )
    ;
    
  • Sometimes the button needs to ask the server for new data.
  • fetch() is a built-in JavaScript function that makes an HTTP request silently in the background. The snippet below quietly asks the Flask server for a new quote.

fetch('/random-quote') 
- As a response, it gets a JSON packet
{ 
    "quote_html": "<p>Some quote</p>", 
    "source": "β€” Some Book" 
}

  • This fetch() call is asynchronous. In Python, if you make a basic network request, the execution of the rest of the script needs to wait for the response before moving on.
    • Technically, there are some libraries you can run to get around this.
  • JavaScript doesn’t block like that - it hands the request off to the network layer and keeps running.
    • What is the network layer?
      • This is a part of the browser that handles the plumbing of requests. It sends bytes over the internet and gets some back.
      • We can treat it like a Black Box.
  • When a response arrives, it gets handled by .then() clauses
    • Each .then() is a pre-instruction: when the previous step finishes, do this
    • If any step fails, the error falls through to a .catch() at the end
fetch('/random-quote')
    .then(response => response.json())
    .then(data => {
        // update the page
    });

What happens if you click multiple times?

  • Each click fires the event listener independently. The browser doesn’t know or care that the previous fetch hasn’t finished. If you click three times quickly, you’ve launched three simultaneous fetch requests
  • These requests all live in the network layer at the same time
    • The network layer manages them concurrently. Instead of queueing behind each other, they all wait for a response at the same time.
    • The responses can arrive out of order. For example, the third request might get a response back faster than the first request.
    • The .then() chain will fire as soon as its response arrives. In this case, the third click would execute its .then() first.
  • This is the sharp edge of async behaviour: you can fire things, but you don’t control when they resolve.
  • In practice, for a quote randomiser, this doesn’t matter much
    • Each response overwrites the same DOM node. There’s no way for the user to care/notice if the updates are out of order.
    • But for something like a bank transfer or a form submission, out-of-order responses could be genuinely dangerous.
  • The standard fix is debouncing or disabling the button while a request is in flight:

    const btn = document.getElementById('refresh-quote');
    
    btn.addEventListener('click', function() {
        btn.disabled = true; // block further clicks
        fetch('/random-quote')
            .then(response => response.json())
            .then(data => {
                // update the page
                document.getElementById('quote-content').innerHTML = data.quote_html;
                document.getElementById('quote-source').innerHTML = data.source;
                // re-enable when done
                btn.disabled = false; 
            });
    });
    

  • btn.disabled = true tells the browser to ignore clicks on this element

    • The moment a click fires, we immediately disable the button
    • Only once the response has arrived and the DOM has updated, do we re-enable it
    • Now there can only ever be one request in flight at a time

Future TILs

  • Debouncing
  • Race Conditions and out-of-order bank transfers

Back to posts