14 methods to defer or schedule JavaScript
Learn how to defer and schedule JavaScript
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.
Table of Contents!
- Why defer or schedule JavaScript?
- How can JavaScript timing affect the Core Web Vitals?
- How to choose the right defer method?
- Method 1: Use the defer attribute
- Method 2: Use the async attribute
- Method 3: Use modules
- Method 4: Place scripts near the bottom of the page
- Method 5: Inject scripts
- Method 6: Inject scripts at a later time
- Method 7: Change the script type (and then change it back)
- Method 9: Use readystatechange
- Method 10: Use setTimeout with no timeout
- Method 11: Use setTimeout with a timeout
- Method 12 Use a promise to set a microtask
- Method 13 Use a microtask
- Method 15: Use postTask
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.
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?
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.
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:
- Deferred scripts will download in parallel
- The DOM will be available at execution time
Disadvantages:
- Deferred scripts might delay your LCP metrics
- Deferred scripts will block the main thread once they are executed
- 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:
- Async scripts will download in parallel.
- Async scripts will execute as soon as possible.
Disadvantages:
- DOMContentLoaded may happen both before and after async.
- The execution order of the script swill be unknown beforehand.
- 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:
- Modules are deferred by default
- Modules are easier to maintain and work great with modulair web design
- Modules allow easy code splitting with dynamic imports where you only import the modules that you need at a certain time.
Disadvantages:
- Modules themselves will not improve the Core Web Vitals
- 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:
- Placing scripts at the bottom of the page does not require much knowledge.
- If done correctly there is not risk that this will break your page
Disadvantages:
- Critical scripts might get downloaded and executed later
- 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:
- Contained, code that injects an async script.
Disadvantages:
- DOMContentLoaded may happen both before and after the script has loaded.
- The execution order of the script swill be unknown beforehand.
- 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:
- Won't compete for critical resources because it will inject the script once the page and it's resources have loaded
Disadvantages:
- If your page is poorly designed Core Web Vitals wise it might take a long time for the page to send the 'load' event
- 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:
- It is easy, just enable rocket loader or another plugin and all your scrips are now executed at a somewhat later time.
- IT will probably fix your paint metrics provided you did not use JS based lazy loading.
- It will work for inline and external scripts.
Disadvantages:
- You will have no fine-grained control over when the scripts execute
- Poorly written script might break
- It uses JavaScript to fix JavaScript
- 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:
- Will not interfere with Core Web Vitals LCP and FCP
- Will never inject scripts that are not used. This will improve the FID and INP
Disadvantages:
- Should not be used with components that might be in the visible viewport
- Is harder to maintain then basic <script src="...">
- 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:
- Can break up long running code into smaller pieces.
Disadvantages:
- setTimeout is as rather crude method and does not offer prioritization for important scripts.
- 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:
- Supported on all browsers
Disadvantages:
- 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:
- The microtask will be scheduled immediately after the task has finished running.
- A microtask can be used to delay less important pieces of JavaScript code in the same event.
Disadvantages:
- Will not break up the main thread input smaller part. The browser will have no chance to respond to user input.
- 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:
- Execute JavaScript with minimal impact to the user
- Will most probably improve FID and INP
Disadvantages:
- Supported in most browsers but not all. There are poly-fills out there that fallback on setTimeout();
- 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:
- Complete controle over JavaScript execution scheduling!
Disadvantages:
- Not supported in some important browsers.
Stop Guessing, Start Measuring
I use Core/Dash to optimize my clients sites. So Should You.
- Easy set up.
- Over 5000 happy clients
- Free trial, No strings attached