DEV Community

Cover image for Why Your Vue App Is Reactive Too Much (and How to Fix It)
Jakub Andrzejewski
Jakub Andrzejewski

Posted on

Why Your Vue App Is Reactive Too Much (and How to Fix It)

Vue’s reactivity system is one of its greatest strengths. It allows you to build dynamic, expressive UIs with very little effort. But like many powerful tools, it’s easy to overuse.

In larger Vue applications, excessive reactivity can quietly become a performance bottleneck. Components re-render more often than expected, watchers fire constantly, computed properties recompute too frequently, and memory usage slowly grows over time.

In many cases, the app works, but it’s doing far more reactive work than necessary.

In this article, we’ll explore:

  • What “over-reactivity” means in Vue
  • How excessive reactivity affects performance and maintainability
  • Common pitfalls with computed, watch, and deep reactive objects
  • How tools like shallowRef and markRaw help you regain control
  • Practical strategies to make your Vue apps faster and calmer

Enjoy!

🤔 What Does “Too Much Reactivity” Mean in Vue?

Vue tracks dependencies automatically. Every reactive object, ref, computed property, and watcher participates in a dependency graph that Vue constantly evaluates.

Problems arise when:

  • Large objects are made fully reactive without need
  • Expensive computations are tracked too broadly
  • Watchers observe more data than necessary
  • Third-party objects are accidentally wrapped in reactivity
  • Reactive state is created eagerly instead of lazily

The result is often:

  • Unnecessary component re-renders
  • Slower updates under load
  • Increased memory usage
  • Hard-to-reason-about data flow
  • Performance issues that are difficult to profile

Reactivity itself is not the problem. Uncontrolled reactivity is.

🟢 Common Reactivity Pitfalls (and How to Fix Them)

Using Deep Reactivity for Large Objects

By default, ref() and reactive() make objects deeply reactive. This means Vue tracks every nested property, even if you only care about the top-level value.

This is especially problematic for:

  • API responses
  • Large configuration objects
  • Complex data structures
  • Immutable datasets

Solution: shallowRef

import { shallowRef } from 'vue'

const data = shallowRef(largeApiResponse)
Enter fullscreen mode Exit fullscreen mode

With shallowRef, Vue tracks only .value, not every nested property. You still get reactivity where it matters, without the overhead.

Making Non-Reactive Objects Reactive by Accident

Libraries, class instances, DOM nodes, or complex external objects often should not be reactive, but Vue doesn’t know that unless you tell it.

Examples include:

  • Charting libraries
  • Map instances
  • WebSocket clients
  • Three.js scenes
  • Complex SDK objects

Solution: markRaw

import { markRaw } from 'vue'

const chart = markRaw(createChart())
Enter fullscreen mode Exit fullscreen mode

markRaw tells Vue: “Do not track this object.”
This prevents unnecessary proxy wrapping and avoids subtle bugs.

Computed Properties That Recompute Too Often

Computed properties are cached, but only as long as their dependencies stay stable.

Common mistakes:

  • Depending on large reactive objects
  • Performing expensive logic inside computed
  • Using computed where a simple method would suffice

Example pitfall:

const filteredItems = computed(() =>
  items.value.filter(item => item.active)
)
Enter fullscreen mode Exit fullscreen mode

If items is a large, deeply reactive array, this recomputes more often than expected.

Fixes include:

  • Reducing dependency scope
  • Splitting state into smaller refs
  • Using shallowRef for large collections
  • Memoizing expensive logic outside reactivity

Watchers Gone Wild

Watchers are powerful, but they are often overused.

Common issues:

  • Watching entire objects instead of specific properties
  • Using deep: true unnecessarily
  • Chaining watchers that trigger each other
  • Using watchers where computed or events would be clearer

Example problematic watcher:

watch(state, () => {
  syncSomething()
}, { deep: true })
Enter fullscreen mode Exit fullscreen mode

This runs far more often than intended.

Better approaches:

  • Watch specific refs instead of whole objects
  • Avoid deep unless absolutely necessary
  • Prefer computed for derived state
  • Use explicit events for side effects

Watchers should be the exception, not the default.

Eager Reactivity Everywhere

Not everything needs to be reactive immediately.

Creating reactive state too early can:

  • Increase startup cost
  • Trigger unnecessary tracking
  • Complicate lifecycle management

Consider lazy initialization or local refs inside components instead of global reactive stores.

Less reactive state often leads to simpler mental models and better performance.

📖 Learn more

If you want to master Vue’s reactivity system and learn how to build performant, maintainable applications, check out VueSchool by clicking this link or the image below:

Vue School Link

VueSchool covers reactivity internals, performance patterns, and real-world Vue architecture techniques.

🧪 Advance skills

A certification boosts your skills, builds credibility, and opens doors to new opportunities.

Check out Certificates.dev by clicking this link or the image below:

Certificates.dev Link

Get certified in Vue.js, Nuxt, JavaScript, React, Angular, and more.

✅ Summary

Vue’s reactivity is incredibly powerful, but more is not always better.
By being intentional about what you make reactive, you can build apps that are faster, easier to reason about, and more scalable over time.

Take care!
And happy coding as always 🖥️

Top comments (0)