Vue 3 Computed Properties vs Methods: When Caching Actually Matters
Vue 3 computed properties cache results and re-run only when tracked dependencies change; template method calls re-run on every render. A worked example shows when that difference actually matters.
01 Jul 2025, 11:41 UTC

The re-render that re-runs everything
Here's a scenario that bites Vue developers sooner or later: a component filters a large list, and the template calls a method like filteredRows(). Everything feels fine in development. Then someone adds an unrelated reactive value — a clock, a typing indicator, a live counter — and the whole component starts re-filtering thousands of rows every time that value ticks. Nothing is broken, exactly. It's just doing the same expensive work over and over for no reason.
The fix is usually a computed property. But the real skill is knowing why that fixes it, because the difference between computed properties and methods in Vue 3 isn't syntax — it's caching and dependency tracking.
What a computed property actually does
A computed property wraps a getter function and caches its result. During evaluation, Vue's reactivity system records every ref or reactive property the getter reads. The getter only re-runs when one of those tracked dependencies changes. Any other state change in the component — even one that triggers a full re-render — leaves the cached value alone.
A method has no such contract. Every time the component re-renders, the template expression is evaluated again, which means the method body executes again. Vue doesn't know what the method read, so it can't know whether the result would be different.
Two consequences follow from this:
- Dependencies must be read synchronously. If your getter awaits something before reading a ref, that ref is never tracked. Computed properties are for synchronous derivations only.
- Conditional reads mean conditional tracking. If a dependency is only read inside an
ifbranch, it's only tracked when that branch executes. The dependency set can change between evaluations — usually correct, occasionally surprising.
A worked example
Assume Vue 3.4+ with <script setup>. Here's a searchable table where the expensive work is filtering and sorting:
<script setup>
import { ref, computed } from 'vue'
const query = ref('')
const rows = ref([/* ... thousands of rows ... */])
const lastUpdated = ref(new Date())
// Re-runs only when `query` or `rows` changes.
const visibleRows = computed(() => {
console.log('filtering...')
const q = query.value.trim().toLowerCase()
return rows.value
.filter(r => r.name.toLowerCase().includes(q))
.sort((a, b) => a.name.localeCompare(b.name))
})
// Re-runs on EVERY re-render, including when `lastUpdated` ticks.
function visibleRowsMethod() {
console.log('filtering (method)...')
const q = query.value.trim().toLowerCase()
return rows.value
.filter(r => r.name.toLowerCase().includes(q))
.sort((a, b) => a.name.localeCompare(b.name))
}
</script>To see the difference yourself, render both in a template and add a button that updates lastUpdated. Each click re-renders the component: the method logs "filtering (method)..." every time, while the computed stays silent until you edit the query or the rows. This is a quick, honest verification you can run in the Vue SFC playground — no test harness needed.
Note the unwrapping rule that trips up newcomers: inside <script setup>, you read the computed with visibleRows.value; in the template, you write just visibleRows. Templates unwrap top-level refs and computed values automatically.
Writable computed: two-way derived state
Computed properties aren't limited to read-only derivations. The get/set object form is the idiomatic way to build derived state that flows both directions — for example, a "select all" checkbox over a filtered list:
const allSelected = computed({
get: () => visibleRows.value.length > 0
&& visibleRows.value.every(r => r.selected),
set: (val) => {
visibleRows.value.forEach(r => { r.selected = val })
}
})Bound with v-model="allSelected", this reads as derived state and writes back into the source data. The mutation lives in the setter, which is the correct place for it — never in the getter.
Trade-offs and limits
Computed properties are not free, and they're not always the right call:
- Side effects are forbidden. A getter must be pure. Mutating state inside it can trigger dev-mode warnings and produces results that aren't guaranteed to be consistent. If you need to react to a change with a side effect (logging, fetching, syncing), use
watchorwatchEffect. - No async work. A computed can't await. Async derived state needs a different tool (a watcher that writes into a ref, or a data-fetching composable).
- Caching has a cost. Every computed adds a reactive effect and memory overhead. For a trivial expression like
firstName + ' ' + lastName, the performance difference versus a method is negligible — pick whichever reads better. - Arguments change the equation. Methods can take parameters (
formatPrice(row.total)); computed properties can't. If you need per-item derivation inside av-for, a method (or a small child component with its own computed) is often cleaner than a computed that returns a closure.
The decision rule I use: if the value is derived from reactive state, read more than once per render, or expensive to compute, make it a computed. If it takes arguments, performs an action, or is trivial, a method is fine.
Try it before you trust it
The console-log experiment above takes five minutes and makes the caching behavior concrete in a way documentation can't. Build the smallest version — one computed, one method, one unrelated ticking ref — and watch which one re-runs. Once you've seen a method re-execute because an unrelated counter changed, you'll reach for computed with confidence instead of habit.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.