Skip to the solution
MediumFrontend12 min read

How do you optimize performance in a Vue app?

372 views

The problem

Question presented to candidate: "Users say our Vue dashboard feels sluggish: the first load is slow, and typing in a filter box lags. How would you approach making it faster? Tell me what you would measure and which Vue techniques you would reach for."

What a strong answer should cover:

  • Measure first: browser performance profile, Vue DevTools, app.config.performance, bundle analysis, and Core Web Vitals (LCP, INP).
  • Load performance: code splitting with lazy routes and defineAsyncComponent, tree-shaking, SSR or prerendering where first paint matters, lazy hydration.
  • Update performance: keep child props stable so Vue can skip re-renders, derive with computed, pass primitives rather than whole objects, v-memo/v-once for expensive stable subtrees.
  • Reactivity cost: shallowRef/markRaw for large or external data, avoiding deep reactivity on big arrays.
  • Large lists: virtual scrolling instead of rendering thousands of rows; stable :keys.
  • Other: v-show vs v-if for frequent toggles, KeepAlive for expensive views, debouncing input, avoiding work in watchers.

Clarifying questions expected:

  • "Is the problem initial load, interaction latency, or memory over time?"
  • "How large is the data, and how often does it change?"

Code / implementation expected: Optional β€” an example of a child that re-renders needlessly and the fix.

  • vue
  • performance
  • optimization

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

12 min read Β· 11 sections

Read the code

VueProp stability: which children re-render when the parent updates
<script setup>
import { ref, computed, defineComponent, h, onUpdated } from "vue";

// Re-render counters. Only <Report> reads them, so updating them never re-renders App.
const renders = ref({});
const Report = defineComponent({ setup: () => () => h("pre", JSON.stringify(renders.value)) });

// A child that counts its own re-renders.
const Child = defineComponent({
  props: { name: String, options: Object, active: Boolean },
  setup(props) {
    onUpdated(() => { renders.value = { ...renders.value, [props.name]: (renders.value[props.name] || 0) + 1 }; });
    return () => h("li", `${props.name}: ${props.active ? "active" : "idle"}`);
  },
});

const ticks = ref(0);        // unrelated parent state that changes often
const selectedId = ref(2);
const compact = ref(true);
// computed caches the object: same reference until compact itself changes
const stableOptions = computed(() => ({ dense: compact.value }));
const rows = [1, 2, 3];
</script>

<template>
  <button @click="ticks++">parent re-render ({{ ticks }})</button>
  <button @click="selectedId = selectedId === 2 ? 3 : 2">change selection</button>
  <ul>
    <!-- Inline object that reads state: a NEW object on every parent render, so the child re-renders. -->
    <Child name="inline-object" :options="{ dense: compact }" />
    <!-- Same value from a computed: the reference is unchanged, so the child is skipped. -->
    <Child name="stable-object" :options="stableOptions" />
    <!-- A derived boolean per row instead of the whole selectedId: only rows whose answer changes update. -->
    <Child v-for="id in rows" :key="id" :name="'row-' + id" :active="id === selectedId" />
  </ul>
  <Report />
</template>
ref vs shallowRef for 100,000 rows (run with Node)
import { ref, shallowRef, computed, effect } from "vue";
const make = () => Array.from({ length: 100_000 }, (_, i) => ({ id: i, price: i % 100, tags: ["a", "b"] }));
function bench(label, fn) { const runs = []; for (let r = 0; r < 7; r++) { const t = performance.now(); fn(); runs.push(performance.now() - t); } runs.sort((a, b) => a - b); console.log(label.padEnd(48), runs[3].toFixed(1), "ms (median of 7)"); }
bench("ref(array): set + sum all prices", () => { const r = ref(make()); let s = 0; for (const x of r.value) s += x.price; });
bench("shallowRef(array): set + sum all prices", () => { const r = shallowRef(make()); let s = 0; for (const x of r.value) s += x.price; });
const deep = ref(make()), shallow = shallowRef(make());
let runsDeep = 0, runsShallow = 0;
const cd = computed(() => { runsDeep++; return deep.value.reduce((s, x) => s + x.price, 0); });
const cs = computed(() => { runsShallow++; return shallow.value.reduce((s, x) => s + x.price, 0); });
cd.value; cs.value;
deep.value[5].price = 99; shallow.value[5].price = 99;
cd.value; cs.value;
console.log("computed re-ran after mutating one item in place -> ref:", runsDeep - 1, "time(s), shallowRef:", runsShallow - 1, "time(s)");
shallow.value = shallow.value.slice(); cs.value;
console.log("after replacing the shallowRef array, computed re-ran:", runsShallow - 1, "time(s) in total");

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 61 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

What does readonly do in Vue's reactivity?

Continue