The web pages and applications you build often display important messages, warnings, or load indicators before allowing access to certain components. For example, a site may show a "Loading…" message upon first visit, an ecommerce app might wait until products are fetched from the server before allowing users to add items to their cart, or an admin portal may check permissions before revealing sensitive controls.
In cases like these, it can be useful to programmatically wait for the page loading to complete in JavaScript before executing other logic. This allows you to ensure the DOM is ready or that critical resources are available before trying to interact with them.
But with many approaches available, how do you choose? Which method works best for specific use cases?
In this comprehensive technical guide, we‘ll explore several methods to wait for page loads in JavaScript, including code examples and expert analysis of the tradeoffs between approaches.
Overview of Page Load Waiting Approaches
Before diving into specifics, let‘s briefly introduce options available:
- window.onload – Wait for full DOM and all assets
- DOMContentLoaded – Wait for DOM only
- setTimeout() – Pause execution for fixed time
- setInterval() – Poll loading at interval
- Promises – Handle async resolution
- Performance API – Monitor load resource timing
The "best" approach depends on your goals and page characteristics. We‘ll compare the merits of each method later on.
First, let‘s explore specifics of implementation, starting with the load event…
The Window Load Event
The most straightforward way to wait until a page finishes loading in JS is with the window.onload
event. This event fires when the entire page DOM along with all dependent resources like images, CSS, fonts, etc finishes loading.
Basic Usage
To run code when onload
fires, assign a callback:
window.onload = function() {
// page fully loaded
};
For example, to display an alert when the page finishes loading:
window.onload = function() {
alert("Page fully loaded!");
};
Explanation
The window object exposes an onload
event we can subscribe to which signals the DOM and assets are ready. This is preferred over old-school solutions like $(document).ready()
since it works without any library dependencies.
Use Cases
Some example use cases where onload
works well:
- Initializing third-party scripts which require the DOM
- Rendering diagramming libraries that parse the page structure
- Displaying a "Page Loaded!" confirmation for the user
- Running core application logic that requires assets
So in general, use onload
when you have JavaScript/components needing the complete DOM and all associated resources like images.
Limitations
The main downside to onload
is that it waits for all page assets before triggering – even non-critical ones. This adds extra latency before executing code.
Large, complex pages with many media assets may have longer load times. Code locked behind onload
then suffers delays, impacting user experience.
Now let‘s compare this with the DOMContentLoaded
event which fires earlier…
The DOMContentLoaded Event
The DOMContentLoaded
event fires when the raw DOM itself finishes parsing, without having to wait for other external resources like images/stylesheets.
Basic Usage
We can subscribe to this event on the document
:
// Modern way
document.addEventListener("DOMContentLoaded", function() {
// DOM loaded but maybe not assets
});
// IE9+ support
document.attachEvent("onreadystatechange", function() {
if (document.readyState === "interactive") {
// DOM loaded but maybe not assets
}
});
To see it in action:
document.addEventListener("DOMContentLoaded", function() {
alert("DOM loaded!");
});
Explanation
This event signals when the HTML document has been fully parsed, without waiting for additional non-critical resources to download.
Assets like images/video may not be ready yet when DOMContentLoaded
fires. However the DOM tree itself is constructed.
Use Cases
Since DOMContentLoaded does not wait for unnecessary assets, it allows pages to initialize user interactions faster. Useful cases include:
- Initializing minimal UI like menus, buttons, overlays
- Dynamically rendering client-side templates
- Making early API calls to warm data caches
- Binding event listeners that don‘t need external resources
So in general, use DOMContentLoaded
to initialize critical interactive UI components that do not depend on other external assets.
Limitations
The downside to DOMContentLoaded
is that external resources like images or stylesheets may not be loaded yet.
Any initialization code that tries accessing images or CSS properties could fail or render incorrectly since those assets might still be downloading.
Therefore, DOMContentLoaded
is only awareness of DOM readiness – don‘t rely on other resources yet without checking.
Using setTimeout() to Pause Execution
Another ubiquitous option is using setTimeout()
to deliberately pause code execution for a fixed duration:
setTimeout(function() {
// runs after 2 second delay
}, 2000);
This delays JavaScript from running for a given number of milliseconds.
For example, to display an alert after 2 seconds:
setTimeout(function() {
alert("2 seconds has elapsed!");
}, 2000);
Explanation
The timer functionality of setTimeout
defers callback execution until an desired amount of time passes.
Use Cases
Since setTimeout
waits a fixed duration, some good use cases include:
- Temporarily displaying loading indicators
- Delaying lower-priority UI updates to prevent jank
- Debouncing scroll/resize handlers
- Pausing before fetching non-critical data
- Throttling high-frequency event callbacks
In these cases, the exact completion times are non-critical and a reasonable delay is enough.
Limitations
The major downside of setTimeout()
is its time-based nature. JavaScript timer durations are not perfectly accurate, especially when the main thread is busy.
Pages may load faster or slower than your delay accounts for. Too short a timeout and loading could still be in progress; too long and it makes users wait needlessly.
setTimeout()
alone is thus not robust enough for validating resource readiness.
Checking Loading Progress with setInterval()
A variation of setTimeout()
is setInterval()
– which repeatedly runs a function on a fixed recurring period.
setInterval(function() {
// runs every 2 seconds
}, 2000);
For example, to log a message every 3 seconds:
let counter = 0; // counter
setInterval(function() {
console.log(`Interval iteration ${counter++}`);
}, 3000);
This will log an increasing counter value to the console every 3 seconds indefinitely.
Explanation
We can leverage the polling nature of intervals to periodically check if loading has completed by running a callback function repeatedly.
Use Cases
Refreshing for updates from asynchronous sources:
- Polling API endpoints for async data
- Periodically checking if DOM components exist
- Updating progress meters as assets load
- Dynamically showing updated load percentage
Scenarios where updates trickle in over an unknown time period are suited to setInterval()
.
Limitations
Like setTimeout()
, choosing the right polling frequency is difficult:
- Too fast can overwork the browser before new data arrives
- Too slow and the user sees outdated information
Interval code also persists indefinitely, risking memory leaks if poorly managed.
Thus we want more adaptive approaches…
Using Promises
Promises provide an elegant alternative to timers and callbacks for working with asynchronous JavaScript code.
Quick Intro
Promises represents an eventual value – any operation that may complete some time later like a network call. They expose methods to subscribe handlers once this async action resolves or fails:
function myAsyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("Done!"), 1000);
});
}
myAsyncFunction()
.then(value => console.log(value));
// Prints "Done!" after approx. 1 second
Promises simplify async logic by avoiding tangled callback pyramids and state tracking variables that traditional approaches require.
Page Load Events
We can likewise wrap browser page load events like DOMContentLoaded
or onload
within promises:
function whenLoaded() {
return new Promise(resolve => {
window.onload = function() {
resolve("Page loaded!");
};
})
}
whenLoaded().then(msg => {
console.log(msg); // "Page loaded!";
});
This abstracts out the loading dependency into a single returned object. Better yet, we can await
this promise:
async function init() {
console.log(await whenLoaded()); // "Page loaded!";
}
init();
Within async functions, the await
operator pauses execution until the passed promise settles, then returns the resolved value. This allows writing synchronous-style code to handle asynchronous events.
Other Resources
We can adapt the promise pattern to waiting for any specific resource:
Async module / script
function whenScriptLoaded(src) {
return new Promise(resolve => {
const script = document.createElement(‘script‘);
script.onload = () => resolve();
script.src = src;
document.head.append(script);
});
}
await whenScriptLoaded(‘path/to/script.js‘);
Image loading
function whenImagesReady(selector) {
return new Promise(resolve => {
const images = document.querySelectorAll(selector);
let loadedImages = 0;
Array.from(images).forEach(img => {
img.onload = () => {
loadedImages++;
if(loadedImages === images.length) {
resolve();
}
};
});
});
}
await whenImagesReady(‘img.product‘);
We leverage incrementing counters to resolve a Promise once the set of images load.
API response
function whenDataAvailable(url) {
return fetch(url)
.then(res => res.json());
}
const data = await whenDataAvailable(‘/my-api‘);
The benefit of promises is they directly return HTTP request objects allowing easy chaining and conversion into desired payloads.
Explanation
Promises provide a generalized mechanism to wrangle asynchronous behaviors by standardizing the concepts of eventual success and failure handlers.
Use Cases
Great for situations where you need to coordinate loading dependencies like:
- Waiting for legacy script loader mechanism
- Sequential data bootstrapping
- Lazy loading 3rd party media apis
- Stalling render until widgets initialize
- Waterfalls for asynchronous dependencies
Limitations
The main downside of promises is browser support. While native promises ship in Evergreen browsers, legacy ones may require polyfills. Promises also simply wrap callback events under a uniform interface, so still need to reason about asynchronous transitions.
However, among page load approaches promises strike a nice balance between code clarity and flexibility.
Now let‘s look at how the Performance API provides timing insights…
Monitoring Loading Progress with Navigation Timing API
Beyond waiting for events, the Navigation Timing API provides detailed metrics around when key page lifecycle milestones occur.
For example, we can capture metrics like:
// Total time take take fully load
const loadTime = window.performance.timing.loadEventEnd - window.performance.timing.navigationStart;
// Time to parse DOM
const domReadyTime domContentLoadedEventEnd - fetchStart;
Simple Check Function
Here‘s a check function that uses the Navigation Timing API to validate if critical resources loaded under timing budgets:
function pageLoadedFastEnough() {
const navTimings = performance.getEntriesByType(‘navigation‘)[0];
const interactiveThreshold = 3000; // Time to interactive under 3 seconds
// DOM parsed under 500ms
const domReadyTime = navTimings.domComplete - navTimings.fetchStart;
if (domReadyTime > 500) {
return false;
}
// Overall interactive events fire within 3 seconds
const interactiveTime = navTimings.domContentLoadedEventEnd - navTimings.fetchStart;
return interactiveThreshold > interactiveTime;
}
if (pageLoadedFastEnough()) {
runExpensiveOperation();
} else {
showLoadingState();
}
Here based on resource timing data, we can choose to either initialize the full application or show a progress indicator if budgets not met.
Use Cases
Beyond just waiting, Navigation Timing allows:
- Diagnosing real-world loading performance issues
- Quantifying optimization opportunities
- Scoring user visits based on loading metrics
- A/B testing experiements for speed gains/regressions
- Determining personalized page load budgets
So while the API itself doesn‘t explicitly wait on loading, we can leverage detailed timing data to build custom wait-for logic for specific use cases.
Implementing Loading Fallbacks
Even using promises, asynchronous behavior means sometimes critical resources fail to load due to network errors.
In these cases, it‘s prudent to combine promises waits with fallback timeouts:
async function waitForData() {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject("Timed out!"), 15000)
);
return Promise.race([
fetchData(), // Call API
timeout // Reject in 15 seconds
]);
}
try {
const data = await waitForData();
// ... Use data
} catch(err) {
// Timed out
}
The Promise.race()
method runs passed promises concurrently, resolving/rejecting with the first one to finish. This ensures we don‘t wait indefinitely on unloaded resources.
Use Cases
When needing to balance loading states against delays in user experience:
- Network requests for critical JSON/media
- Listening for browser lifecycle events
- Initializing third-party libraries with async startups
- Long-running fetch pipelines that cascade
In these cases fallbacks provide insurance against cascading failures down the line.
Comparing Approaches: When to Use Each
Now that we‘ve surveyed techniques available to wait for page loads, which should you choose?
Deciding factors include:
What needs waiting on
window.load
– full DOM and all assetsDOMContentLoaded
– only DOM tree- Other resources – scripts, API data, media queries
Code clarity
- Callbacks vs. promises
- Browser support
Performance goals
- Timeouts and fallbacks
- Metrics and diagnostics
Here‘s a quick comparison of key differences:
Approach | Waits On | Clarity | Speed | Custom Resources |
---|---|---|---|---|
window.onload | All assets | Medium | Slow | No |
DOMContentLoaded | DOM only | Medium | Fast | No |
setTimeout() | Duration | Simple | Fast* | Yes |
setInterval() | Duration | Complex | Fast* | Yes |
Promises | Events | Clean | Fast | Yes |
Navigation Timing API | Metrics | Verbose | Fast | Complex queries |
Generally, use DOMContentLoaded to initialize critical UI rapidly while waiting on the full window.onload event for code needing every asset. Leverage Promises for better handling deferred code and declarative logic flow. Manage failures with timeouts. Diagnose real-world loading behaviour using Navigation Timing.
Pick the right paradigm for your specific goal, and combine approaches to balance speed and robustness!
Conclusion
In this extensive guide, we explored a variety of techniques for delaying JavaScript execution until after desired page loading milestones, including:
- window.onload and DOMContentLoaded events for browser lifecycle signals
- setTimeout() and setInterval() for simple timeouts
- Promises for robust async logic and resource coordination
- Navigation Timing metrics for diagnostics
- Timeouts to prevent infinite loading states
Deciding factors when choosing approaches include visibility needs, code clarity, performance goals, browser support, and handling failure cases.
Carefully Waiting on the necessary resources before allowing rendering & interaction helps craft robust, resilient cross-browser apps that delight users. Master these methods to take control over initialization & improve progressive loading!