Chapter 10 - Supercharge Vue.js: Code Splitting and Lazy Loading for Lightning-Fast Apps

Code splitting and lazy loading in Vue.js optimize performance by loading components on-demand. This reduces initial bundle size, improves load times, and enhances user experience, especially for large applications.

Chapter 10 - Supercharge Vue.js: Code Splitting and Lazy Loading for Lightning-Fast Apps

Vue.js is all about building blazing-fast web apps, but as your project grows, so does your bundle size. That’s where code splitting and lazy loading come in handy. Let’s dive into these advanced techniques to supercharge your Vue.js applications.

First things first, what exactly is code splitting? It’s like breaking up your code into smaller, more manageable chunks. Instead of loading everything upfront, you only load what you need, when you need it. This can significantly improve your app’s initial load time and overall performance.

Lazy loading goes hand in hand with code splitting. It’s the art of loading components or modules only when they’re required. Think of it as a “just-in-time” approach to loading your app’s resources.

Now, let’s get our hands dirty with some code. We’ll start by setting up a basic Vue.js project using the Vue CLI. If you haven’t already, install it globally:

npm install -g @vue/cli

Create a new project:

vue create vue-code-splitting-demo
cd vue-code-splitting-demo

When prompted, choose the default Vue 3 preset. Once the project is set up, we’re ready to roll.

Let’s create a few components to demonstrate code splitting. Add these files to your src/components directory:

Home.vue:

<template>
  <div>
    <h1>Welcome Home!</h1>
    <p>This is the main page of our app.</p>
  </div>
</template>

<script>
export default {
  name: 'Home'
}
</script>

About.vue:

<template>
  <div>
    <h1>About Us</h1>
    <p>We're a cool company doing cool things.</p>
  </div>
</template>

<script>
export default {
  name: 'About'
}
</script>

Contact.vue:

<template>
  <div>
    <h1>Contact Us</h1>
    <p>Get in touch with us anytime!</p>
  </div>
</template>

<script>
export default {
  name: 'Contact'
}
</script>

Now, let’s set up our router. Install Vue Router if you haven’t already:

npm install vue-router@4

Create a new file src/router/index.js:

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../components/Home.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../components/About.vue')
  },
  {
    path: '/contact',
    name: 'Contact',
    component: () => import(/* webpackChunkName: "contact" */ '../components/Contact.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

Notice how we’re importing the About and Contact components. We’re using dynamic imports with webpack magic comments. This tells webpack to create separate chunks for these components.

Update your src/main.js to use the router:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

Now, let’s modify App.vue to use our router:

<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/contact">Contact</router-link>
    </nav>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

With this setup, when you run your app and open the browser’s network tab, you’ll see that the About and Contact components are loaded only when you navigate to those routes. Pretty cool, right?

But wait, there’s more! We can take this a step further by using Webpack’s prefetch and preload directives. These allow us to give hints to the browser about resources that might be needed in the future.

Let’s update our router configuration:

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about", webpackPrefetch: true */ '../components/About.vue')
  },
  {
    path: '/contact',
    name: 'Contact',
    component: () => import(/* webpackChunkName: "contact", webpackPreload: true */ '../components/Contact.vue')
  }
]

We’ve added webpackPrefetch: true to the About component and webpackPreload: true to the Contact component. Prefetch is used for resources that might be needed for future navigations, while preload is for resources that will definitely be needed for the current page.

Now, let’s talk about async components. These are perfect for larger components that aren’t immediately necessary. Vue 3 provides an defineAsyncComponent helper for this purpose.

Create a new component HeavyComponent.vue:

<template>
  <div>
    <h2>I'm a heavy component!</h2>
    <p>I took a while to load, but I'm worth it.</p>
  </div>
</template>

<script>
export default {
  name: 'HeavyComponent',
  mounted() {
    console.log('Heavy component mounted!')
  }
}
</script>

Now, let’s use this component in our Home.vue:

<template>
  <div>
    <h1>Welcome Home!</h1>
    <p>This is the main page of our app.</p>
    <button @click="showHeavy = true">Load Heavy Component</button>
    <Suspense v-if="showHeavy">
      <template #default>
        <HeavyComponent />
      </template>
      <template #fallback>
        <p>Loading heavy component...</p>
      </template>
    </Suspense>
  </div>
</template>

<script>
import { defineAsyncComponent, ref } from 'vue'

const HeavyComponent = defineAsyncComponent(() =>
  import(/* webpackChunkName: "heavy" */ './HeavyComponent.vue')
)

export default {
  name: 'Home',
  components: {
    HeavyComponent
  },
  setup() {
    const showHeavy = ref(false)
    return { showHeavy }
  }
}
</script>

Here, we’re using defineAsyncComponent to lazily load our HeavyComponent. We’re also using Vue 3’s <Suspense> component to handle the loading state.

But what if we want to optimize our images too? We can use the v-lazy-image directive for this. First, install it:

npm install v-lazy-image

Then, in your main.js:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import VLazyImage from "v-lazy-image"

const app = createApp(App)
app.use(router)
app.component("v-lazy-image", VLazyImage)
app.mount('#app')

Now you can use it in your components like this:

<template>
  <div>
    <v-lazy-image src="path/to/image.jpg" />
  </div>
</template>

This will lazy load your images, improving your page load times even further.

Remember, while code splitting and lazy loading are powerful techniques, they’re not always necessary for every project. For smaller applications, the overhead of splitting your code might outweigh the benefits. Always measure your performance gains to ensure you’re actually improving your app’s performance.

Webpack’s bundle analyzer is a great tool for visualizing your bundle size and identifying opportunities for optimization. Install it:

npm install --save-dev webpack-bundle-analyzer

Then add this to your vue.config.js:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  configureWebpack: {
    plugins: [
      new BundleAnalyzerPlugin()
    ]
  }
}

Run your build command, and you’ll get a visual representation of your bundle size.

In conclusion, code splitting and lazy loading are powerful tools in your Vue.js optimization toolkit. They allow you to deliver a faster initial load time and a more efficient application overall. By leveraging Webpack’s features and Vue’s built-in capabilities, you can create a smooth, performant user experience that keeps your users coming back for more.

Remember, optimization is an ongoing process. Keep an eye on your bundle sizes, regularly profile your app’s performance, and always be on the lookout for new ways to improve. Happy coding!