- The DOM (Document Object Model): When a browser receives bytes, it parses them through tokenisation and node creation to construct a hierarchical tree of objects representing the structure and content of the document.
Bytes to characters
Tokenisation
Tokens to nodes
DOM Tree
-
For more information about the DOM, you should read our What Is the Document Object Model in Web Applications article.
-
The CSSOM (CSS Object Model): At the same time that the DOM is constructed, the browser also parses CSS rules into a similar structure called the CSSOM. It defines how styles cascade and apply to respective elements, accounting for inherited rules.
-
The render tree intersection: The browser combines the DOM and CSSOM to create the Render Tree.
- The render tree is a crucial internal structure created by web browsers that determines what content appears on the screen and how it is styled.
- Unlike the DOM (which contains every single HTML element including hidden ones), the render tree only contains visible nodes.
Included: Every visible DOM element, along with its computed styles. Pseudo-elements like ::before and ::after are also included because they generate visual content.
Excluded: Elements that are completely hidden from view - such as <head>, elements with display: none;, and script/style tags - are left out of the render tree entirely.
Elements with visibility: hidden or opacity: 0 are included in the render tree because they still occupy physical space on the page layout, even if they aren't visually drawn.
To actually display the page, the browser goes through four different rendering phases once the render tree has been built:
1. Style
- The browser evaluates CSSOM rules against the DOM to compute exact styles for every render tree element.
2. Layout (or Reflow)
- The browser traverses the render tree and calculates the exact geometric coordinates (size and position) of each node on the screen.
3. Paint
- Pixels are filled onto the screen, rendering text, colours, images, borders, and shadows.
4. Composite
- Painted layers are sent to the GPU and drawn to the screen in the correct layering order.
CSS is render blocking. To maximise performance, developers extract and inline "Critical CSS" (styles required for above the fold content) directly into the HTML <head>, allowing the browser to render the initial view without waiting for external spreadsheets to load.
-
This performance bottleneck occurs when JavaScript repeatedly interleaves layout reads (e.g., querying element.offsetWidth) and writes (e.g., modifying element.style.width) in a tight loop.
-
This forces the browser to prematurely execute synchronous layout recalculations over and over.
-
To mitigate this issue you can batch your reads together and your writes together (or use tools like requestAnimationFrame).
When building modern JavaScript applications (using frameworks like React, Vue or Svelte):
- The framework uses a virtual DOM or reactive dependency graph to efficiently compute changes in the JavaScript state.
- When state changes, the framework updates the real DOM.
- Any change to the real DOM triggers the browser to update parts of the CSSOM, re-evaluate the Render Tree and perform a new Layout and Paint. Minimising unnecessary updates to keep this pipeline fast is the core goal of frontend performance optimisation.
Events in the browser follow a strict three-phase propagation lifecycle:
- Capturing (Trickling) Phase: The event travels downward from the root (
window) through the ancestors down to the target element.
- Target Phase: The event arrives at the specific target element that triggered it.
- Bubbling Phase: The event bubbles upward from the target back up to the root (
window).
event.stopPropagation()
- Prevents the event from continuing its journey up through ancestor elements (stopping the bubbling phase) or down through the descendants (stopping the capturing phase).
- However, other listeners on the same current element will still execute.
event.stopImmediatePropagation()
event.stopImmediatePropagation() goes a step further by stopping event propagation entirely.
- It also blocks any other event listeners attached to the exact same element from running.
- When attaching event listeners, instead of binding individual event listeners to dozens or even hundreds of child elements (which consumes unnecessary memory and slows down initialisation), you should attach a single event listener to a common parent element.
Example:
Imagine we have a list of fruit:
Instead of adding a click listener for every single <li>, we can add one listener to the parent <ul> (fruit-list):
Benefits of this method
- Faster initialisation and reduced memory usage: Each time an event listener is attached to an HTML element, the browser has to allocate a chunk of memory to track it. Reducing the number of event listeners your program uses by delegating them properly leads to faster initialisation of the page and less memory usage (resulting in faster load times).
- Improved performance: Performance isn't just about initial load time; it's also about how the browser handles updates over time. With event delegation, the parent element stays put, managing events smoothly without constantly binding and unbinding functions as elements change.
- Seamlessly handling dynamic DOM elements: Event delegation gives the developer more control over dynamic DOM elements. Because the event listener is attached to the parent element (e.g.,
fruit-list in the previous example) - adding a fourth element is easier, because you do not have to manually add an event listener for the extra item as it is the parent that is doing the listening.
JavaScript allows you to build completely bespoke, application-specific events rather than solely relying on native browser triggers (like click or input).
This can be achieved with CustomEvent and dispatchEvent().
Example usage:
Observer APIs provide efficient, asynchronous ways to watch for DOM changes or an element's relationship with the viewport, avoiding the performance penalties of continuous polling or heavy event listeners.
- An
IntersectionObserver tracks when a target element intersects (enters or leaves) the viewport or a specified ancestor container. It is the modern standard for lazy-loading images, infinite scrolling, and scroll-triggered animations.
Example:
ResizeObserver
Example:
MutationObserver
- A
MutationObserver watches for changes made to the DOM tree, such as added or removed child nodes, modified attributes or text content updates.]
Example:
Managing when code runs is crucial for maintaining a high frame rate on your site and keeping the main thread responsive.
requestAnimationFrame vs requestIdleCallback
Example:
High-frequency events like scroll, resize and input can fire multiple times per second, causing layout thrashing or performance bottlenecks if left unmanaged.
- This delays invoking a function until after a specified amount of time has elapsed since the last time it was invoked.
- It is ideal for search inputs (
input events) where you only want to query the API after the user stops typing.
Example:
- This ensures that a function is called at most once every specified period, no matter how many times the event fires.
- It is ideal for window
resize and scroll tracking.
Example:
The modern fetch API
- The JavaScript
fetch API provides a powerful, flexible and promise-based mechanism for making HTTP requests.
- It relies on three core interfaces:
Headers Object
- Allows you to query, set, append and delete HTTP headers explicitly.
Request Object
- Encapsulates a resource request, including URL, method, headers, body, mode and cache directives.
Response Object
-
Unlike older libraries (like Axios), fetch() does not reject its promise on HTTP error status codes (such as 404 Not Found or 500 Internal Server Error).
-
It only rejects on network failures or request blocking. Therefore, you must manually check response.ok (which is true for status codes in the 200-299 range).
Example:
- When building responsive apps, you will often need to cancel pending network requests - such as when a user types rapidly in a search box or navigates away from a page.
- This is managed with
AbortController and AbortSignal.
AbortController
- A controller object that allows you to abort one or more web requests as needed.
AbortSignal
- A signal instance tied to the controller that is passed into the fetch configuration to listen for cancellation events.
Example:
- Different data types require different serialisation strategies before transmission across a network.
- JSON: Used for structured textual data via
JSON.stringify() for payloads and response.json() for parsing.
FormData: Used to construct key/value pairs representing form fields and binary files, natively handling multipart/form-data encoding.
- Binary data (
Blob, ArrayBuffer, TypedArray)
Blob (Binary Large Object): Represents immutable raw data, frequently used for files, images, or blobs of downloaded data.
ArrayBuffer: A low-level, fixed-length block of raw binary memory data.
Example:
- Real-time communication enables servers to push data to clients instantaneously without requiring repeated polling.
Server-Sent Events (SSE): Uni-directional streaming via EventSource
- SEE establishes a uni-directional connection where the server streams updates down a persistent HTTP connection using the text/event-stream format.
- It handles automatic reconnection out of the box.
Example:
WebSockets: Full-duplex communication via the WebSocket API
- Unlike SEE, WebSockets provide a full-duplex (two-way) real-time communication channel over a single TCP socket, allowing both client and server to send messages independently at any time.
Example:
localStorage and sessionStorage are built-in browser Web Storage APIs that allow you to store key-value pairs.
- They operate within a specific origin (domain, protocol and port) and generally offer around 5MB of storage capacity per origin.
localStorage: Data persists indefinitely until explicitly cleared via code or user action (such as clearing browser data). It is shared across all tabs and windows from the same origin.
sessionStorage: Data persists only for the duration of the top-level page session. If the user closes that specific tab or window, the session data is destroyed.
Dynamic Cross-Tab Synchronisation (storage event)
- Changes made to
localStorage in one tab automatically trigger a storage event on the window object of all other open tabs from the same origin.
- This enables real-time synchronisation (e.g., syncing user login state or themes across tabs).
sessionStorage does not trigger cross-tab events because it is isolated to a single tab.
- Web storage is completely exposed to JavaScript execution within the page.
- If your site is vulnerable to Cross-Site Scripting (XSS) - where an attacker injects malicious scripts - the attacker can easily read everything in
localStorage or sessionStorage using localStorage.getItem().
- Therefore, sensitive information (such as JWT access tokens or passwords) typically shouldn't be stored here.
Example - Web storage and cross-tab events:
- HTTP cookies are small pieces of data traditionally used for server-client communication (such as session management, tracking and personalisation).
- Interacting with cookies via the
document.cookie API is notoriously problematic.
- It gets and sets cookies as a single, raw semicolon-separated string (e.g.,
user=John; loggedIn=true;).
- Meaning that there are no built-in methods to easily query, update or delete individual cookies without custom parsing functions.
To protect cookies from attacks, modern web applications rely on strict security attributes:
HttpOnly: When set, the cookie cannot be accessed via client-side JavaScript (document.cookie will not show it). This is a critical defence mechanism against XSS theft, as malicious injected scripts cannot steal session cookies. (This can only be set via HTTP headers from the server, not in JavaScript)
Secure: This ensures that the cookie is only transmitted over encrypted connections (HTTPS), preventing interception via Man-in-the-Middle (MitM) attacks.
SameSite: Controls whether cookies are sent with cross-site requests to mitigate Cross-Site Request Forgery (CSRF):
Strict: The cookie is never sent in cross-site requests (e.g., following a link from an external site).
Lax: The cookie is withheld on cross-site sub-requests (like images or IFrames), but sent when a user navigates to the origin site (e.g., following a link).
None: The cookie is sent in all cross-site requests (must be accompanied by the Secure attribute).
Example - document.cookie usage:
IndexedDB is a low-level, client-side transactional database system built directly into the browser. Unlike key-value web storage which only stores strings, IndexedDB can store large amounts of structured data, including complex JavaScript objects, files and large binary objects (Blobs), making it ideal for offline-first applications (e.g., Progressive Web Apps).
- Asynchronous API: Unlike
localStorage (which is synchronous and can therefore block the main thread if overloaded), IndexedDB operations are entirely asynchronous, using IDBRequest objects, success/error callbacks or Promises.
- Object Stores: Similar to tables or collections in SQL/NoSQL databases, data is grouped into object stores.
- Transactions: All read and write operations on IndexedDB must be performed within a transaction (
readonly or readwrite) , ensuring data integrity and atomicity.
- Indices: You can create indices on object store properties to enable fast, efficient querying and searching of records.
Example - Opening IndexedDB and performing transactions:
Comments (0)