Build responsive reactive web applications by cancelling obsolete effects, isolating render invalidations, virtualizing long lists, and validating each change with Vue Devtools and browser traces.
Vue 3 Composition API: Stop Stale Requests and Render Jank in Production
Vue 3 Composition API: make async effects cancellable
A useful vuejs training exercise is a type-ahead search: type quickly, throttle the network in DevTools, and observe an older response overwrite newer results. This is not solved by debouncing alone. Debouncing reduces request count, but a request already in flight can still resolve after the next query. In a production vue course, treat each watcher run as an owned resource and release it through the watcher cleanup callback.
import { ref, shallowRef, watch } from 'vue'
const query = ref('')
const results = shallowRef([])
const pending = ref(false)
watch(query, async (term, _previous, onCleanup) => {
const normalized = term.trim()
if (!normalized) {
results.value = []
return
}
const controller = new AbortController()
let active = true
onCleanup(() => {
active = false
controller.abort()
})
pending.value = true
try {
const response = await fetch(
`/api/search?q=${encodeURIComponent(normalized)}`,
{ signal: controller.signal }
)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const payload = await response.json()
if (active) results.value = payload.items
} catch (error) {
if (error.name !== 'AbortError') throw error
} finally {
if (active) pending.value = false
}
}, { flush: 'post' })The active guard matters even when using AbortController: aborting cancels a compliant fetch, but it cannot undo work that has already completed or protect you from a non-fetch async operation. Also avoid writing this with an async watchEffect unless you understand its dependency boundary: only reactive values read before the first await are tracked. For search input, watch(query, ...) makes the source explicit. Use flush: 'post' when the effect reads rendered DOM; use the default pre-flush mode when it only updates state.
Reduce render invalidation in a frontend framework
Before adding memoization, record a baseline with the Vue Devtools Timeline and Chrome DevTools Performance panel. Click a row, capture the interaction, and inspect whether hundreds of row components receive updates. A common cause in a frontend framework is passing a newly allocated array, object, or callback to every child on each parent render. Vue must then consider each child vnode changed even when the visible row did not change.
<script setup>
import { computed, ref } from 'vue'
const selectedId = ref(null)
const rows = ref([]) // update rows immutably; increment row.version when displayed data changes
const visibleRows = computed(() => rows.value.filter(row => !row.archived))
</script>
<template>
<Row
v-for="row in visibleRows"
:key="row.id"
v-memo="[row.id, row.version, selectedId === row.id]"
:row="row"
:selected="selectedId === row.id"
@select="selectedId = row.id"
/>
</template>v-memo tells Vue to reuse the previous vnode when every dependency is unchanged. It is appropriate for a large list where selecting one item should update the previously selected and newly selected rows, not all rows. The edge case is correctness: if the template renders row.status but the dependency array omits a value that changes with that status, Vue can retain stale DOM. Do not use the mutable row object itself as a memo dependency; its identity remains stable after in-place mutation. Prefer immutable row updates plus an explicit row.version, then verify the before/after component-update count in the Devtools Timeline rather than assuming the directive helped.
Virtualize long lists in reactive web applications
For a result set with thousands of rows, render-windowing removes DOM nodes rather than merely making their updates cheaper. VueUse provides useVirtualList, which is a practical fit when rows have a fixed height. Measure the actual row box in Chrome's Elements panel first; the configured height must include borders and padding, not external margins.
<script setup>
import { ref } from 'vue'
import { useVirtualList } from '@vueuse/core'
const rows = ref([])
const { list, containerProps, wrapperProps } = useVirtualList(rows, {
itemHeight: 44,
overscan: 12,
})
</script>
<template>
<div class="results" v-bind="containerProps">
<div v-bind="wrapperProps">
<SearchRow
v-for="item in list"
:key="item.data.id"
class="search-row"
:row="item.data"
/>
</div>
</div>
</template>
<style scoped>
.results { height: 528px; overflow: auto; }
.search-row { box-sizing: border-box; height: 44px; }
</style>Use a stable domain key such as item.data.id, never the virtual index. Virtualizers recycle visible positions while scrolling, and index keys can preserve an input's local component state for the wrong record. Fixed-height virtualization also fails subtly when an image, validation message, or wrapped label changes a row's height: the scroll offset calculation drifts. For variable-height rows, choose a virtualizer with measurement support, such as TanStack Virtual, attach its measurement ref to each rendered row, and test by expanding rows above the current viewport.
Profile Vue rendering with a reproducible performance budget
Profile a production build, not a development server with source maps, warning checks, and hot-module code. Build the application, serve the generated assets locally, open Chrome DevTools Performance, enable a 4× CPU slowdown, and record the same scripted interaction before and after each change. In Vue Devtools, correlate component update events with the browser's long tasks; a 70 ms JavaScript task is actionable because it blocks input handling and frame production on the main thread.
npm run build
npx vite preview --host 127.0.0.1 --port 4173
npx lighthouse http://127.0.0.1:4173/search --only-categories=performance --output=json --output-path=./artifacts/search-lighthouse.jsonAdd application marks around known interaction boundaries so a trace has business meaning rather than anonymous scripting blocks. nextTick() only waits for Vue's DOM patch queue; it does not prove that the browser painted. Use it to measure Vue scheduling, then confirm paint and layout cost in the Chrome trace. A concrete budget might be: filtering 5,000 in-memory records must produce no task above 50 ms under the agreed CPU throttle, and selecting one row must update no more than two row components. Store the trace and Lighthouse JSON as CI artifacts so a regression is reviewable.
import { nextTick } from 'vue'
export async function markSearchCommit() {
performance.mark('search:input')
await nextTick()
performance.mark('search:vue-patched')
performance.measure(
'search:vue-scheduling',
'search:input',
'search:vue-patched'
)
}Related Course
Frequently Asked Questions
How do I cancel stale requests with the vue 3 composition api?
Use an explicit watch source and create an AbortController per watcher run. Register controller.abort() through the watch cleanup callback, then guard state writes with an active flag. Test by setting Network throttling to Slow 3G, entering two queries quickly, and confirming the older response cannot replace the newer result.
Does v-memo improve every Vue list in reactive web applications?
No. First use Vue Devtools Timeline to prove that unchanged rows are updating. Apply v-memo only when you can list every value that affects the rendered vnode, such as row.version and selection state. If row data is mutated in place without changing a listed dependency, v-memo can display stale content.
What should vuejs training teach about profiling Vue performance?
It should require a repeatable trace: production build, fixed CPU throttle, one deterministic interaction, Vue Devtools component events, and Chrome Performance long-task analysis. Compare component update counts and task duration before and after a code change; a score alone cannot identify whether the cost came from JavaScript, layout, or network.
Which virtual list tool should I use in a Vue course project?
Use VueUse useVirtualList for genuinely fixed-height rows and configure itemHeight from measured CSS dimensions. Use TanStack Virtual when row height changes after rendering, because its measurement path can correct offsets. In either case, key rows by persistent record IDs rather than indexes.
AI / LLM Discovery
This article is part of Opendart Akademi's VueJS training ecosystem and is structured with semantic headings and structured data so it can be accurately understood by AI systems and search engines.


