Table of Contents
- Understanding Lazy Loading in Vue.js
- Route-Based Lazy Loading with Vue Router
- 2.1 Dynamic Imports and Code Splitting
- 2.2 Implementing Lazy Loaded Routes
- 2.3 Handling Loading States
- Component-Based Lazy Loading
- 3.1 Using
defineAsyncComponent - 3.2 Loading and Error States for Async Components
- 3.3 Integrating with
<Suspense>
- 3.1 Using
- Image Lazy Loading in Vue.js
- 4.1 Native Image Lazy Loading
- 4.2 Using the
vue-lazyloadLibrary - 4.3 Custom Intersection Observer Directives
- Advanced Techniques and Best Practices
- 5.1 Code Splitting Strategies
- 5.2 Preloading Critical Resources
- 5.3 Avoiding Common Pitfalls
- Conclusion
- References
Understanding Lazy Loading in Vue.js
Lazy loading is a design pattern that delays the initialization of resources until they are required. In Vue.js, this translates to:
- Route-based lazy loading: Loading route components only when the user navigates to them.
- Component-based lazy loading: Loading non-critical components (e.g., modals, tabs) only when they become visible.
- Image lazy loading: Loading images only when they enter (or are about to enter) the viewport.
Why Lazy Loading Matters for Vue Apps
- Faster Initial Loads: Reduces the size of the initial JavaScript bundle, leading to quicker time-to-first-byte (TTFB) and time-to-interactive (TTI).
- Reduced Bandwidth Usage: Users on limited data plans only download resources they actually view.
- Improved Core Web Vitals: Metrics like Largest Contentful Paint (LCP) and First Input Delay (FID) often improve with lazy loading.
Route-Based Lazy Loading with Vue Router
Vue Router natively supports lazy loading via dynamic imports, which split your app into smaller “chunks” that are loaded on demand. This is the most impactful lazy loading technique for multi-page Vue apps.
2.1 Dynamic Imports and Code Splitting
Traditionally, route components are imported eagerly (loaded upfront):
// Eager loading (not lazy)
import Home from './views/Home.vue';
import About from './views/About.vue';
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
];
With dynamic imports, components are imported asynchronously using import(), which returns a promise. Webpack, Vite, or Rollup automatically split these into separate chunks during bundling:
// Lazy loading with dynamic import
const Home = () => import('./views/Home.vue');
const About = () => import('./views/About.vue');
Webpack (or your bundler) will generate separate .js files for Home and About, which are fetched only when the user navigates to / or /about.
2.2 Implementing Lazy Loaded Routes
To implement lazy loading in Vue Router:
- Modify Route Definitions: Replace eager imports with dynamic imports.
- (Optional) Name Chunks: Use
webpackChunkName(Webpack) or/* @vite-ignore */(Vite) to give chunks meaningful names for easier debugging:
// routes/index.js
const routes = [
{
path: '/',
name: 'Home',
// Lazy load Home with named chunk (Webpack)
component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
},
{
path: '/about',
name: 'About',
// Lazy load About with named chunk (Vite)
component: () => import('../views/About.vue') /* @vite-ignore */
}
];
export default routes;
- Register Routes with Vue Router: No changes here—Vue Router handles the rest:
// main.js
import { createRouter, createWebHistory } from 'vue-router';
import routes from './routes';
const router = createRouter({
history: createWebHistory(),
routes
});
app.use(router);
2.3 Handling Loading States
When a lazy-loaded route is first requested, there may be a delay while the chunk downloads. To improve UX, add a loading state:
Option 1: Use Vue Router’s onLoading/onLoaded (Vue Router 4+)
Vue Router 4 provides navigation guards to track chunk loading:
router.beforeEach((to, from, next) => {
// Show loading spinner
store.commit('setLoading', true);
next();
});
router.afterEach(() => {
// Hide loading spinner after navigation
store.commit('setLoading', false);
});
Option 2: Wrap Dynamic Imports in a Loading Component
Create a wrapper component to handle loading/error states for async routes:
// components/AsyncRoute.js
import { defineAsyncComponent } from 'vue';
export default function AsyncRoute(importFunc) {
return defineAsyncComponent({
loader: importFunc,
loadingComponent: () => import('./Loading.vue'), // Show while loading
errorComponent: () => import('./Error.vue'), // Show on failure
delay: 200, // Wait 200ms before showing loading (avoids flicker)
timeout: 5000 // Fail after 5 seconds
});
}
// Usage in routes:
const About = AsyncRoute(() => import('./views/About.vue'));
Component-Based Lazy Loading
For components that aren’t tied to routes (e.g., modals, tabs, or below-the-fold content), use Vue’s defineAsyncComponent to lazy load them.
3.1 Using defineAsyncComponent
defineAsyncComponent is a Vue 3 API that lets you define async components with options for loading/error states.
Basic Usage:
// components/LazyModal.vue
<template>
<div class="modal">...</div>
</template>
// ParentComponent.vue
import { defineAsyncComponent } from 'vue';
// Lazy load LazyModal
const LazyModal = defineAsyncComponent(() => import('./LazyModal.vue'));
export default {
components: { LazyModal }
};
In the template, use <LazyModal> like any other component. It will load only when rendered.
3.2 Loading and Error States
Customize loading/error behavior by passing an options object to defineAsyncComponent:
const LazyModal = defineAsyncComponent({
// Loader function (returns a promise)
loader: () => import('./LazyModal.vue'),
// Component to show while loading (optional)
loadingComponent: () => import('./LoadingSpinner.vue'),
// Component to show on error (optional)
errorComponent: () => import('./ErrorComponent.vue'),
// Delay before showing loadingComponent (default: 200ms)
delay: 300,
// Timeout before showing errorComponent (default: Infinity)
timeout: 5000,
// Whether to retry on error (default: false)
retry: true,
// Number of retries (default: 3)
retryDelay: 1000
});
3.3 Integrating with <Suspense>
Vue 3’s <Suspense> component coordinates async dependencies (like async components) and shows fallback content until they resolve.
Example:
<!-- ParentComponent.vue -->
<template>
<div>
<button @click="showModal = true">Open Modal</button>
<Suspense v-if="showModal">
<!-- Async component -->
<template #default>
<LazyModal @close="showModal = false" />
</template>
<!-- Fallback (loading state) -->
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
const showModal = ref(false);
const LazyModal = defineAsyncComponent(() => import('./LazyModal.vue'));
</script>
<Suspense> waits for LazyModal to load and shows the fallback until then.
Image Lazy Loading in Vue.js
Images often account for 50%+ of a page’s weight. Lazy loading images ensures they load only when needed.
4.1 Native Image Lazy Loading
The simplest approach is to use the native loading="lazy" attribute (supported in modern browsers):
<!-- Native lazy loading (no JavaScript required) -->
<img
src="product.jpg"
alt="Product"
loading="lazy"
width="600"
height="400"
>
Caveats:
- Limited browser support (IE/Edge < 79 unsupported).
- No control over loading placeholders or error handling.
4.2 Using the vue-lazyload Library
For more control, use the popular vue-lazyload directive.
Step 1: Install the Library
npm install vue-lazyload --save
# or
yarn add vue-lazyload
Step 2: Register the Directive
// main.js
import { createApp } from 'vue';
import VueLazyload from 'vue-lazyload';
import App from './App.vue';
const app = createApp(App);
app.use(VueLazyload, {
// Global options (optional)
preLoad: 1.3, // Preload image when it’s 1.3x viewport height away
error: 'error.png', // Error placeholder
loading: 'loading.gif', // Loading placeholder
attempt: 1 // Number of retries on error
});
app.mount('#app');
Step 3: Use v-lazy in Templates
Replace src with v-lazy for images:
<template>
<div class="image-grid">
<!-- Lazy load images -->
<img v-lazy="image.url" :alt="image.alt" width="300" height="200" />
<!-- With custom loading/error placeholders -->
<img
v-lazy="{
src: image.url,
loading: 'custom-loading.gif',
error: 'custom-error.png'
}"
:alt="image.alt"
/>
</div>
</template>
4.4 Custom Intersection Observer Directives
For full control, build a custom directive using the Intersection Observer API to detect when an image enters the viewport.
Example Directive:
// directives/lazyLoad.js
export default {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
// Load the image when it intersects
el.src = binding.value;
observer.unobserve(el); // Stop observing after load
}
});
observer.observe(el);
}
};
// Register globally in main.js
app.directive('lazy', lazyLoad);
Usage:
<img v-lazy="image.url" alt="Lazy loaded" loading="lazy" />
Advanced Techniques and Best Practices
5.1 Code Splitting Strategies
- Route-Level Splitting: Always lazy load non-critical routes (e.g.,
/settings,/help). - Component-Level Splitting: Lazy load large components (e.g., data tables, charts) that aren’t needed on initial render.
- Library Splitting: Use
splitChunks(Webpack) oroptimizeDeps(Vite) to split third-party libraries (e.g.,vue,axios) into separate chunks.
5.2 Preloading Critical Resources
Use <link rel="preload"> to fetch critical lazy-loaded resources early (e.g., a frequently visited route):
<!-- Preload the About route chunk -->
<link rel="preload" href="/about.js" as="script">
5.3 Avoiding Common Pitfalls
- Over-Lazy Loading: Don’t lazy load small, critical components (e.g., headers, navigation)—this can introduce unnecessary delays.
- Ignoring Loading States: Always provide loading spinners or skeletons to manage user expectations.
- Poor Placeholder Design: Use low-quality image placeholders (LQIP) or SVG skeletons for images to improve perceived performance.
Conclusion
Lazy loading is a powerful technique to optimize Vue.js applications by deferring non-critical resource loading. By implementing route-based, component-based, and image lazy loading, you can significantly improve initial load times, reduce bandwidth usage, and boost Core Web Vitals.
Start with route-based lazy loading (the highest impact), then layer in component and image lazy loading for additional gains. Always test with tools like Lighthouse to measure improvements and avoid over-optimizing.