NashTech Blog

Vue.js Best Practices: Common Mistakes to Avoid and Performance Optimization

Table of Contents

Introduction

Vue.js has become one of the most beloved frontend frameworks, praised for its gentle learning curve and intuitive design. However, even experienced developers can fall into common traps that significantly impact application performance and maintainability.

Whether you’re building a simple dashboard or a complex enterprise application, avoiding these pitfalls can be the difference between a smooth user experience and frustrated users abandoning your app. In this comprehensive guide, we’ll explore the most frequent Vue.js mistakes and provide actionable solutions to optimize your application’s performance.

The stakes are high: poor performance can lead to decreased user engagement, lower SEO rankings, and ultimately, business losses. Let’s dive into how you can write better Vue.js code that scales.

I. Common Vue.js Mistakes That Degrade Application Performance

1. Misusing v-if vs v-show

One of the most fundamental yet misunderstood concepts in Vue.js is when to use v-if versus v-show.

❌ The Mistake:

<template>
  <!-- Modal that toggles frequently -->
  <div v-if="showModal" class="modal">
    <ExpensiveComponent />
  </div>
</template>

Many developers default to v-if thinking it’s always the better choice, but this creates unnecessary DOM manipulation when elements toggle frequently.

✅ The Solution:

<template>
  <!-- Use v-show for frequently toggled elements -->
  <div v-show="showModal" class="modal">
    <ExpensiveComponent />
  </div>
</template>

When to use what:

  • v-if: Use for conditional rendering when the element rarely appears (lazy rendering)
  • v-show: Use for elements that toggle frequently (CSS-based visibility)

Performance Impact: Using v-if for frequently toggled elements can cause up to 3x slower rendering due to constant DOM creation/destruction.

2. Prop Drilling

Prop drilling is when you pass data through multiple component layers, even when intermediate components don’t need it.

❌ The Problem:

<!-- App.vue -->
<Parent :user="user" />

<!-- Parent.vue -->
<Child :user="user" />

<!-- Child.vue -->
<GrandChild :user="user" />

<!-- GrandChild.vue -->
<template>
  <div>{{ user.name }}</div>
</template>

This creates a fragile chain where any change in the data structure affects multiple components.

✅ The Solution – Global State Management:

// store/user.js (Pinia)
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: 'John Doe',
    age: 30,
    email: 'john@example.com'
  }),
  actions: {
    updateName(newName) {
      this.name = newName
    }
  }
})
<!-- Any component can access the store directly -->
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>

<template>
  <div>
    <p>{{ userStore.name }}</p>
    <button @click="userStore.updateName('Jane Doe')">Update Name</button>
  </div>
</template>

Best Practice Guidelines:

  • Use props + emit for direct parent-child communication
  • Use Pinia/Vuex when state needs to be shared across multiple components
  • If you’re passing props through more than 2 levels, consider global state management

3. Watcher Overuse

Watchers are powerful but often misused for simple data transformations that should use computed properties.

❌ Inefficient Approach:

export default {
  data() {
    return {
      firstName: '',
      lastName: '',
      fullName: ''
    }
  },
  watch: {
    firstName() {
      this.fullName = `${this.firstName} ${this.lastName}`
    },
    lastName() {
      this.fullName = `${this.firstName} ${this.lastName}`
    }
  }
}

This approach creates unnecessary reactive dependencies and manual synchronization.

✅ Optimized Solution:

export default {
  data() {
    return {
      firstName: '',
      lastName: ''
    }
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  }
}

Key Differences:

  • Computed properties: For deriving data from existing state (pure functions)
  • Watchers: For side effects when data changes (API calls, DOM manipulation)

4. Memory Leaks

Failing to clean up resources is one of the most common causes of memory leaks in Vue applications.

❌ Memory Leak Example:

export default {
  mounted() {
    window.addEventListener('scroll', this.handleScroll)
    this.timer = setInterval(this.fetchData, 1000)
    this.websocket = new WebSocket('ws://localhost:8080')
  }
  // Missing cleanup = guaranteed memory leak!
}

✅ Proper Cleanup:

export default {
  mounted() {
    window.addEventListener('scroll', this.handleScroll)
    this.timer = setInterval(this.fetchData, 1000)
    this.websocket = new WebSocket('ws://localhost:8080')
  },
  beforeUnmount() { // Vue 3 (use beforeDestroy in Vue 2)
    window.removeEventListener('scroll', this.handleScroll)
    clearInterval(this.timer)
    this.websocket.close()
  }
}

Critical Resources to Clean Up:

  • Event listeners (window, document events)
  • Timers (setInterval, setTimeout)
  • Third-party library instances
  • WebSocket connections
  • Observer subscriptions

5. Non-Unique Keys in v-for

Using array indices as keys is one of the most dangerous practices in Vue.js.

❌ Dangerous Practice:

<template>
  <!-- Using index as key - DANGER! -->
  <div v-for="(item, index) in items" :key="index">
    <input v-model="item.name" />
    <button @click="deleteItem(index)">Delete</button>
  </div>
</template>

When you delete the first item, all input values shift to wrong positions because Vue can’t properly track which component corresponds to which data.

✅ Safe Approach:

<template>
  <div v-for="item in items" :key="item.id">
    <input v-model="item.name" />
    <button @click="deleteItem(item.id)">Delete</button>
  </div>
</template>

Golden Rule: Always use unique, stable identifiers as keys, never array indices.

II. Performance Optimization Strategies

Strategy 1: Smart Loop and Conditional Management

  • v-if/v-else: Apply conditional rendering strategically to avoid creating unnecessary DOM elements.
  • Computed properties: Leverage Vue’s built-in caching mechanism to handle complex calculations efficiently and reduce redundant computations.

Optimize Template Logic:

<template>
  <!-- Use computed properties instead of methods in templates -->
  <div v-for="user in filteredUsers" :key="user.id">
    {{ user.name }}
  </div>

  <!-- Use v-if for expensive components -->
  <HeavyChart v-if="shouldShowChart" />

  <!-- Use v-show for frequent toggles -->
  <LoadingSpinner v-show="isLoading" />
</template>

<script>
export default {
  computed: {
    filteredUsers() {
      // This is cached and only recalculates when dependencies change
      return this.users.filter(user => user.isActive)
    },
    shouldShowChart() {
      return this.chartData.length > 0 && this.userPreferences.showChart
    }
  }
}
</script>

Strategy 2: DOM Optimization Techniques

  • v-once: Use this directive for static content that does not change during the component’s lifecycle, ensuring Vue renders it only once.
  • Keep-alive: Retain component instances across navigations to reduce unnecessary re-rendering and improve performance.

Reduce Unnecessary Re-renders:

<template>
  <!-- v-once for static content that never changes -->
  <h1 v-once>{{ appTitle }}</h1>

  <!-- keep-alive for expensive components -->
  <keep-alive>
    <component :is="currentTab" />
  </keep-alive>
</template>

Strategy 3: Advanced State Management

  • Vuex/Pinia: Implement centralized state management to handle complex applications with shared state, ensuring consistency and easier maintainability.

Normalize Your State Structure:

// Normalized state for O(1) lookups
const store = {
  state: {
    users: {
      1: { id: 1, name: 'John', departmentId: 10 },
      2: { id: 2, name: 'Jane', departmentId: 20 }
    },
    departments: {
      10: { id: 10, name: 'Engineering' },
      20: { id: 20, name: 'Marketing' }
    },
    userIds: [1, 2]
  },
  getters: {
    // Efficient derived data
    usersWithDepartments: (state) => {
      return state.userIds.map(id => ({
        ...state.users[id],
        department: state.departments[state.users[id].departmentId]
      }))
    }
  }
}

Strategy 4: API Call Optimization

  • Pagination: Use pagination to limit the amount of data loaded at once, reducing payload size and improving initial load performance.
  • Caching: Apply intelligent caching strategies to avoid redundant API calls and improve overall efficiency.
  • Debouncing/throttling: Regulate the frequency of API calls to prevent excessive server requests during frequent user interactions.

Implement Smart API Strategies:

// Debounced search to prevent excessive API calls
import { debounce } from 'lodash'

export default {
  data() {
    return {
      searchQuery: '',
      searchResults: [],
      cache: new Map()
    }
  },
  methods: {
    // Debounce prevents API calls on every keystroke
    search: debounce(async function(query) {
      // Implement caching
      if (this.cache.has(query)) {
        this.searchResults = this.cache.get(query)
        return
      }

      try {
        const results = await this.$http.get(`/search?q=${query}`)
        this.cache.set(query, results)
        this.searchResults = results
      } catch (error) {
        this.handleError(error)
      }
    }, 300),

    // Implement pagination for large datasets
    async loadMore() {
      const nextPage = await this.$http.get(`/users?page=${this.currentPage + 1}`)
      this.users.push(...nextPage.data)
      this.currentPage++
    }
  }
}

Strategy 5: Employing Key Monitoring Tools for Performance Insights

  1. Vue DevTools – Real-time component performance analysis
  2. Lighthouse – Comprehensive web performance auditing
  3. Bundle Analyzer – Visualize and optimize bundle size
  4. Vue Performance DevTool – Component-specific render tracking

Conclusion

Optimizing Vue.js performance is less about complex tricks and more about clean, maintainable code and avoiding common mistakes. With best practices like proper conditional rendering, effective state management, and optimized API handling—combined with continuous monitoring using tools such as Vue DevTools and Lighthouse—you can keep your applications fast, scalable, and user-friendly.

Key Takeaways:

  • Use the right rendering directive: Choose between v-if and v-show based on how frequently elements toggle.
  • Adopt proper state management: Avoid deep prop drilling by using global solutions like Pinia or Vuex when state is shared across multiple components.
  • Leverage computed properties: Reserve watchers for side effects, and use computed properties for data derivation to reduce redundant computations.
  • Prevent memory leaks: Always clean up event listeners, timers, WebSocket connections, and third-party subscriptions in lifecycle hooks.
  • Ensure unique keys in loops: Use stable identifiers instead of array indices in v-for to maintain data integrity.
  • Optimize the DOM: Apply v-once for static content and keep-alive for expensive components to reduce unnecessary re-renders.
  • Enhance API efficiency: Implement pagination, caching, and debouncing/throttling to reduce server load and improve responsiveness.
  • Rely on monitoring tools: Utilize Vue DevTools, Lighthouse, and bundle analyzers to continuously monitor, test, and refine performance.

References

  1. Vue.js Official Documentation – Performance
  2. Vue.js Style Guide
  3. Pinia Documentation – State Management
  4. Vue DevTools Documentation
  5. Lodash Debounce Documentation
  6. MDN Performance API
Picture of Loan Nguyen Thi

Loan Nguyen Thi

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top