Vue Performance — v-memo, shallowRef, markRaw, defineAsyncComponent
TL;DR
Vue 3 is fast by default — the reactivity system tracks at the per-property level, so re-renders are narrowly scoped. The senior toolbox for tuning: v-memo (skip child re-render if deps unchanged), shallowRef/shallowReactive (opt out of deep tracking), markRaw (opt out entirely), defineAsyncComponent (code splitting), KeepAlive (cache off-screen components), and virtualization for long lists. Profile with the Vue DevTools Performance panel before optimizing.
Interview Q&A
Q: When does a Vue component re-render?
A: When a reactive value it reads during render changes. Vue’s per-property tracking means a component only re-renders if a value it actually used was updated — not because a sibling’s state changed, not because a parent re-rendered (with caveats — see below).
Compared to React (which re-renders by default and you optimize with memo), Vue is the inverse: components re-render only when their deps change. This is one of the framework’s biggest wins.
Q: Does a parent re-render cause child re-renders?
A: No in Vue 3 — the child re-renders only if its props or its own reactive deps change. This is different from React, where parent re-render triggers child re-render unless React.memo wraps the child.
But: if a parent’s re-render passes new prop values (even if structurally equal), the child does re-render. That’s where v-memo and stable prop references matter.
Q: v-memo — what’s it for and what does it do?
A: Skip re-rendering a subtree (and its children) when the listed dependency list is unchanged between renders.
<div v-memo="[item.id, item.updatedAt]">
<ExpensiveItem :item="item" />
</div>
Vue compares the array on each render; if all entries are === to last render, it skips the diff/render for this whole subtree. Best on:
- Expensive list items that only change when a specific field changes.
- Static-ish content inside dynamic parents.
Empty deps v-memo="[]" = “never re-render after first” (use with caution; rare).
Don’t reach for v-memo by default. It’s the Vue equivalent of useMemo — micro-optimization, only after profiling shows the re-render is a real cost.
Q: shallowRef / shallowReactive for perf — when?
A: When you have a large object/array and you only care about reference changes, not deep mutations.
// Bad — deep reactive on a 10k-item array is expensive
const items = ref<Item[]>([]);
// Good — shallow; you're replacing the whole array on update
const items = shallowRef<Item[]>([]);
items.value = await fetchItems(); // triggers
items.value.push(newItem); // doesn't trigger — but you don't care, you'll replace
The cost of reactive / ref is the Proxy traversal — small for a few keys, real for big nested structures. shallowRef saves that cost.
See 03_ref_vs_reactive.md for the API details.
Q: markRaw — when?
A: Tell Vue never reactify this object. Useful when:
- It’s a heavy class instance (
ml.Model,THREE.Scene,EditorView) that shouldn’t be Proxy’d. - Proxying breaks the object’s internal contracts (some libraries check identity).
- You’re storing it in a reactive context but it’s never going to change.
const state = reactive({
user: { name: "Ada" },
editor: markRaw(new EditorView()), // big object, leave alone
});
The user object is reactive; the editor is not — and Vue won’t deep-walk it.
Q: defineAsyncComponent — code splitting.
A: Lazy-load a component on first render:
const HeavyChart = defineAsyncComponent(() => import("./HeavyChart.vue"));
Bundler splits the chunk; first render fetches it. With <Suspense> parent or defineAsyncComponent({ loadingComponent, errorComponent }) for loading states.
Pair with Vue Router’s route-level lazy loading:
const routes = [
{ path: "/dashboard", component: () => import("./Dashboard.vue") },
];
Each route gets its own chunk; users only download what they navigate to.
Q: <KeepAlive> — when to cache?
A: Keep a component alive (preserve state, skip unmount/remount) when it’s hidden:
<KeepAlive :include="['UserList', 'PostList']" :max="10">
<component :is="currentView" />
</KeepAlive>
The component receives onActivated/onDeactivated instead of onUnmounted/onMounted. Use for:
- Tabs that have expensive state (scroll position, form input).
- Navigation patterns where users return to a screen (back button → restored state).
Memory cost: cached components keep their state in memory. :max bounds the cache (LRU eviction).
Q: Virtualizing long lists.
A: Above ~200-500 items, render only what’s visible. Libraries:
vue-virtual-scroller— proven, supports dynamic heights.@tanstack/vue-virtual— TanStack’s primitive (same library as react-virtual).
<RecycleScroller
:items="items"
:item-size="60"
key-field="id"
v-slot="{ item }"
>
<ItemRow :item="item" />
</RecycleScroller>
Variable heights need measurement; static heights are cheaper. Pair with IntersectionObserver for infinite-load.
Q: Bundle size — what knobs?
A:
- Tree-shake unused Vue features — Vue’s tree-shakeable; using
import { ref } from "vue"only ships what you use. - Optional features (Options API, Compositions DevTools hooks) can be dropped in prod via build flags (
__VUE_OPTIONS_API__: false). vite-plugin-vue-devtoolsis dev-only; ensure it’s not bundled for prod.- CSS — scoped vs unscoped, atomic vs component CSS, Tailwind purge.
- Async imports for route-level splitting, conditional features.
See ../09_build_tools/ for build-tool depth.
Q: Profiling.
A: Vue DevTools → Performance tab — record interactions, see which components re-rendered and why (onRenderTriggered gives the dep that caused it).
For deeper Chrome DevTools profiling:
- Performance tab — flamegraphs, identify long tasks.
- Coverage tab — unused JS/CSS.
- Lighthouse — Core Web Vitals.
Q: Common Vue perf bugs.
A:
- Reactive object too deep — every nested property is proxied; large data → use
shallowRef/shallowReactive/markRaw. - Inline functions as props —
:onClick="() => doX()"creates a new function each render; the child sees a “changed” prop. Hoist or use stablemethods. v-forwithkey="index"— same as React; moving items confuses Vue’s diffing. Use a stable ID.- Computed cache invalidating on each render — a computed that reads
Date.now()or non-reactive data won’t cache effectively. - Watching deep objects when you don’t need to —
deep: truewalks the structure every change. Watch the specific path. - Hydration mismatches (SSR) —
consoleerrors in dev, expensive re-renders in prod. Guard browser-only code.
Gotchas / edge cases
v-memodeps array must be a true array of primitive-comparable values; objects compare by reference.shallowRefwith mutations doesn’t trigger — common “why isn’t this updating?” bug.<KeepAlive>+ components with side effects on mount — those don’t re-run on activation; useonActivatedfor re-init.defineAsyncComponentrace — if the user navigates away before load completes, Vue handles it; if your loader has side effects, guard them.v-once— render the element/subtree once and never update. Cheaper thanv-memowhen you’re sure nothing should change.
What a senior is expected to say
- “Vue 3 re-renders are scoped per component based on what reactive values were read during render — much narrower than React’s default. I don’t reach for memoization unless profiling shows a problem.”
- “
v-memofor the rare case where a subtree is expensive and only changes on specific keys. Don’t apply broadly.” - “
shallowReffor big arrays/objects you only replace;markRawfor heavy class instances you don’t want proxied;shallowReactivefor top-level reactive with nested values you don’t care about.” - “Route-level code splitting via
() => import()in Vue Router;defineAsyncComponentfor component-level.<KeepAlive>for tabs/back-nav where state preservation matters.” - “Virtualize lists above ~200 items. Stable keys in
v-for. Hoist inline handlers to avoid prop-identity churn.” - “Profile with Vue DevTools Performance +
onRenderTriggeredto find what’s causing re-renders.”
Cross-references
- Reactivity internals (cost model): 02_reactivity_internals.md
refvsreactivevsshallow*API: 03_ref_vs_reactive.md- General performance, Web Vitals, virtualization: ../15_performance/
Further reading
- Vue docs — Performance: https://vuejs.org/guide/best-practices/performance.html
- Vue DevTools: https://devtools.vuejs.org/
vue-virtual-scroller: https://github.com/Akryum/vue-virtual-scroller