Squeezing Every Drop of Performance from Vue 3: shallowRef, v-memo, and Async Components

Squeezing Every Drop of Performance from Vue 3: shallowRef, v-memo, and Async Components

Vue 3’s Proxy-based reactivity system and optimized virtual DOM make it incredibly fast by default. For most standard web applications, you rarely need to think about rendering bottlenecks.

However, when you start building enterprise-scale dashboards, rendering massive data tables, or handling complex WebSocket streams, the default reactivity system can become a liability. If your application feels sluggish when dealing with large datasets, you don't need a new framework—you just need to tune your engine.

Here are three advanced techniques to optimize Vue 3 for extreme performance.

1. Escaping Deep Reactivity with shallowRef

By default, when you wrap an object or an array in ref(), Vue deeply traverses that data structure and makes every single nested property reactive.

If you are fetching a massive JSON payload with 10,000 records from an API to display in a data table, deep reactivity is a massive waste of CPU cycles and memory. You only need the array itself to be reactive so the table renders, not every individual property of every row.

The Slow Way:

<script setup>
import { ref, onMounted } from "vue";

// Vue will recursively map Proxies to every single nested object here
const massiveDataset = ref([]);

onMounted(async () => {
  massiveDataset.value = await fetchLargeData();
});
</script>

The Performant Way (shallowRef):shallowRef only tracks the .value property. It does not look inside the object. This bypasses the deep Proxy conversion, saving massive amounts of memory and initialization time.

<script setup>
import { shallowRef, onMounted } from "vue";

// Vue only watches the top-level '.value' reassignment
const massiveDataset = shallowRef([]);

onMounted(async () => {
  // This triggers an update instantly without deep tracking
  massiveDataset.value = await fetchLargeData();
});
</script>

Note: If you need to mutate a specific property inside a shallowRef and force the UI to update, you can manually call triggerRef(massiveDataset).

2. Bypassing the Virtual DOM with v-memo

Introduced in Vue 3.2, v-memo is a directive that memoizes (caches) a sub-tree of the template.

When Vue re-renders a component, it compares the old Virtual DOM to the new Virtual DOM. If you have a massive list (v-for) and only one item changes, Vue still has to walk through the entire list to figure that out.

v-memo tells Vue: "Skip looking at this element and its children entirely unless these specific values change."

The Implementation:

<template>
  <div
    v-for="item in massiveList"
    :key="item.id"
    v-memo="[item.id === selectedId, item.updatedAt]"
  >
    <h3>{{ item.title }}</h3>
    <p>{{ item.description }}</p>
    <span v-if="item.id === selectedId">Selected!</span>
  </div>
</template>

In this example, Vue will skip the virtual DOM diffing for every single item in the list except the one where item.id === selectedId evaluates to true, or if that specific item's updatedAt timestamp changes. For lists larger than 1,000 items, this turns a sluggish UI into a perfectly smooth 60fps experience.

3. Chunk Splitting with defineAsyncComponent

Not every component needs to be loaded when the user first visits your page. Heavy components—like complex data visualization charts (e.g., Chart.js or ECharts), rich text editors, or massive modal dialogs—bloat your initial JavaScript bundle.

You can tell Vue and Vite to split these into separate network requests and only load them when they are actually needed using defineAsyncComponent.

The Standard (Blocking) Import:

<script setup>
// This forces the chart library into the main bundle
import HeavyAnalyticsChart from "./HeavyAnalyticsChart.vue";
</script>

The Async (Lazy-Loaded) Import:

<script setup>
import { defineAsyncComponent, ref } from "vue";

// This creates a separate JS chunk that is only fetched over the network
// when <HeavyAnalyticsChart> actually enters the DOM.
const HeavyAnalyticsChart = defineAsyncComponent(
  () => import("./HeavyAnalyticsChart.vue"),
);

const showChart = ref(false);
</script>

<template>
  <button @click="showChart = true">Load Analytics</button>

  <template v-if="showChart">
    <HeavyAnalyticsChart />
  </template>
</template>

Conclusion

Performance tuning in Vue 3 is all about understanding what the framework is doing under the hood. By utilizing shallowRef to prevent unnecessary reactivity, v-memo to shortcut the virtual DOM, and defineAsyncComponent to trim your bundle size, you can build enterprise applications that feel instantly responsive.

Does your application feel sluggish as your user base grows? I specialize in auditing and refactoring frontend architectures for maximum performance. Check out my Maintenance & Feature Expansion services to see how we can optimize your codebase.

A Note on Process

This blog serves as a personal learning journal where I dive into topics that capture my curiosity. To refine my understanding and the clarity of the writing, I use AI as a collaborative partner in the research and composition process.

Built with the help of AI

By Ismail Ammor ·