Vue is built to handle most typical use cases efficiently without requiring a lot of manual tweaking. But sometimes, you’ll hit situations that need a bit more fine-tuning. In this article, we’ll go over the key things to keep an eye on for optimizing performance in a Vue app.
Page Load Performance vs. Update Performance
When talking about optimizing performance in Vue apps, there are two main aspects that should be explained:
- Page Load Performance - This is all about how quickly the app loads content and becomes usable the first time someone visits it. It’s usually measured with Core Web Vitals metric Largest Contentful Paint (LCP).
- Update Performance - This focuses on how quickly the app responds to user actions, for instance, how fast a list updates when someone types in a search box. It’s usually measured with Core Web Vitals metric Interaction to Next Paint (INP).
Ideally, we’d want to maximize both, but different frontend architectures make it easier or harder to hit performance goals in each area. Plus, the kind of app you’re building has a big impact on which performance aspects should be the priority.
This article will focus mainly on the things that you can do to improve performance of Vue apps, like built-in directives, plugins, and concepts. We won’t touch on other aspects of optimization, such as HTML,CSS, API improvements.
Performance Measuring Tools
To boost performance, we first need a way to measure it. Luckily, there are some excellent tools that allow you to audit/measure performance of Vue websites in local development, Continuous Integration, and also in production.
For checking load performance in production:
Testing performance during local development can be done with:
Finally, you can also measure performance in Continuous Integration with:
Improving Page Load
This is all about how quickly the app loads content and becomes usable the first time someone visits it.
Architecture
If fast page load times are crucial for your app, try to avoid making it a purely client-side SPA.
It’s better for the server to send HTML that already contains the content users need, as client-side rendering alone can slow down the time until content becomes visible. Server-Side Rendering (SSR) or Static Site Generation (SSG) can help with this. To learn more about how SSR can improve performance of your Vue application, check out this article.
If your app doesn’t need a lot of complex interactivity, you can also use a traditional backend server to render the HTML and add Vue for client-side enhancements.
If you need your main app to be an SPA but have marketing pages (like landing pages or blog pages), consider splitting them off! Ideally, these marketing pages should be deployed as static HTML with minimal JavaScript using static site generation.
Bundling
The less code we ship to the client/browser, the more performant our Vue application will be.
- Prefer dependencies that offer ES module formats and are tree-shaking friendly.
- Make sure to tree-shake any unused code. Many Vue plugins and integrations come with this setup by default.
- If you are using Vue primarily for progressive enhancement and prefer to avoid a build step, consider using petite-vue (only 6kb) instead.
- Check a dependency's size and evaluate whether it is worth the functionality it provides. Tools like bundlejs.com can be used for quick checks, but measuring with your actual build setup will always be the most accurate.
Code Splitting
Code splitting is a technique employed by build tools to divide an application bundle into smaller, more manageable chunks. These chunks can be loaded either on demand or in parallel, optimizing performance by reducing the amount of code that needs to be loaded initially.
It can be achieved by using one or all of the following approaches.
// belowTheFold.js
function lazyLoadImport() {
return import("./belowTheFold.js");
}
belowTheFold.js
and its dependencies will be split into a separate chunk and only loaded when lazyLoadImport()
is called.
Lazy loading is particularly useful for features that aren't essential immediately after the initial page load. In Vue applications, this can be implemented by using Vue's Async Component feature, enabling you to split the component tree into smaller chunks that are loaded on demand as needed.
import { defineAsyncComponent } from "vue";
const MyComponent = defineAsyncComponent(() => import("./MyComponent.vue"));
For applications using Vue Router, it is strongly recommended to use Lazy Loading Routes:
const router = createRouter({
// ...
routes: [
{ path: "/tasks/:id", component: () => import("./views/TaskDetails.vue") },
],
});
Vue Router will only fetch the component code when entering the page for the first time, and then use the cached version.
Improving App Reactivity
This focuses on how quickly the app responds to user actions, for instance, how fast a list updates when someone types in a search box.
v-show vs v-if
Both v-if
and v-show
control the visibility of elements, but they do so in different ways. v-if
adds or removes elements from the DOM entirely, which can be more performance-intensive. On the other hand, v-show
toggles the display
CSS property, making it a more efficient choice for elements that need to be shown or hidden frequently.
<script setup>
import { ref } from "vue";
const show = ref(false);
</script>
<template>
<div>
<button @click="show = !show">Toggle</button>
<div v-show="show">Toggled display property</div>
<div v-if="show">Element added to the DOM</div>
</div>
</template>
Consider using v-show
for elements that are frequently toggled to make the state changes more performant.
Rendering content without a need to update later
v-once
is a built-in directive that can be used to render content that relies on runtime data but never needs to update. The entire sub-tree it is used on will be skipped for all future updates.
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element has children -->
<div v-once>
<h1>Comment</h1>
<p>{{msg}}</p>
</div>
<!-- `v-for` directive -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
Conditionally skip updating large lists
v-memo
is a built-in directive that can be used to conditionally skip the update of large sub-trees or v-for lists.
<div v-memo="[valueA, valueB]">...</div>
When the component re-renders, if both valueA and valueB remain the same, all updates for this <div>
and its children will be skipped.
Debouncing
When managing user input, such as search queries or form submissions, it's important to debounce or throttle events to prevent performance issues.
Debouncing delays the execution of a function until a specified time has passed since the last invocation, while throttling ensures the function is executed only once within a given time interval.
<script setup>
import { ref } from "vue";
import debounce from "lodash-es/debounce";
const value = ref("");
const search = debounce(async () => {
await api.getSearchResults(value); // mocked search operation
}, 400);
watch(value, (newVal) => {
search();
});
</script>
<template>
<input v-model="”value”" />
</template>
In this example, the search function will execute only 400 milliseconds after the user stops typing, minimizing the number of API calls and improving performance.
General improvements
Apart from page load time and app reactivity, there are also several general improvements that you can add to your app to make it generally more performant.
Avoid making static data reactive
Vue reactivity is a great tool that allows us to keep track of the changes on a property and modify its value whenever it is referenced. But sometimes, we can mistakenly make a static property reactive such as in the example below:
<script setup>
import { ref } from "vue";
const milisecondsInAnHour = ref(3600000);
</script>
The value of milisecondsInAnHour
variable won’t change over time so there is no need to make it reactive.
Shallow reactivity for large data sets
Vue's reactivity system is deep by default, which makes state management intuitive but can introduce overhead when dealing with large data sets. This is because each property access triggers proxy traps for dependency tracking.
To mitigate this, Vue offers shallowRef()
and shallowReactive()
, which allow you to opt-out of deep reactivity. These shallow APIs create reactive state only at the root level, leaving nested objects untouched.
const shallowArray = shallowRef([
/* big list of deep objects */
]);
// this won't trigger updates...
shallowArray.value.push(newObject);
// this does:
shallowArray.value = [...shallowArray.value, newObject];
Caching
Navigating forward and backward usually results in triggering all the logic to render views and components. This can result in triggering multiple requests for the data that should be already available because we have already visited this page.
For such content we can utilize the concept of caching (and more specifically the stale-while-revalidate approach) that will allow us to cache the result of a request and return it instantly for the user, greatly improving the performance of next entries.
Stale-while-revalidate is an HTTP cache strategy where the browser checks if a cached response is still fresh. If it is, the browser serves the cached content; if not, it "revalidates" by fetching a fresh response from the network while still serving the stale content to the user.
In Vue we can use the SWRV library like following:
import useSWRV from "swrv";
const { data, error } = useSWRV("/api/user", fetcher);
In this example, the Vue composable useSWRV
accepts a key
and a fetcher
function. key
is a unique identifier of the request, normally the URL of the API. And the fetcher
accepts key
as its parameter and returns the data
asynchronously. useSWRV
also returns 2 values: data
and error
. When the request (fetcher) is not yet finished, data will be undefined
.
Rendering large lists
One of the most common performance challenges in frontend applications is rendering large lists. Regardless of how efficient the framework is, displaying a list with thousands of items can be slow due to the sheer number of DOM nodes the browser has to manage. To solve this issue, you could use the vue-virtual-scroller package.
Avoid memory leaks
The most common scenario for a memory leak is adding a window event listener in the initial load of the component but not removing it after the component is unmounted.
<script setup>
Import { onMounted, onUnmounted } from ‘vue’
onMounted(() => window.addEventListener('mousemove', doSomething))
onUnmounted(() => window.removeEventListener('mousemove', doSomething))
</script>
Optimize images
Usually, unoptimized images are the main source for bad performance. However, this issue can be easily fixed by using solutions such as unpic/vue which is a high-performance responsive image component for Vue.
<script setup lang="ts">
import { Image } from "@unpic/vue";
</script>
<template>
<image
src="https://cdn.shopify.com/static/sample-images/bath_grande_crop_center.jpeg"
layout="constrained"
width="800"
height="600"
alt="A lovely bath"
/>
</template>
It generates a responsive <img>
tag that follows best practices, with the correct srcset, sizes and styles. The component also detects image URLs from most image CDNs and CMSs and can resize images with no build step.
Summary
Vue as a framework is designed with performance in mind while you can also use built in optional features like directives to make it even more performant. Implementing solutions from the previous sections should help you optimize performance of your Vue app.
Keep in mind that in order to improve performance of your web application, you would also need to optimize performance of your HTML, CSS, and API to achieve a generally performant application.
Bonus: Monitor Vue app with DebugBear
By using DebugBear, you get all valuable data about performance of your Vue app, information about competitors, ability to run tests from multiple locations, and all that from a single dashboard! Sign up for a free trial here!
Apart from the dashboard, you could also use the HTML Size Analyzer to see exactly what content is contributing to your document size. Find issues like inline images, large hydration state or code duplication.
This can help you discover not optimized code and places that could be improved.
Additional resources
If you would like to learn more about these concepts, please check out the following articles: