Optimising Vue.js Performance with Lazy Loading and Code Splitting

Hero image for Optimising Vue.js Performance with Lazy Loading and Code Splitting. Image by Joel Filipe.
Hero image for 'Optimising Vue.js Performance with Lazy Loading and Code Splitting.' Image by Joel Filipe.

Performance work in Vue often starts with the bundle. If we ship too much JavaScript on the initial visit, the application may technically work, but it will still feel heavy on slower devices and weaker networks. Lazy loading and code splitting help by ensuring we load the right code at the right moment.

That said, these techniques are only effective when applied deliberately. Splitting everything into tiny fragments can create just as many problems as bundling everything together.


Understand the Two Main Goals

When we talk about lazy loading and code splitting, we are usually trying to improve two things:

  • initial page load performance
  • the amount of JavaScript the browser has to parse and execute up front

In other words, this is not just about transfer size. It is about work. A smaller initial bundle usually means less download, less parse time, and less execution time.


Start with Route‑Level Splitting

For most applications, routelevel splitting is the highestvalue place to begin:

import { createRouter, createWebHistory } from 'vue-router';const router = createRouter({  history: createWebHistory(),  routes: [    {      path: '/dashboard',      component: () => import('@/pages/DashboardPage.vue'),    },    {      path: '/reports',      component: () => import('@/pages/ReportsPage.vue'),    },  ],});export default router;

This keeps rarely visited sections out of the initial bundle, which is almost always a sensible default for larger applications.

Vue Router's own guidance is quite direct here: route components should usually be lazy loaded with dynamic imports. That is the cleanest starting point because route boundaries are already meaningful user boundaries.


Route Components and Async Components are Not the Same Thing

This detail is easy to blur. Vue Router treats lazyloaded route components as dynamic imports, and that is distinct from Vue's general async component feature.

That matters because route boundaries and componentlevel splitting solve different problems. Route splitting is about page delivery. Async components are more useful inside a route when a specific panel, chart, editor, or adminonly tool is expensive and not immediately needed.


Use Async Components for Heavy UI

When a component is expensive and not immediately needed, defineAsyncComponent() can be a good fit:

import { defineAsyncComponent } from 'vue';export const ChartPanel = defineAsyncComponent(  () => import('@/components/ChartPanel/ChartPanel.vue'),);

This works especially well for tab panels, charts, editors, and adminonly tools that do not need to block the first render.

It becomes less useful when we start wrapping half the application in async boundaries without a clear reason. The point is to defer meaningful cost, not to create the impression of sophistication.


Chunk Strategy Still Needs Product Sense

One of the more useful parts of the Vue Router documentation is the reminder that related async modules can be grouped into the same chunk. That can make a lot of sense when nested route sections are nearly always visited together.

This is where performance work stops being a mechanical exercise and becomes architecture. If the product boundary is "all user account pages", a grouped account chunk may be smarter than several tiny fragments that create extra requests and awkward loading transitions.


Measure Before and After

It is easy to assume code splitting always helps. Usually it does, but not every split is valuable. If a chunk is requested immediately after page load on almost every visit, splitting it out may add network overhead without a meaningful UX gain.

This is why measurement matters. We should inspect bundle output, route timings, and real usage patterns rather than treating lazy loading as a ritual.

The most useful questions are usually practical ones:

  • what is still in the initial bundle?
  • which routes are genuinely expensive?
  • which deferred chunks are requested almost immediately anyway?
  • where are users waiting without useful feedback?

Loading Experience is Part of Performance

Performance work should not make the application feel more fragmented. If code splitting introduces several jarring loading states, the user may not experience the application as faster even if the bundle metrics improved.

That is why I would treat loading UX as part of the same problem. Routelevel spinners, skeletons, async component fallbacks, and preloading strategy all shape whether the optimisation feels coherent in practice.


Watch for Architectural Side Effects

A few warning signs are worth watching:

  • async imports scattered randomly through the codebase
  • loading states duplicated inconsistently
  • overly small chunks that fragment the user journey
  • large shared dependencies repeated across many async routes

Good scalability comes from choosing lazy loading points that align with actual product boundaries.

At first glance, it can seem that lazy loading is enough on its own. It is not. If the application contains heavy thirdparty packages, unnecessary abstractions, or unstable update paths, bundle splitting will only mask part of the problem.

Vue's own performance guidance makes this clear: architecture, dependency choice, and rendering strategy still matter.

The Vue documentation below is especially useful if we want the exact API behaviour behind these patterns:


Wrapping up

Lazy loading and code splitting are powerful because they let us align delivery with user intent. When we split at route boundaries, defer genuinely expensive UI, and keep the loading experience coherent, Vue applications become faster without becoming harder to maintain. That balance is the real goal.

Key Takeaways

  • Begin with routelevel splitting because it usually delivers the clearest benefit.
  • Route lazy loading and async components solve related but different problems.
  • Use async components for genuinely heavy UI that is not needed immediately.
  • Measure the impact instead of assuming every split improves performance.

Once lazy loading reflects actual product boundaries, performance work becomes more robust and much easier to scale.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript
  5. Performance
  6. Vue.js