Vue
05 / 08

Performance & Best Practices

Vue Performance & Best Practices

Optimize your Vue applications for maximum performance and follow best practices for maintainable, scalable code:

Component Performance Optimization

v-once & v-memo

Use v-once to render elements only once and v-memo to memoize template sub-trees.

<template>
  <!-- v-once: renders only once, never updates -->
  <div v-once>
    <h1>{{ staticTitle }}</h1>
    <p>{{ staticDescription }}</p>
  </div>
  
  <!-- v-memo: memoizes based on dependency array -->
  <div v-memo="[count]">
    <!-- Only re-renders when count changes -->
    <p>Count: {{ count }}</p>
    <p>Other data: {{ otherData }}</p>
  </div>
  
  <!-- Memo for list items -->
  <div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
    <!-- Only re-renders when id or selected changes -->
    {{ item.name }}
  </div>
</template>

Computed vs Methods

<script setup>
import { ref, computed } from 'vue'

const items = ref([/* lots of items */])

// ✅ Good: Computed - cached, only recalculates when items change
const expensiveComputation = computed(() => {
  console.log('Computing...')
  return items.value
    .filter(item => item.active)
    .map(item => item.price)
    .reduce((sum, price) => sum + price, 0)
})

// ❌ Bad: Method - recalculates on every render
const expensiveMethod = () => {
  console.log('Computing...')
  return items.value
    .filter(item => item.active)
    .map(item => item.price)
    .reduce((sum, price) => sum + price, 0)
}
</script>

<template>
  <div>
    <!-- Computed: Cached, efficient -->
    <p>Total: {{ expensiveComputation }}</p>
    
    <!-- Method: Recalculates every time -->
    <p>Total: {{ expensiveMethod() }}</p>
  </div>
</template>

Virtual Scrolling

For rendering large lists, use virtual scrolling to only render visible items.

// Using vue-virtual-scroller
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

const items = ref(
  Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    description: `Description for item ${i}`,
  }))
)
</script>

<template>
  <RecycleScroller
    :items="items"
    :item-size="80"
    key-field="id"
    class="scroller"
    style="height: 600px"
  >
    <template #default="{ item }">
      <div class="item">
        <h3>{{ item.name }}</h3>
        <p>{{ item.description }}</p>
      </div>
    </template>
  </RecycleScroller>
</template>

Lazy Loading Components

<script setup>
import { defineAsyncComponent } from 'vue'

// Lazy load heavy components
const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// With loading and error states
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./components/HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200, // Delay before showing loading component
  timeout: 3000, // Timeout for loading
})

const showChart = ref(false)
</script>

<template>
  <div>
    <button @click="showChart = true">Show Chart</button>
    
    <Suspense v-if="showChart">
      <template #default>
        <HeavyChart :data="chartData" />
      </template>
      <template #fallback>
        <div>Loading chart...</div>
      </template>
    </Suspense>
  </div>
</template>

Avoid Unnecessary Reactivity

<script setup>
import { ref, shallowRef, markRaw } from 'vue'

// For large, immutable data, use shallowRef
const hugeDataset = shallowRef({
  /* thousands of items */
})

// For non-reactive objects (like third-party class instances)
const chartInstance = shallowRef(null)

onMounted(() => {
  // markRaw prevents Vue from making it reactive
  chartInstance.value = markRaw(new Chart())
})

// Constants don't need to be reactive
const API_URL = 'https://api.example.com' // ✅ Not reactive
const config = { timeout: 5000 } // ✅ Not reactive

// Only make data reactive if it needs to trigger updates
const userSettings = ref({ theme: 'dark' }) // ✅ Needs reactivity
</script>

Production Build Optimization

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    // Enable tree-shaking
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true, // Remove console.log in production
        drop_debugger: true,
      },
    },
    // Code splitting
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-components': ['./src/components/ui'],
        },
      },
    },
    // Chunk size warnings
    chunkSizeWarningLimit: 1000,
  },
  optimizeDeps: {
    include: ['vue', 'vue-router', 'pinia'],
  },
})

Best Practices

  • Use <script setup> for cleaner, more performant code

  • Prefer computed() over methods for derived data

  • Use shallowRef/shallowReactive for large data structures

  • Lazy load routes and heavy components

  • Use v-show for frequently toggled elements, v-if for rare toggles

  • Extract reusable logic into composables

  • Keep components focused and single-responsibility

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free