Lazy loading Web Components with Intersection Observer
Fri Mar 20 2020
One of the many benefits of adopting Web Components (WC) as your component model of choice is that you can make direct use native web platform features with no modifications, wrappers or build scripts. One such example is using the Intersection Observer API and dynamic imports to load WC only when they're needed on the page. Using this pattern defers the loading of the component source on modern browsers until the tag is visible on the page, so:
- if a user visits on a browser that can't or won't run JavaScript, they won't download the JS that they'll never use
- if a user visits on a browser that doesn't support WC, they won't download the JS that they'll never use.
- if a user visits on a browser that does support WC but doesn't visit a page containing the WC, they won't download the JS that they'll never use
- if a user visits on a browser that does support WC and does visit a page containing the WC, but doesn't scroll to the part of the page that contains the WC, they won't download the JS that they'll never use
- if a user visits on a browser that does support WC and does visit a page containing the WC, and does scroll to the part of the page that contains the WC, they'll download the JS that they require on demand.
In this way we can progressively enhance the user experience not only based on browser features, but also only when a component is actually required. All this can be achieved with just a few lines of code:
// keep a record of the components we've loaded in this way
const imported = {};
// Set up our observer:
let observer = new IntersectionObserver((entries, observerRef) => {
// The callback is run when the visibility of one or more of the elements
// being observed has changed. It's also called on page load.
entries.forEach(async (entry) => {
//`isIntersecting` will be `true` if any part of the element is currently visible
if (entry.isIntersecting) {
// We are assuming here that your Web Component is located in a file
// named after it's tag name
const name = entry.target.nodeName.toLowerCase();
// Once we've observed this element come into view, we can safely remove
// the observer since we won't need to import the WC code again
observerRef.unobserve(entry.target);
if (!imported[name]) {
// Keep a note of which WCs have been loaded so if we have multiple
// instances we don't import twice
imported[name] = true;
// Let's load the WC code
import(`./${name}.js`);
}
}
});
});
// Observe all components with the desired class
const els = document.querySelectorAll('.dynamic-element');
els.forEach((el) => {
observer.observe(el);
});
And that's it. The observer callback is run when the visibility of one or more of the elements being observed has changed. There are 3 scenarios:
- The element has just come into view
- The amount of the element that's visible has changed
- The element has just gone out of view
Here we're only interested in the first case so isIntersecting
is all we need - when that's true
we know the component has just come into view and we can import the required code. We can also detach the observer so it's not called again when the element (or others of the same type) come into view again. Note that the observer is always run on page load so that if there's a component on screen when the page is loaded it's immediately imported.
I've added an example on this very page. By the time you've scrolled to the end of the page the browser will have dynamically loaded the following <lamplight-lazy></lamplight-lazy>
component (check in devtools to see the lamplight-lazy.js
file loaded when you scroll to this point.)
Let me know if you have any other tips for progressive enhancement with Web Components!