4 Proven Ways to Boost Your Web Page Speed

Quick Links

You may have noticed some webpages you own take 10+ seconds to load—you’re not alone. Slow webpages cause higher bounce rates, and that’s bad. Modern tooling and conventional techniques are the culprit. Fortunately, with a little knowledge and smart strategy, you can easily overcome this common problem.

Your general aim should be to keep your webpages light, avoid making many network requests during the initial page load, and focus on getting something on the page as fast as possible. Arm yourself with the following techniques, and avoid conventions like frameworks and libraries that bloat your page size. Your distinct approach will set you apart from the competition.

Avoid Bloated Frameworks

A person's hand touching conceptual icons of lean engineering and design.

Wright Studio/Shutterstock

In a previous post that gave advice for beginner web developers, I suggested that beginners should avoid frameworks because they’re difficult to learn, but they’re also resource-intensive to run. Most frameworks reinvent the wheel by delegating a lot of work to JavaScript, which is suboptimal.

Your strategy should be a lean and fast webpage, so a framework may be the wrong choice. The primary value propositions for a framework are single-page applications, better code organization, and supposedly more efficient DOM updates—the first two are debatable, and the third is only true in some cases.

For smaller, more focused projects, I personally use web components. Not only are they leaner, but long after React, Angular, and Vue are dead, web components will continue to work because they’re part of the W3 standard. The Lit framework (from Google) is a light wrapper around web components that looks very promising; it takes care of some boilerplate (tedious and repeated code).

So, if it’s possible, avoid (heavy) frameworks; a lot of the following advice assumes that you do. However, if you do use a framework (like React), some things may not apply, but they’re fundamental nonetheless.

Use Less and Lighter Dependencies

When the page initially loads, network requests to pull dependencies are a primary contributor to poor performance. I try my very best to avoid dependencies at all costs, especially helper libraries like Lodash or Ramda. Unfortunately, it means rolling custom solutions for some things.

For example, one time I needed a simple event bus, which is a class that’s essentially a communication channel shared across my code. A popular solution is a library called RxJS, but instead of including it as a dependency, which is 17.7 kB in size (compressed), I wrote my own 200-byte class. This saves only 100 ms initial load time, but they all add up.

The traditional method of writing code for the browser involves bundling it. This process requires you to write your code in Node.js and use a tool, such as webpack, to convert it into a format that the browser can use. The result is one large block of code with everything included, and often this can grow in size to over 100 KB. Techniques eventually evolved to address this, such as code-splitting, which splits the code into chunks; then the framework or browser will download them only when they’re needed. Another technique is tree-shaking, which analyzes code to extract only what’s needed; ESM does something vaguely similar.

ESM is the modern JavaScript module system, and it’s the new standard way to import libraries. This is an example of ESM:

        <script type="module">
  const {foo} from "https://example.com/all-the-things.js"
script>

The previous example will download the desired module and any other modules that it depends upon. This is how ESM works, and there are some vague similarities with tree-shaking.

ESM is not a silver bullet, and it certainly has its limitations. While I recommend that you use it, I stand by my earlier advice of writing your own minimal solutions (where possible)—doing so means that you avoid a bloated dependency tree for third-party libraries. If it takes less than 30 minutes to write, then why not?

One limitation of ESM is that if a module depends on many other modules, the browser must download all of them—this collection is called a dependency tree. For example, importing the capitalize function from Lodash requires loading many additional files:

        <script type="module">
    import capitalize from 'https://cdn.jsdelivr.net/npm/lodash-es@4.17.21/capitalize.js';
    alert(capitalize("hello world!"))
script>

You can see from the images that using Lodash to capitalize the first letter of a word will make 23 requests and take 4.4 s on a GPRS connection (20 KB/s); while on a 100 Mb connection, it still takes 2.2 s. If a 5000x faster connection speed only results in a 2x faster download, it suggests that bandwidth is not the issue.

In the third image, the section labeled 1 shows that the browser cascades requests, downloading only the code that it needs (vaguely similar to tree-shaking). The browser downloads each file, checks what its imports (dependencies) are, and then downloads those. The connection utilizes HTTP/2, which operates significantly faster than HTTP/1 in some scenarios. This is because HTTP/2 maintains a single, persistent connection, while in some cases, HTTP/1 relies on multiple separate connections, each involving negotiation overhead. Now imagine an older browser loads this web page, and it uses HTTP/1 to load each module.

To capitalize the first letter of a sentence, it takes very little code:

        function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

Prefer to implement a custom solution if it’s easy; this keeps the code light and focused on your goals. In addition, understand that imports are expensive when using ESM, and you should think carefully about how you divide up your code; try to minimize dependencies between modules. Also, bundling is still a very effective way to minimize the number of requests that the browser makes. There’s a lot to consider, so spend some time on this.

The moral here is to use dependencies sparingly. Frameworks are a huge dependency, and libraries are a liability. Lean and fast is the goal, and network latency will kill your performance.

Both defer and async are attributes that you can use on script tags to alter how they behave. Both affect performance, but they work slightly differently from each other, and there are clear use cases for each.

Excluding defer, async, and ESM for now, note that all script tags download in parallel to each other but execute before the HTML parser. This is how script tags normally behave.

Two side-by-side windows: on the left, HTML code with two script tags—one with 2000 ms latency, one with none; on the right, a browser showing a graph where both scripts load simultaneously, illustrating parallel loading.

Using a custom web server, I intentionally set download delays for scripts 1 and 2. For annotation 5, notice that both scripts began downloading at almost the same time, and the script with the 2000 ms delay started first. If the downloads weren’t happening in parallel, the longer (2000 ms) script would have blocked the other one from downloading until it finished.

Looking at annotation 4, you can also see that the HTML (annotation 3) was parsed only after all scripts finished. This shows that the scripts block the HTML parser while they’re being downloaded and executed.

Defer

The defer attribute on script tags tells the browser to delay script execution until after it has rendered the HTML. This is important, because we don’t want to block the initial page content while we wait for heavy JavaScript code to download and execute. Also note that deferred scripts execute in the order they appear in the HTML.

        <html>
  <head>
    <script src="/js/foo.js" defer>script>
  head>
  <p>I render before the script.p>
html>

ESM is also deferred by default.

A deferred script runs after the HTML is fully loaded, so you can safely access the DOM. If you need to run code after all deferred scripts have finished, use the DOMContentLoaded event—it fires once the HTML is parsed and all deferred scripts have run:

        
document.addEventListener("DOMContentLoaded", () => {
  console.log("The DOM is fully loaded!")
})

However, DOMContentLoaded does not wait for images, async scripts, or iframes.

Deferred scripts wait until the browser downloads and parses all stylesheets too, which is different from normal script tag behavior.

That’s a lot to remember, but simply put: a deferred script waits for the HTML renderer and stylesheets to complete first. To be 100% sure that the page is ready, listen for the DOMContentLoaded.

Async

The async attribute tells the browser to download the script while parsing the HTML. The script can run at any time. The script and HTML parser do not wait for each other, but the script may interrupt the HTML parser if necessary. It’s like a cross between a normal and deferred script.

        
  

Summary

  • Deferred (and ESM) scripts download immediately and execute after the HTML parser and stylesheets.
  • Async scripts download immediately and execute whenever, possibly interrupting the HTML parser.
  • Normal scripts download immediately and execute before the HTML parser.

The following image shows timelines for each script type, indicating when they download and execute in relation to the HTML parser:

A table with two columns: the left column lists script tag types, and the right column shows a timeline visualization of fetch, execution, and parser activity for each type.

WHATWG (Apple, Google, Mozilla, Microsoft), licensed under CC-BY-4.0

Use async scripts for isolated third-party scripts such as advertisements—scripts that don’t interact with the rest of your page. Use deferred scripts as much as you can. The use of deferred scripts will be crucial in the next section, which discusses critical CSS.

Inline Critical CSS

When a webpage initially renders, we say that the styles visible on the screen are above the fold, and the styles below the screen are below the fold. Critical CSS is concerned with rendering above-the-fold styles as fast as possible to give the user the impression of a fast-loading web page and to avoid testing their patience. This is important because the longer users wait for a page to load, the more likely they will leave.

To render content above the fold as quickly as possible, we need to split our styles into two bundles, one for above-the-fold content and the other for below it. To achieve this, it’s as simple as manually analyzing which styles apply to each section—for desktop and mobile.

To split your styles into two bundles, the most basic example looks like this:

        html>
<html lang="en">

<head>
  <script src="below-the-fold.js" defer>script>
  <style>
    /* Critical CSS. */
  style>
head>

<body>
  <aside>Above the fold.aside>

  <main>
    <p>Below the fold.p>
  main>

  <link rel="stylesheet" type="text/css" href="below-the-fold.css">
body>

html>

To make it more readable, the previous example is incomplete; a real-world example looks like this:

Two side-by-side windows: on the left, HTML code with numbered annotations from 1 to 6; on the right, a web page displaying content, with sections both below and above the fold visible.

The style tag in section 1 contains the critical CSS, which should contain above-the-fold styles. In our example, the aside tag (section 6) contains all content that’s above the fold. If you want to style it, then modify section 1.

When the page initially renders, the content below the fold has no styles, so it will briefly flash, showing unstyled content—this is called a flash of unstyled content (FOUC). To remedy this, I hide the content with opacity 0 (section 1). When the JavaScript (section 4) executes, it will change this opacity to 1. There is a soft transition effect, so it will fade in beautifully.

Because stylesheets in the head element block the HTML renderer, we instead load our non-critical CSS near the end of the body (section 3) after all other HTML has been rendered. Essentially, the browser first renders the HTML and then downloads the stylesheet at the end of the body, before it executes the deferred script.

We use a deferred script because it executes after all HTML has finished rendering and once all stylesheets have been downloaded and applied.

The process looks like this:

  1. The browser requests the HTML.
  2. The browser starts parsing the document.
  3. The browser downloads the JavaScript in the background and immediately moves on (section 5).
  4. The browser parses the inline styles (section 1).
  5. The browser renders the aside tag, which is above the fold (section 6).
  6. The browser renders the main content (section 2), which is below the fold and opacity 0, thanks to the critical CSS (section 1).
  7. The browser downloads the non-critical CSS (section 3).
  8. The browser completes parsing of HTML and styles, then fires DOMContentLoaded.
  9. The JavaScript fires to make below-the-fold content visible (annotation 4).

In summary: The browser works down to the CSS at the foot of the document, rendering all HTML and making below-the-fold content invisible. Once that is completed, the JavaScript fires to make below-the-fold content visible.

In short: Render above the fold quickly and hide everything else until it’s ready.


Take the time to digest what was said here, and build upon it. I suggest that you forgo the typical recommendations of React, Angular, and thousands of dependencies. Instead, you should:

  1. Scrutinize your dependencies, and keep them minimal. Fewer requests mean quicker page loads.
  2. Understand how script tags work, when they load, and how to avoid blocking the HTML parser.
  3. Understand when stylesheets load—both in the head and body—and then understand whether they block the HTML renderer.
  4. Render the visible HTML as fast as possible; load everything in due course.

If you want to apply what you’ve just learned, but need some resources to help you, check out the websites that every beginner web developer ought to know. Or, if you want to begin but lack inspiration, then check out our guide on how to build your first simple word counter web app.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top