Lazy Load Vue.js Components When They Become Visible
Over the last couple of years as a web developer, I’ve seen the same pattern over and over again: the homepage becomes a political issue within a company because every department wants to present itself and, of course, every department considers itself the most important. Usually, two things happen: a slider is added at the top of the page so that each department can get its slide at the very top, and more and more stuff is added to the homepage because: everything is important.
It is worth mentioning that this outcome is not necessarily inevitable and is definitely not the best possible outcome. Generally speaking, users don’t scroll very far on the homepage and they don’t use sliders very actively most of the time. But if experience has taught me one thing: although most stakeholders are aware of these problems, it’s still a very common outcome.
So what’s the problem with very long pages in combination with a typical modern PWA architecture? If those pages consist of many different components, those components add a lot of weight to the bundle size of our application. That’s especially unfortunate considering that a lot of our users will never scroll down the page to actually see these components.
Lazy loading to the rescue
It’s pretty common practice nowadays to use lazy loading techniques to delay the loading of images until they are visible. You can read more about lazy loading images with Vue.js in my article about this very topic. But what if we could also apply this approach to Vue.js components?
In the video above, you can see how components are not loaded until they become visible. Initially, only a gray placeholder box is visible instead of the component itself. The video was recorded on a very slow internet connection.
Show me the code
Thanks to the relatively new Intersection Oberserver API and the concept of Async Components in Vue.js, we can implement a lazy loading utility function rather easily.
// src/utils/lazy-load-component.js
export default function lazyLoadComponent({
componentFactory,
loading,
loadingData,
}) {
let resolveComponent;
return () => ({
// We return a promise to resolve a
// component eventually.
component: new Promise((resolve) => {
resolveComponent = resolve;
}),
loading: {
mounted() {
// We immediately load the component if
// `IntersectionObserver` is not supported.
if (!("IntersectionObserver" in window)) {
componentFactory().then(resolveComponent);
return;
}
const observer = new IntersectionObserver((entries) => {
// Use `intersectionRatio` because of Edge 15's
// lack of support for `isIntersecting`.
// See: https://github.com/w3c/IntersectionObserver/issues/211
if (entries[0].intersectionRatio <= 0) return;
// Cleanup the observer when it's not
// needed anymore.
observer.unobserve(this.$el);
// The `componentFactory()` resolves
// to the result of a dynamic `import()`
// which is passed to the `resolveComponent()`
// function.
componentFactory().then(resolveComponent);
});
// We observe the root `$el` of the
// mounted loading component to detect
// when it becomes visible.
observer.observe(this.$el);
},
// Here we render the the component passed
// to this function via the `loading` parameter.
render(createElement) {
return createElement(loading, loadingData);
},
},
});
}
In the code block above, you can see the lazyLoadComponent()
function which returns an Async Component factory. It renders a loading
component until the real component, which we pass to the function via the componentFactory
property, is lazy loaded. We use the Intersection Oberserver API in order to detect when the component becomes visible. Executing the componentFactory()
triggers a dynamic import of the component.
import SkeletonBox from "./components/SkeletonBox.vue";
export default {
yname: "App",
components: {
MediaObject: lazyLoadComponent({
componentFactory: () => import("./components/MediaObject.vue"),
loading: SkeletonBox,
}),
},
};
Above you can see how to use the lazyLoadComponent()
function inside of a Vue.js component. If you’re interested in the implementation of the SkeletonBox
component, which we use as a loading placeholder, you can read my article about how to build it.
Do you want to learn more about advanced Vue.js techniques?
Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.
Analyzing the results
In order to find out how this approach affects the loading performance of a real application, I built a little demo app. You can check out the code at GitHub and you can test it yourself on Netlify.
If we take a look at the network tab of our browser of choice, we can see that we can save 126 kb on the initial page load. To be fair, most of that (115 kb) is because of images, but we’re also able to shave off about a third of the JavaScript code needed to initially render the page. Considering that this is a very simple application with some very simple components, it’s still not too shabby.
In the following screenshot you can see a graphical analysis of the bundles created by webpack. Particularly notable is the fact that the very heavy marked
package is moved into a separate bundle with the component which uses it. This helps a lot in reducing the file size of the main bundles.
Wrapping it up
Lazy loading can be a huge win if you work to improve the loading performance of your application. But you have to keep in mind that it also has its downsides. You should implement it very carefully and you may consider to only lazy load certain components that add a lot of weight or which are not very important to your users (in which case you should consider removing the component altogether).