Frontend / Vue / 13_performance.md

Vue Performance — v-memo, shallowRef, markRaw, defineAsyncComponent

Updated 4 interview angles 6 min read source
On this page15
  • When does a Vue component re-render?
  • Does a parent re-render cause child re-renders?
  • v-memo — what’s it for and what does it do?
  • shallowRef / shallowReactive for perf — when?
  • markRaw — when?
  • defineAsyncComponent — code splitting.
  • — when to cache?
  • Virtualizing long lists.
  • Bundle size — what knobs?
  • Profiling.
  • Common Vue perf bugs.
  • Gotchas
  • Related
  • Further reading
  • Interview angle
  • Vue Performance — v-memo, shallowRef, markRaw, defineAsyncComponent

    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.

    When does a Vue component re-render?

    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.

    Does a parent re-render cause child re-renders?

    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.

    v-memo — what’s it for and what does it do?

    Skip re-rendering a subtree (and its children) when the listed dependency list is unchanged between renders.

    vue
    <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.

    shallowRef / shallowReactive for perf — when?

    When you have a large object/array and you only care about reference changes, not deep mutations.

    ts
    // 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 ref vs reactive vs shallowRef / shallowReactive for the API details.

    markRaw — when?

    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.
    ts
    const state = reactive({
      user: { name: "Ada" },
      // big object, leave alone
      editor: markRaw(new EditorView()),
    });

    The user object is reactive; the editor is not — and Vue won’t deep-walk it.

    defineAsyncComponent — code splitting.

    Lazy-load a component on first render:

    ts
    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:

    ts
    const routes = [
      { path: "/dashboard", component: () => import("./Dashboard.vue") },
    ];

    Each route gets its own chunk; users only download what they navigate to.

    <KeepAlive> — when to cache?

    Keep a component alive (preserve state, skip unmount/remount) when it’s hidden:

    vue
    <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).

    Virtualizing long lists.

    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).
    vue
    <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.

    Bundle size — what knobs?

    • 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-devtools is 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 Build Tools — Senior Interview Prep for build-tool depth.

    Profiling.

    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.

    Common Vue perf bugs.

    • 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 stable methods.
    • v-for with key="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: true walks the structure every change. Watch the specific path.
    • Hydration mismatches (SSR) — console errors in dev, expensive re-renders in prod. Guard browser-only code.

    Gotchas

    • v-memo deps array must be a true array of primitive-comparable values; objects compare by reference.
    • shallowRef with 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; use onActivated for re-init.
    • defineAsyncComponent race — 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 than v-memo when you’re sure nothing should change.

    Further reading

    Interview angle 4

    • “How does Vue’s re-render behaviour differ from React’s?” - it is scoped by dependency rather than by tree position. A component re-renders only if a reactive value it actually read during render changed, so a parent updating does not re-render children by default. That is why Vue needs far less manual memoization than React historically did.
      • Follow-up: “So when do you reach for v-memo?” - rarely, and only when profiling shows a specific expensive subtree that changes on a few known keys. Applying it broadly adds comparison cost and staleness bugs for no measured benefit.
    • “Which reactivity escape hatches matter for performance?” - shallowRef for a large array or object you replace rather than mutate, markRaw for a heavy class instance that should never be proxied, and shallowReactive when only the top level needs tracking. All three avoid deep proxying that costs time and buys nothing.
    • “What are the standard list wins?” - virtualize past roughly 200 rows, stable keys in v-for so patching can match nodes, and hoisted handlers so every row does not get a new function identity each render.
    • “How do you find out what is re-rendering?” - Vue DevTools’ performance panel, plus onRenderTriggered, which tells you which reactive dependency caused the render. That is more direct than React’s equivalent and usually names the bug immediately.