14 methods to defer or schedule JavaScript

Learn how to defer and schedule JavaScript

Arjen Karel Core Web Vitals Consultant
Arjen Karel
linkedin

Why defer or schedule JavaScript?

JavaScript can (and will) slow down your website in a few ways. This can have all sort of negative impact on the Core Web Vitals. JavaScript can compete for network resources, it can compete for CPU resources (block the main thread) and it can even block the parsing of a webpage. Deferring and scheduling your scripts can drastically improve the Core Web Vitals.

To minimize the nasty effects that JavaScript can have on the Core Web Vitals it is usually a good idea to specify when a script gets queued for downloading and schedule when it can take up CPU time and block the main thread.

How can JavaScript timing affect the Core Web Vitals?

How can JavaScript timing affect the Core Web Vitals? Just take a look at this real life example. The first page is loaded with 'render blocking' JavaScript.  The paint metrics as well as the Time to interactive are pretty bad. The second example is of the the exact same page but with the JavaScript deferred. You will see the LCP image still took a big hit. The third example has the same script executed after the page 'load event' and has the function calls broken up into smaller pieces. This last one is passing the Core Web Vitals with room to spare.

js render blocking lhnull
js deferred lhnull
js after load lhnull


By default, external JavaScript in the head of the page will block the creation of the render tree. More specifically: when the browser encounters a script in the document it must pause DOM construction, hand over control to the JavaScript runtime, and let the script execute before proceeding with DOM construction. This will affect your Paint Metrics (Largest Contentful Paint and First Contentful Paint).

Deferred or async JavaScript can still impact paint metrics, especially the Largest Contentful Paint because it will execute, and block the main thread, once the DOM has been created (and common LCP elements like might not have been downloaded).

External JavaScript files will also compete for network resources. External JavaScript files are usually downloaded earlier then images. IF you are downloading too many scripts, the downloading of your images will be delayed.

Last but not least, JavaScript might block or delay user interaction. When a script is using CPU resources (blocking the main thread) a browser will not respond to input (clicks, scrolling etc) until that script has completed.

How does scheduling or deferring JavaScript fix the core web vitals?

Scheduling or deferring JavaScript does not fix the core web vitals per se. It is all about using the right tool for the right situation. As a rule you should try to delay your scripts as little as possible but queue them for download and execute them at the appropriate time.

How to choose the right defer method?

Not all scripts are the same and every script has it's own functionality. Some scripts are important to have early in the rendering process, others are not.

js defer vs async vs blockingnull

I like to categorize JavaScripts in 4 groups based on their level of importance.

1. Render critical. These are the scripts that will change the appearance of a web-page. If they do not load the page will not look complete. These scripts should be avoided at all costs. If you cannot avoid them for some reason they should not be deferred. For example a top slider or an A/B testing script.
2. Critical. These scripts will not change the appearance of a webpage (too much) but the page won't work well without them. These scripts should be deferred or asynced. For example your menu scripts.
3. Important. These are scripts that you want to load because they are valuable to you or the visitor. I tend to load these scripts after the load event has been fired. For example analytics or a 'back to top' button.
4. Nice to have. These are scripts that you can live without if you absolutely need to. I load these scripts with the lowest of priority and only execute them when the browser is idle. For example a chat widget or a facebook button.

Method 1: Use the defer attribute

Scripts with the defer attribute will download in parallel and are added to the defer JavaScript queue. Just before the browser will fire the DOMContentLoaded event all the scripts in that queue will execute in the order in which they appear in the document.

<script defer src='javascript.js'></script>

The 'defer trick' usually fixes a lot of issues, especially the paint metrics. Unfortunately there is no guarantee, it depends on the quality of the scripts. Deferred scripts will execute once all the scripts have been loaded and the HTML is parsed (DOMContentLoaded). The LCP element (usually a large image) might not be loaded by then and the deferred scripts will still cause a delay in the LCP.

When to use:

Use deferred scripts for Critical scripts that are needed as soon as possible.

Advantages:

  1. Deferred scripts will download in parallel
  2. The DOM will be available at execution time

Disadvantages:

  1. Deferred scripts might delay your LCP metrics 
  2. Deferred scripts will block the main thread once they are executed
  3. It might not be safe to defer scripts when inline or async scripts depend on them

Method 2: Use the async attribute

Scripts with the async attribute download in parallel and will execute immediately after they have finished downloading.

<script async src='javascript.js'></script>

Async scripts will do little to fix your pagespeed issues. It is great that they are downloaded in parallel but that is about it. Once they are downloaded they will block the main thread as they are executed.

When to use:

Use async scripts for Critical scripts that are needed as soon as possible and are self-contained (do not rely on other scripts).

Advantages:

  1. Async scripts will download in parallel.
  2. Async scripts will execute as soon as possible.

Disadvantages:

  1. DOMContentLoaded may happen both before and after async.
  2. The execution order of the script swill be unknown beforehand.
  3. You cannot use async scripts that rely on other async or deferred scripts

Method 3: Use modules

Modulair scripts are deferred by default unless they have they async attribute. In that case they will be treated like async scripts

<script module src='javascript.js'></script>

Modules are a new way of thinking about JavaScript and fix some design flaws. Other than that using script modules will not speed up your website. 

When to use:

When your application is build in a modulair way it makes sense to also use JavaScript modules.

Advantages:

  1. Modules are deferred by default
  2. Modules are easier to maintain and work great with modulair web design
  3. Modules allow easy code splitting with dynamic imports where you only import the modules that you need at a certain time.

Disadvantages:

  1. Modules themselves will not improve the Core Web Vitals
  2. Importing modules just-in-time or on the fly might be a slow and worsen the FID and INP 

Method 4: Place scripts near the bottom of the page

Footer scripts are queued for download at a later time. This will prioritize other resources that are in the document above the script tag.

<html>
   <head></head>
   <body>
      [your page contents here]
      <script defer src='javascript.js'></script>
   </body>
</html>

Placing your scripts at the bottom of the page is an interesting technique. This will schedule other resources (like images) ahead of your scripts. This will increase the chance that they are available to the browser and painted on the screen before the JavaScript files have finished downloading and the main thread will be blocked by script execution. Still ... no guarantee.

When to use:

When your scripts are already performing quite well but you want to slightly prioritize other resources like images and webfonts.

Advantages:

  1. Placing scripts at the bottom of the page does not require much knowledge.
  2. If done correctly there is not risk that this will break your page

Disadvantages:

  1. Critical scripts might get downloaded and executed later
  2. It does not fix any underlying JavaScript issues

Method 5: Inject scripts

Injected scripts are treated like async scripts. They are downloaded in parallel and are executed immediately after downloading.

<script>
    const loadScript = (scriptSource) => {
        const script = document.createElement('script');
        script.src = scriptSource;
        document.head.appendChild(script);
    }
    
    // call the loadscript function that injects 'javascript.js'
    loadScript('javascript.js');
</script>

From a Core Web Vitals perspective this technique is exactly the same as using <script async>.

When to use:

This method is often used by third party scripts that trigger as early as possible. The function call makes it easy to encapsulate and compress code.

Advantages:

  1. Contained, code that injects an async script.

Disadvantages:

  1. DOMContentLoaded may happen both before and after the script has loaded.
  2. The execution order of the script swill be unknown beforehand.
  3. You cannot use this on scripts that rely on other async or deferred scripts

Method 6: Inject scripts at a later time

Nice-to-have scripts should in my opinion never be loaded deferred. They should be injected injected at the most opportune moment. In the example below the script will execute after the browser has send the 'load' event.

<script>
window.addEventListener('load', function () {
  // see method 5 for the loadscript function
  loadScript('javascript.js');
});
</script>

This is the first technique that will reliably improve the Largest Contentful paint. All important resources, including images will be downloaded when the browser fires the 'load event'. This might introduce all sorts of issues because it might take a long time for the load event to be called.

When to use:

For nice-to-have scripts that have no reason to impact the paint metrics.

Advantages:

  1. Won't compete for critical resources because it will inject the script once the page and it's resources have loaded

Disadvantages:

  1. If your page is poorly designed Core Web Vitals wise it might take a long time for the page to send the 'load' event
  2. You need to be careful not to apply this to critical scripts (like lazy loading, menu etc)

Method 7: Change the script type (and then change it back)

If a script tag is found somewhere on the page that 1. has a type attribute and 2. the type attribute is not "text/javascript" the script will not be downloaded and executed by a browser. Many JavaScript Loaders (like CloudFlare's RocketLoader) rely on his principle. The idea is quite simple and elegant.

First all scripts are rewritten as this:

<script type="some-cool-script-type" src="javascript.js"></script>

Then, at some point during the loading process these scripts are converted back to 'normal javascripts'.

When to use:

This is not a method I would recommend. Fixing JavaScript impact will take a lot more then just moving every script a bit further down the queue. On the other hand, If you have little control over the scripts running on the page or have insufficient JavaScript knowledge this might be your best bet.

Advantages:

  1. It is easy, just enable rocket loader or another plugin and all your scrips are now executed at a somewhat later time.
  2. IT will probably fix your paint metrics provided you did not use JS based lazy loading.
  3. It will work for inline and external scripts.

Disadvantages:

  1. You will have no fine-grained control over when the scripts execute
  2. Poorly written script might break
  3. It uses JavaScript to fix JavaScript
  4. It does nothing to fix long running scripts

Method 8: Use the intersection observer

With the intersection observer you can execute a function (which in this case loads an external JavaScript) when an element scrolls into the visible viewport.

<script>
const handleIntersection = (entries, observer) => {
    if (entries?.[0].isIntersecting) {
        // load your script or execute another 
           function like trigger a lazy loaded element
        loadScript('javascript.js');

        // remove the observer
        observer.unobserve(entries?.[0].target);
    }
};
const Observer = new window.IntersectionObserver()
Observer.observe(document.querySelector('footer'));
</script>

This is by far the most effective method of deferring JavaScript there is. Only load the scripts that you need, just before you need them. Unfortunately real life is hardly even this clean and not many scripts can be bonded to an element that scrolls into view.

When to use:

Use this technique as much as possible! Whenever a script only interacts with an off-screen component (like a map, a slider, a form) this is the best way to inject this script.

Advantages:

  1. Will not interfere with Core Web Vitals LCP and FCP
  2. Will never inject scripts that are not used. This will improve the FID and INP

Disadvantages:

  1. Should not be used with components that might be in the visible viewport
  2. Is harder to maintain then basic <script src="...">
  3. Might introduce a layout shift

Method 9: Use readystatechange

document.readystate can be used as and alternative to the 'DOMContentloaded' and 'load' event. The 'interactive' readystate is usually a good place to call critical scripts that need to change the DOM or addenthandlers. 
The 'complete' ready state is a good place to call scripts that are less critical.

document.addEventListener('readystatechange', (event) => {
  if (event.target.readyState === 'interactive') {
    initLoader();
  } else if (event.target.readyState === 'complete') {
    initApp();
  }
});

Method 10: Use setTimeout with no timeout

setTimeout is a frowned-upon yet heavily underestimated method in the pagespeed community. setTimeout has gotten a bad wrap because it is often misused.  Many developers believe setTimeout can only be used to to delay script execution by the set amount of milliseconds. While this is true setTimeout actually does something far more interesting. It creates a new task at the end of the the browsers event loop. This behavior can be used to schedule your tasks effectively. It can also be used to break up long tasks into separate smaller tasks

<script>
   setTimeout(() => {
       // load a script or execute another function
       console.log('- I am called from a 0ms timeOut()')
    }, 0);

   console.log('- I was last in line but executed first')
/* Output: - I was last in line but executed first - I am called from a 0ms timeOut() */ </script>

When to use:

setTimeout created a new task in the browsers event loop. Use this when your main thread is being blocked by many function calls that run sequentially.

Advantages:

  1. Can break up long running code into smaller pieces.

Disadvantages:

  1. setTimeout is as rather crude method and does not offer prioritization for important scripts.
  2. Will add the to-be-executed code at the end of the loop

Method 11: Use setTimeout with a timeout

Things get even more interesting when we call setTimeout with a timeout of more then 0ms

<script>
   setTimeout(() => {
       // load a script or execute another function
       console.log('- I am called from a 10ms timeOut()')
    }, 10);

   setTimeout(() => {
       // load a script or execute another function
       console.log('- I am called from a 0ms timeOut()')
    }, 0);

   console.log('- I was last in line but executed first')
/* Output: - I was last in line but executed first - I am called from a 0ms timeOut() - I am called from a 10ms timeOut() */ </script>

When to use:

When you need an easy method to schedule one script after another a small timeout will do the trick

Advantages:

  1. Supported on all browsers

Disadvantages:

  1. Does not offer advanced scheduling

Method 12 Use a promise to set a microtask

Creating a micro-task is also an interesting way of scheduling JavaScript. Micro-tasks are scheduled for execution immediately after the current execution loop has finished.

<script>
   const myPromise = new Promise((resolve, reject) => {
      resolve();
   }).then(
      () => {       
         console.log('- I was scheduled after a promise')
         }
   );
console.log('- I was last in line but executed first') /* Output: - I was last in line but executed first - I was scheduled after a promise */ </script>

When to use:

When a task needs to be scheduled immediately after another task. 

Advantages:

  1. The microtask will be scheduled immediately after the task has finished running.
  2. A microtask can be used to delay less important pieces of JavaScript code in the same event. 

Disadvantages:

  1. Will not break up the main thread input smaller part. The browser will have no chance to respond to user input.
  2. You will probably never need to use microtasks to improve the Core Web Vtials unless you allready know exactly what you are doing.

Method 13 Use a microtask

The same result can be achieved by using queueMicrotask(). The advantage of using queueMicrotask() over a promise is that is is slightly faster and you do not need to handle your promisses.

<script>
   queueMicrotask(() => {
      console.log('- I am a microtask')
   })
     
   console.log('- I was last in line but executed first')

   /*
      Output:
      - I was last in line but executed first
      - I am a microtask
   */
</script>


Method 14: Use requestIdleCallback

The window.requestIdleCallback() method queues a function to be called during a browser's idle periods. This enables developers to perform background and low priority work on the main event loop, without impacting latency-critical events such as animation and input response. Functions are generally called in first-in-first-out order; however, callbacks which have a timeout specified may be called out-of-order if necessary in order to run them before the timeout elapses.

<script>
requestIdleCallback(() => {
    const script = document.createElement('script');
    script.src = 'javascript.js';
    document.head.appendChild(script);
});
</script>

When to use:

Use this for scripts that are Nice to have or to handle non-critical tasks after user input

Advantages:

  1. Execute JavaScript with minimal impact to the user
  2. Will most probably improve FID and INP

Disadvantages:

  1. Supported in most browsers but not all. There are poly-fills out there that fallback on setTimeout();
  2. No guarantee the code will ever fire 

Method 15: Use postTask

The method allows users to optionally specify a minimum delay before the task will run, a priority for the task, and a signal that can be used to modify the task priority and/or abort the task. It returns a promise that is resolved with the result of the task callback function, or rejected with the abort reason or an error thrown in the task.

<script>
scheduler.postTask(() => {
    const script = document.createElement('script');
    script.src = 'javascript.js';
    document.head.appendChild(script);
}, { priority: 'background' });
</script>

When to use:

postTask is the perfect API to schedule script with. Unfortunately browser support at this time as bad so you should not use it.

Advantages:

  1. Complete controle over JavaScript execution scheduling!

Disadvantages:

  1. Not supported in some important browsers.

I help teams pass the Core Web Vitals:

lighthouse 100 score

A slow website is likely to miss out on conversions and revenue. Nearly half of internet searchers don't wait three seconds for a page to load before going to another site. Ask yourself: "Is my site fast enough to convert visitors into customers?"

14 methods to defer or schedule JavaScriptCore Web Vitals 14 methods to defer or schedule JavaScript