MEDIA GUIDES / Web Performance

Vue Router Lazy Loading: The Complete Guide

The need for performant and fast-loading websites and applications continues to grow as web pages become more interactive and as the average internet user expects to spend less than two seconds before a website is fully loaded. According to data from Google, the probability of a visitor bouncing (leaving without taking any action) from your website increases by 32% as page load time goes from 1 second to 3 seconds.

In Vue apps, there are many factors that can lead to your websites and apps becoming unnecessarily heavier, resulting in poor performance and bad user experience. Luckily, there are several performance optimization techniques you can apply to make your Vue apps more efficient and user friendly; one of these techniques is Vue Router lazy loading. In this guide, we’ll cover everything you need to know about Vue Router lazy loading, from the basics and advanced tips to practical examples you can learn from.

In this article:

What is Vue Router Lazy Loading and Why Does it Matter?

Vue Router is Vue’s official client-side routing library, used especially in building single-page applications (SPAs) by mapping browser URLs to different components or pages. Lazy loading, on the other hand, is a performance optimization technique in web development that allows you to load a resource (such as code, components, or static assets like images, fonts, etc) in an app only at the moment when the user requests for it by performing a specific action.

When you build a Vue app, all your components are bundled into JavaScript files, and if your app is complex and grows large, this bundle becomes big and slows down the first page load. Lazy loading helps to cut down the starting bundle size, so users see the first page quicker.

For example, when a user clicks a link to a new page, that’s when the code for that page gets downloaded and run. Combining Vue Router and lazy loading allows for a performance boost, preventing code for pages or components users will never visit from loading during the initial page load.

With Vue Router lazy loading, you get:

  • Faster Initial Page Load: Users see your main page quicker because the browser downloads less code upfront.
  • Better Performance: Slow connections or mobile devices don’t need to download as many resources, leading to faster site speeds.
  • Efficient Resource Usage: Users don’t download code for features they never use (like an admin dashboard for normal users).

Setting up a Vue 3 App With Vite

To replicate a real-world application, we’ll set up a demo app for this guide using Vite as the build tool.

First, make sure Node.js is installed on your computer. Then, run the following command to scaffold a Vue Single Page App:

npm create vue@latest

In the setup prompts, make sure the Router (SPA development) option is toggled on:

> npx
> create-vue

┌  Vue.js - The Progressive JavaScript Framework
│
◇  Project name (target directory):
│  vue-project
│
◆  Select features to include in your project: (↑/↓ to navigate, space to
select, a to toggle all, enter to confirm)
│  ◼ TypeScript
│  ◼ JSX Support
│  ◼ Router (SPA development)
│  ◻ Pinia (state management)
│  ◻ Vitest (unit testing)
│  ◻ End-to-End Testing
│  ◻ ESLint (error prevention)
│  ◻ Prettier (code formatting)

Next, change directories into the project folder and run npm install && npm run dev to install the app’s dependencies and start the development server.

Defining Lazy-Loaded Routes in Vue

Dynamic routes are supported by default starting from Vue Router version 4. If you bootstrapped your app with create-vue like we did earlier, you’d notice the dynamic import pattern in the router/index.ts file:

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
 history: createWebHistory(import.meta.env.BASE_URL),
 routes: [
   {
     path: '/',
     name: 'home',
     component: HomeView,
   },
   {
     path: '/about',
     name: 'about',
     // route level code-splitting
     // this generates a separate chunk (About.[hash].js) for this route
     // which is lazy-loaded when the route is visited.
     component: () => import('../views/AboutView.vue'),
   },
 ],
})

export default router

In the above code, the AboutView component is imported dynamically while the HomeView component is imported statically. The key difference between static and dynamic import lies in the import() syntax, a function-like expression that allows loading an ECMAScript module asynchronously and dynamically into a potentially non-module environment.

Here’s an example of a static import (also known as eager loading):

import HomePage from './views/HomePage.vue'
// This loads immediately on the first page load

And dynamic import (lazy loading):

const HomePage = () => import('./views/HomePage.vue')
// This loads only when needed

When a component is dynamically imported in a route, your app’s build tool (Vite in this case) splits your code into separate JavaScript chunks during the production build process. So, instead of delivering one large JavaScript file containing all your app’s code during the initial page load, multiple smaller bundles are loaded independently as the user navigates to each route–a process known as code splitting.

Grouping Components in the Same Chunk

Sometimes you want multiple components to share the same chunk, especially if they are related. For instance, if you have a dashboard page and you want to load it together with pages like /dashboard/profile and /dashboard/settings to reduce network requests, you can do so by adding a config in vite.config.js to setup manual chunks:

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'group-user': [
            './src/views/Dashboard.vue',
            './src/views/Profile.vue',
            './src/views/Settings.vue',
          ],
        },
      },
    },
  },
});

build.rollupOptions.output.manualChunks is a Vite build option that allows us to manually control how code is split into separate JavaScript chunks during the production build process. In the above example, when the app is being bundled during a production build, a group-user.js file is created, containing the code for the three components we grouped together.

This grouping offers two major benefits:

  1. Reduced Network Requests: By bundling related components, the browser only needs to make one HTTP request to download group-user.js instead of three separate requests for each component’s chunk. This is crucial for faster navigation after the first page load.
  2. Optimized Initial Load: For the primary component (like UserDashboard.vue), the related sub-components are already downloaded. This eliminates the loading delay when a user navigates from the dashboard to the profile or settings pages.

Handling Loading and Error States with Suspense

One of the drawbacks of lazy loading routes is the potential for pages to be blank while async parts loads or data to be displayed on the page is being fetched. Blank pages can create a disruptive experience for users and.

To fix this, we can wrap the contents in a <Suspense> component. <Suspense> is a Vue 3 component that helps to render loading screens or fallback content while waiting for data and async dependencies in the component tree to be resolved.

For example, say we have a Dashboard component with two sub components/pages: Profile and Settings:

<!-- Dashboard.vue -->
<template>
  <div class="dashboard">
    <h1>Dashboard</h1>


    <nav>
      <router-link to="/dashboard/profile">Profile</router-link>
      <router-link to="/dashboard/settings">Settings</router-link>
    </nav>

    <router-view v-slot="{ Component }">
      <Suspense>
        <template #default>
          <component :is="Component" />
        </template>
        <template #fallback>
          <div>Loading...</div>
        </template>
      </Suspense>
    </router-view>
  </div>
</template>

<!-- Profile.vue -->
<template>
  <div>
    <h2>User Profile</h2>
    <p>{{ userData }}</p>
  </div>
</template>

<script setup>
// Top-level await triggers Suspense
const userData = await fetchUserData()

async function fetchUserData() {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1000))
  return 'Success'
}
</script>

<!-- Settings.vue -->
<template>
  <div>
    <h2>User Settings</h2>
    <p>{{ settings }}</p>
  </div>
</template>

<script setup>
// Top-level await triggers Suspense
const settings = await fetchSettings()

async function fetchSettings() {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 800))
  return {data: [...]}
}
</script>

In the above example, the <Suspense> component in Dashboard.vue wraps the <router-view>, so it triggers whenever one of the child routes is loading or has async operations. <template #default> contains the actual content you want to render while <template #fallback> renders the placeholder or loader.

Performance Optimization Tips and Best Practices

To further improve your app’s loading speed and overall performance, take these additional steps and adopt the following best practices.

  • Always use a bundle analysis tool to visually inspect which modules and libraries contribute most to your final build size. Identify large, infrequently used components or heavy third-party dependencies that can be optimized or replaced. Some popular build analysis tools include vite-bundle-analyzer and vite-bundle-visualizer.
  • If you’re using Vite as your build tool, use npm run build -- --report to generate a detailed report of the build process and gain insights into:
    • Bundle size and composition (which modules contribute most to the size).
    • Dependency tree visualization.
    • Performance metrics and potential optimizations.
  • Avoid oversplitting or creating too many small chunks, which can be far worse than one large bundle. Instead, group small, related components together and use lazy-loading for other non-primary routes.
  • Strictly reserve lazy-loading for routes and components that are not part of the main, critical application flow or are rarely accessed.
  • Test your app on slow networks like 3G and monitor core performance metrics like Largest Contentful Paint (LCP), Time to Interactive (TTI), and other Core Web Vitals.

Using Vue Router Lazy Loading with Cloudinary

Lazy loading in Vue Router helps reduce the initial bundle size and speeds up the first render. When working with Cloudinary, this can be especially helpful because media driven pages often rely on heavier UI components. By splitting routes into smaller chunks, you only load the Cloudinary related code when the user navigates to a view that needs it.

A common pattern is creating a dedicated gallery or product view that pulls optimized images or videos from Cloudinary. Instead of bundling that view into the main chunk, you define the route with a dynamic import. This keeps the entry bundle lean and improves perceived performance before a user even reaches the media view.

Once the route loads, you can then fetch transformations on demand. Vue’s reactivity system makes it simple to update sources based on dynamic parameters such as breakpoints or user choices. The user receives a smoother navigation experience because Vue avoids loading unused components until they are required.

Making the Most Out of Vue Router Lazy Loading

Vue Router lazy loading is an effective way to improve the performance of your Vue applications, especially when multiple routes are involved. By strategically loading code only when it’s needed, you create faster initial load times and smoother user experiences. In this article, we covered various techniques for lazy loading routes in a Vue app, including basic dynamic imports, managing error and loading states with <Suspense>, chunk grouping, and performance optimizations.

As your application grows, these practices become increasingly valuable – they save resources, boost app performance, and provide a rich user experience even on slower networks or less powerful devices.

Get started with Vue and Cloudinary today, with the power of our Vue SDK to maximize the impact of your lazy loading efforts. Sign up for free and see it for yourself.

Frequently Asked Questions

How can I make my chunk names more meaningful?

You can use custom chunk names to make the chunks easily identifiable when debugging using the chunkFileNames option:

// Configure in vite.config.js
build: {
  rollupOptions: {
    output: {
      chunkFileNames: 'assets/[name]-[hash].js'
    }
  }

Do all browsers support dynamic import?

Nearly all modern browsers, including current versions of Chrome, Firefox, Safari, Edge, and Opera, fully support dynamic imports. Older, unsupported browsers (like IE11) may require a polyfill or specific build configuration to work

Should I lazy load small components?

Generally, not all components require lazy loading. The overhead of an HTTP request often outweighs the benefit for components under 10KB. Instead, focus on route components, large feature components, and modals/dialogs that are not needed immediately.

QUICK TIPS
Tamas Piros
Cloudinary Logo Tamas Piros

In my experience, here are tips that can help you better optimize Vue Router lazy loading beyond the basics:

  1. Use webpackChunkName comments for cleaner debugging
    Even in Vite (which uses Rollup under the hood), named dynamic imports (via Webpack-style comments) often persist during tooling transitions. Naming chunks like import(/* webpackChunkName: "about" */ './About.vue') aids in identifying chunks during performance audits.
  2. Preload strategic lazy chunks after initial load
    Use <link rel="preload"> or dynamic imports triggered during idle time (requestIdleCallback) to preload key lazy-loaded routes like frequently visited pages or checkout flows, improving perceived navigation speed.
  3. Avoid importing large libraries in lazy components
    If a lazy-loaded component imports a heavy library (like Moment.js or Lodash full), the entire chunk becomes bloated. Use tree-shakable or alternative micro-libraries (date-fns, lodash-es) for smaller, scoped imports.
  4. Use route-level guards to defer loading until preconditions are met
    Combine beforeEnter guards with lazy loading to defer route activation until data is fetched or permissions are validated. This improves UX by avoiding failed component loads or redirect flickers.
  5. Integrate lazy loading with route transitions for smoother UX
    Combine Vue Router lazy loading with <Transition> components to fade in content after it’s loaded. This masks latency and adds polish to dynamic navigation.
  6. Lazy load route-based layout components separately
    If you use multiple layout shells (e.g. auth, dashboard, marketing), lazy load these shells via nested routes so you’re not loading layout code for routes that don’t use them.
  7. Stagger lazy loading with progressive hydration
    When using SSR or hydration strategies, lazy load non-critical routes or widgets after initial hydration using Vue’s defineAsyncComponent and Suspense to prevent hydration mismatch errors.
  8. Monitor lazy load impact with performance.mark and Web Vitals
    Use performance.mark and performance.measure APIs around dynamic imports and combine with web-vitals package to monitor real impact of lazy loading on FID, LCP, and TTI.
  9. Create shared async boundaries for related components
    Wrap sets of lazy-loaded routes inside a shared <Suspense> in a layout component to avoid duplicating fallback UIs and to centralize loading/error state management.
  10. Bundle Cloudinary SDK separately
    If you’re using Cloudinary’s Vue SDK, isolate it into its own chunk or bundle to prevent it from inflating core app size. Then, lazy load it only where Cloudinary transformations or widgets are needed, such as media galleries or video routes.
Last updated: Dec 18, 2025