search ]

Optimize Performance with Lazy Loading for JavaScript

The script that activates the search option on Savvy Blog is loaded only when the user clicks on the search icon in the top bar. The functionality of the search is not particularly complex, but do you see a reason to load it when the visitor doesn’t intend to use the blog’s search option at all?

The purpose of this post is to demonstrate a technique for scripts that are much more complex. This way, you can significantly reduce the page weight and shorten the time required for JavaScript execution.

Consequently, you can improve loading time and achieve better Core Web Vitals metrics, especially Interaction to Next Paint (INP) which is directly affected by how much JavaScript runs on the page.

Before implementing on-demand loading, consider whether simpler approaches like deferring JavaScript with the defer or async attribute would be sufficient. On-demand loading is best for scripts tied to specific user interactions.

When to Use On-Demand Script Loading

There are three main strategies for loading non-critical JavaScript:

  • defer attribute – Downloads in the background and executes after HTML parsing completes, maintaining execution order. Best for most scripts that interact with the DOM.
  • async attribute – Downloads in the background and executes immediately when ready, in no particular order. Best for independent scripts like analytics.
  • On-demand injection – Loads scripts only when a specific user interaction or event occurs. Best for features that most visitors never use.

This post focuses on the third approach. It is the most effective for Core Web Vitals because it completely removes unused JavaScript from the initial page load.

The loadScript Function

For our purposes, to use this Lazy Loading technique, let’s define a function called loadScript:

function loadScript(url) {
  let isLoaded = document.querySelectorAll('.search-script');
  if(isLoaded.length > 0) {
    return;
  }

  let myScript = document.createElement("script");
  myScript.src = url;
  myScript.className = 'search-script';
  document.body.appendChild(myScript);
}

The function first checks if the script is already loaded. If it is, the function simply ends without trying to load the script multiple times.

Then, we create an element of type script using document.createElement and pass the URL that will be used for the src attribute of the lazily loaded script.

We add the relevant class for the previous check (whether the script is already loaded or not), and then append the script to the body element of the page.

Triggering the Script on User Interaction

The last step is to define an event listener that listens for a click on the search icon. When this happens, we call the loadScript function with the URL of our script.

let searchInput = document.querySelector('#trigger-overlay');
searchInput.addEventListener('click', function (e) {
    loadScript('/path/to/search.js');
});

In the case of this blog, I’m also displaying the search popup before calling the script using loadScript.

We saved loading a specific script that doesn’t need to be loaded all the time.

By the way, the WP-Rocket plugin has a similar option called Delay Javascript Execution. It works slightly differently – it is based on any user interaction on the site, not a specific interaction you can define.

You can also exclude specific scripts from WP Rocket’s deferred loading when needed.

Loading Scripts with Intersection Observer

Another powerful approach is loading scripts when a specific element becomes visible in the viewport. This uses the Intersection Observer API:

function loadScriptOnVisible(selector, url) {
  const target = document.querySelector(selector);
  if (!target) return;

  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        loadScript(url);
        observer.disconnect();
      }
    });
  });

  observer.observe(target);
}

This is useful for scripts tied to elements below the fold, like maps, comment sections, or embedded widgets. The script only loads when the user scrolls to that section.

Adding a Callback to loadScript

The basic loadScript function has no way to know when the script has finished loading. You can add a callback or return a Promise:

function loadScript(url) {
  return new Promise((resolve, reject) => {
    const isLoaded = document.querySelector(`script[src="${url}"]`);
    if (isLoaded) {
      resolve();
      return;
    }

    const script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.body.appendChild(script);
  });
}

Now you can chain actions that depend on the script being loaded:

searchInput.addEventListener('click', async function() {
  await loadScript('/path/to/search.js');
  initializeSearch();
});

WordPress Integration

If you are working with WordPress, you will want to properly enqueue your scripts and then dequeue them from the normal loading flow. You can use wp_deregister_script to prevent the default loading and then load the script on demand using the technique shown above.

For a broader approach to reducing JavaScript on specific pages, consider using Plugin Organizer to selectively load plugins only where they are needed.

FAQs

Common questions about lazy loading JavaScript:

What is the difference between defer, async, and on-demand script loading?
The defer attribute downloads the script in the background and executes it after HTML parsing, maintaining order. The async attribute downloads and executes immediately when ready, in no particular order. On-demand loading only fetches and executes the script when a specific event occurs, such as a click or scroll. On-demand loading provides the best performance because the script is never loaded unless needed.
Does lazy loading JavaScript improve Core Web Vitals?
Yes. Lazy loading JavaScript directly improves Interaction to Next Paint (INP) by reducing the amount of JavaScript that executes during page load. It also helps Largest Contentful Paint (LCP) by freeing up the main thread for rendering. The less JavaScript that runs on initial load, the more responsive the page feels.
When should I use Intersection Observer instead of a click event?
Use Intersection Observer when the script is tied to a visible element on the page, such as an embedded map, comment section, or widget below the fold. Use a click event when the script is tied to a specific user action like clicking a button or icon. Both approaches prevent unnecessary loading.
Will the script load every time the user clicks the button?
No. The loadScript function includes a check to see if the script is already loaded. If it is, the function returns immediately without adding a duplicate script tag. This ensures the script is only loaded once regardless of how many times the user triggers the event.
Can I use this technique with WordPress plugins?
Yes, but it requires dequeuing the plugin's script from the normal WordPress enqueue system and then loading it on demand with the loadScript function. Alternatively, you can use plugins like WP Rocket's Delay JavaScript Execution feature or Plugin Organizer to selectively control which scripts load on which pages.
Is on-demand script loading compatible with all browsers?
Yes. The document.createElement and appendChild methods used in the loadScript function are supported by all modern browsers and even older ones like IE9+. The Intersection Observer API is supported by all modern browsers. For very old browsers, you can use a polyfill or fall back to loading the script normally.

Summary

Lazy loading JavaScript on demand is one of the most effective ways to improve page performance. By loading scripts only when a specific user interaction occurs – like a click, scroll, or element becoming visible – you eliminate unnecessary JavaScript from the initial page load.

This directly benefits Core Web Vitals, especially INP and LCP. Combine this technique with deferred JavaScript loading for scripts that must load on every page, and you have a comprehensive strategy for JavaScript optimization.

Join the Discussion
0 Comments  ]

Leave a Comment

To add code, use the buttons below. For instance, click the PHP button to insert PHP code within the shortcode. If you notice any typos, please let us know!

Savvy WordPress Development official logo