NashTech Blog

What’s New in Vue 3 Compared to Vue 2 and Why It Matters

Picture of Thanh Nguyen Tat
Thanh Nguyen Tat
Table of Contents

Introduction

Vue.js has earned its place as one of the most beloved frontend frameworks thanks to its simplicity, flexibility, and strong community. For many years, Vue 2 powered a large number of production applications and proved itself to be stable and reliable.

However, the release of Vue 3 marked a significant evolution rather than a simple version upgrade. Vue 3 rethinks how components are structured, how reactivity works, and how applications scale over time. Among all improvements, one change stands above the rest: the introduction of the Composition API.

This article explores the core differences between Vue 2 and Vue 3, with a particular focus on Options API vs Composition API.

1. Options API vs Composition API: The Core Difference

1.1. The Problem Vue 3 Is Solving

In real-world applications, components rarely stay small. A typical component often handles multiple responsibilities such as fetching data, filtering lists, managing user input, and reacting to state changes. While Vue 2’s Options API works well for simple components, it begins to show limitations as complexity increases.

The main issue is how code is organized. Options API organizes code by type (data, methods, computed, watch), not by feature. As a result, logic that belongs together is often scattered across the component.

1.2. Vue 2: How Options API Organizes Code

Let’s consider a product list component that supports searching and tracks how many times the user changes the search keyword.

export default {
  data() {
    return {
      products: [
        { id: 1, name: 'iPhone' },
        { id: 2, name: 'MacBook' },
        { id: 3, name: 'AirPods' }
      ],
      keyword: '',
      searchCount: 0
    }
  },

  computed: {
    filteredProducts() {
      return this.products.filter(product =>
        product.name.toLowerCase().includes(this.keyword.toLowerCase())
      )
    }
  },

  watch: {
    keyword(newValue) {
      console.log('Search keyword changed:', newValue)
      this.searchCount++
    }
  },

  methods: {
    resetSearch() {
      this.keyword = ''
    }
  },

  mounted() {
    console.log('Component mounted')
  }
}

When this component runs, typing into the search input filters the product list in real time. Each keyword change increases the search counter and logs a message to the console. Functionally, everything works as expected.

The problem becomes apparent when reading or maintaining the code. The search logic is split across multiple sections: state in data, filtering logic in computed, side effects in watch, and user actions in methods. When the component grows, understanding or modifying a single feature requires jumping between different parts of the file.

1.3. Vue 3: Composition API Changes the Structure

Vue 3 introduces the Composition API, which allows developers to group code by logical concern rather than by option type. This approach is especially valuable in medium to large applications.

Here is the same feature implemented using Vue 3’s Composition API:

import { ref, computed, watch, onMounted } from 'vue'

export default {
  setup() {
    const products = ref([
      { id: 1, name: 'iPhone' },
      { id: 2, name: 'MacBook' },
      { id: 3, name: 'AirPods' }
    ])

    const keyword = ref('')
    const searchCount = ref(0)

    const filteredProducts = computed(() => {
      return products.value.filter(product =>
        product.name.toLowerCase().includes(keyword.value.toLowerCase())
      )
    })

    watch(keyword, (newValue) => {
      console.log('Search keyword changed:', newValue)
      searchCount.value++
    })

    const resetSearch = () => {
      keyword.value = ''
    }

    onMounted(() => {
      console.log('Component mounted')
    })

    return {
      keyword,
      filteredProducts,
      searchCount,
      resetSearch
    }
  }
}

From a runtime perspective, the behavior is identical to the Vue 2 version. The UI responds the same way, and the user experience does not change. The key improvement lies in how the code is organized. All logic related to searching is placed together in one cohesive block, making the component easier to read and reason about.

1.4. Logic Reusability: Why Composition API Replaces Mixins

One of the biggest challenges in Vue 2 was reusing logic across components. Mixins often introduced hidden dependencies and naming conflicts, making applications harder to debug.

Vue 3 replaces this pattern with Composables, which are simple functions that encapsulate reactive logic.

import { ref, computed, watch } from 'vue'

export function useSearch(items) {
  const keyword = ref('')
  const searchCount = ref(0)

  const filteredItems = computed(() =>
    items.value.filter(item =>
      item.name.toLowerCase().includes(keyword.value.toLowerCase())
    )
  )

  watch(keyword, () => {
    searchCount.value++
  })

  return {
    keyword,
    filteredItems,
    searchCount
  }
}

This composable can be reused across multiple components without side effects. It is explicit, testable, and easy to reason about – something that was difficult to achieve cleanly in Vue 2.

2. Reactive System: From Object.defineProperty to Proxy

One of the most fundamental changes in Vue 3 lies in its reactivity system. While this change is mostly invisible at the API level, it has a significant impact on correctness, performance, and developer experience.

2.1. Vue 2 Reactivity: Limitations of Object.defineProperty

Vue 2 relies on Object.defineProperty to track changes. This approach works by converting existing object properties into getters and setters at initialization time. However, it comes with inherent limitations: Vue cannot detect newly added or removed properties after an object has been made reactive.

export default {
  data() {
    return {
      user: {
        name: 'John'
      }
    }
  },
  mounted() {
    // This change will NOT trigger reactivity
    this.user.age = 30
  }
}

To work around this limitation, developers had to use Vue.set, which often felt unintuitive and easy to forget.

Vue.set(this.user, 'age', 30)

In large applications, these edge cases could easily lead to bugs that were difficult to trace.

2.2. Vue 3 Reactivity: Powered by JavaScript Proxy

Vue 3 replaces this mechanism with JavaScript Proxy, allowing Vue to intercept all operations on an object, including property addition, deletion, and array index updates.

import { reactive } from 'vue'

const user = reactive({
  name: 'John'
})

user.age = 30   // fully reactive
delete user.name // also reactive

Why This Matters

Because Proxy operates at a lower level, Vue 3 can track changes more accurately and consistently. Developers no longer need to think about reactivity edge cases. The result is cleaner code, fewer bugs, and more predictable behavior, especially in complex state objects.

3. Performance Improvements in Vue 3

Performance was a major design goal for Vue 3. Instead of relying solely on runtime optimizations, Vue 3 shifts much of the work to the compiler.

Smarter Virtual DOM Updates

Vue 3’s compiler analyzes templates at build time and marks dynamic parts using patch flags. This allows Vue to update only what actually changes.

<template>
  <div>
    <p>{{ count }}</p>
    <span>Static text</span>
  </div>
</template>

At runtime, Vue 3 knows that only count is dynamic. The <span> node is skipped during updates entirely.

Practical Impact

In real applications, this results in:

  • Faster re-rendering of components
  • Less CPU usage
  • Better performance for large component trees

These gains are especially noticeable in dashboards, tables, and data-heavy interfaces.

4. Built-in Features That Simplify Development

4.1. Cleaner Syntax and Better Optimization: script setup

One of the most developer-friendly features in Vue 3 is <script setup>. It is not just syntactic sugar—it is a compile-time optimization.

Traditional setup() Syntax

export default {
  setup() {
    const count = ref(0)

    const increment = () => {
      count.value++
    }

    return { count, increment }
  }
}

While this works well, it introduces boilerplate that grows with component complexity.

<script setup> Syntax

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

const count = ref(0)

const increment = () => {
  count.value++
}
</script>

Why <script setup> Is Better

With <script setup>:

  • Variables are automatically exposed to the template
  • There is no need for return
  • The compiler generates more efficient code

This syntax encourages simpler, flatter components and aligns well with modern JavaScript practices.

4.2. Fragment Support

In Vue 2, components were required to have a single root element, often leading to unnecessary wrapper <div> elements.

<!-- Vue 2 -->
<template>
  <div>
    <Header />
    <Main />
  </div>
</template>

Vue 3 removes this restriction:

<!-- Vue 3 -->
<template>
  <Header />
  <Main />
</template>

This results in cleaner DOM output and more semantic HTML.

4.3. Teleport: Solving the Modal Problem

Modals and tooltips often need to escape parent containers to avoid CSS issues. Vue 3 introduces Teleport for this exact use case.

<Teleport to="body">
  <div class="modal">
    Modal Content
  </div>
</Teleport>

Despite being defined inside a component, the modal is rendered directly under <body>, avoiding z-index and overflow issues.

4.4. Suspense: Built-in Async Handling

Handling loading states for async components is a common requirement. Vue 3 provides Suspense as a first-class solution.

<Suspense>
  <template #default>
    <AsyncUserProfile />
  </template>
  <template #fallback>
    Loading profile...
  </template>
</Suspense>

This allows developers to declaratively define loading behavior without additional state management.

6. TypeScript as a First-Class Citizen

Vue 3 was written in TypeScript from the ground up, and this decision deeply influences the developer experience.

Stronger Type Inference

import { ref } from 'vue'

const count = ref(0)
// TypeScript automatically infers: Ref<number>

Props and emits are also fully typed:

const props = defineProps<{
  title: string
  count?: number
}>()

Why This Matters

Better typing means:

  • Fewer runtime errors
  • More confident refactoring
  • Improved IDE autocomplete

For teams working on large codebases, this translates directly into higher productivity and code quality.

7. A Modern Ecosystem for Modern Applications

Vue 3 is designed to work seamlessly with a new generation of tools that significantly improve the development workflow.

Vite: Lightning-Fast Development

Vite leverages native ES modules, eliminating the need for bundling during development.

npm create vite@latest my-vue-app
npm run dev

The dev server starts almost instantly, and Hot Module Replacement (HMR) updates are nearly instantaneous.

Pinia: The Successor to Vuex

Pinia provides a simpler and more intuitive state management solution.

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})

Pinia integrates naturally with Composition API and offers excellent TypeScript support.

Conclusion

Vue 3 represents the future of the Vue ecosystem. While Vue 2 laid a strong foundation, Vue 3 refines and extends that foundation with a more scalable architecture, better performance, and a superior developer experience.

The Composition API is the most important change, enabling developers to write cleaner, more maintainable, and more reusable code. For new projects, Vue 3 should be the default choice. For existing Vue 2 applications, migration is a strategic investment that pays off in the long term.

If Options API made Vue approachable, Composition API makes Vue scalable.

Picture of Thanh Nguyen Tat

Thanh Nguyen Tat

Leave a Comment

Suggested Article

Discover more from NashTech Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading