Learn Pinia - Performance & Reactivity Tuning
Episode 13 of 23

Learn Pinia - Performance & Reactivity Tuning

Large state management can become a source of excessive re-renders. This episode covers how to control re-renders with storeToRefs and granular state, using $subscribe with the detached option, and batching many updates with $patch for better performance.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

The bigger the app, the easier it is for a store to become a source of unnecessary re-renders. Every state field that changes can trigger updates in the components that read it — and if every component reads the whole store, one small change ends up in a mass re-render.

Episode 13 covers Pinia reactivity tuning: controlling re-renders with storeToRefs and granular access, using $subscribe with the detached option, and batching updates with $patch. This isn't micro-optimization — these are patterns that keep the app from slowing down as stores grow.

storeToRefs vs Direct Access

Notice how the way you read affects re-renders:

JSGranular access versus the whole store
// Re-renders when the used fields change
const { count, double } = storeToRefs(store)
 
// Wider potential re-renders: the template reads many properties
const store = useCounterStore()

const { count, double } = storeToRefs(store) takes only the fields you need, so Vue only tracks those dependencies. Conversely, using the full store and reading many properties can create more dependencies — not wrong, but less precise.

Granular State

Don't put all your app's data in one big store. Split it into small stores and specific state:

JSGranular state per domain
// Bad: one state holds many domains
state: () => ({ user: {}, cart: [], ui: { modal: null } })
 
// Better: separate per store
useAuthStore()  // user
useCartStore()  // cart
useUiStore()    // modal, sidebar

With separate stores, a component that only needs the cart won't be affected when ui changes. This principle reduces re-renders and makes dependency tracking sharper.

$subscribe with the detached Option

By default, $subscribe stops when the component that registered it is unmounted. For subscribers that need to live longer, pass { detached: true }:

JSDetached subscribe in a component
store.$subscribe(
  (_mutation, state) => {
    saveToStorage(state)
  },
  { detached: true },
)

{ detached: true } keeps the subscriber active even after the component is unmounted. Use it for long-running tasks like autosave. Otherwise, leave the default so subscribers are cleaned up along with the component and don't leak.

Batching Updates with $patch

Several sequential assignments trigger many updates. $patch merges them into one:

JSMany updates in one patch
// Three separate updates
store.firstName = 'Arman'
store.lastName = 'Dwi'
store.age = 26
 
// One patch
store.$patch({
  firstName: 'Arman',
  lastName: 'Dwi',
  age: 26,
})

store.$patch({ ... }) groups changes so subscribers and DevTools only record a single mutation. This matters for more efficient storage sync and observability.

$subscribe vs watch

Pinia provides two ways to monitor changes: the store's $subscribe and Vue's watch. When to use which?

  • $subscribe only detects state changes after they've been successfully mutated, and { flush: 'sync' } can be set so the callback runs before the component re-renders. Suited for external sync like storage or analytics.
  • watch(store.$state, ...) uses Vue's regular watch mechanism, supports deep and immediate, and is easier to stop if the source is your own composable.
JSWatching state with watch
watch(
  () => store.$state.cart,
  (cart) => {
    console.log('cart changed', cart)
  },
  { deep: true },
)

For simple cases, choose the one that uses the least external API: watch when you're already inside a component and need standard dependency tracking, $subscribe when you work with the store directly — for example in a plugin or when building a utility that lives outside components.

Warning

Optimization doesn't start with micro-patches. Start from the data structure: small stores per domain and granular access. $patch and detached are refinements once the foundations are correct.

Closing

Episode 13 equips you with performance patterns you can apply right away. You can now control re-renders with storeToRefs and granular state, manage subscriber lifetimes with detached, and batch updates with $patch.

Key takeaways:

  • storeToRefs limits dependency tracking to the fields you use.
  • Split large state into per-domain stores.
  • { detached: true } makes $subscribe live longer.
  • $patch merges many updates into a single mutation.
  • Optimization starts from store structure, not micro-patches.
  • Measure the impact with DevTools before chasing optimizations.

In the next episode, episode 14, we'll discuss async server state and advanced integration — Pinia Colada for fetching and caching server state, combining stores with Vue Router, and integration with external composables. This is Pinia's bridge to modern application architecture.

Learn Pinia - Performance & Reactivity Tuning | Learning Pinia