Vue rendering & performance
How a Vue template becomes a render function
Every template compiles to a render function that returns a vnode tree, where a vnode is a plain object like { type: 'div', props: {...}, children: [...] }. The SFC compiler does this at build time. The runtime calls that function as a reactive effect, tracking the reactive values read during render. When one changes, the effect re-runs, produces a new vnode tree, and the renderer diffs it against the old one and touches only the DOM that differs.
Two consequences show up day to day. A component re-renders when its own reactive dependencies change. A parent re-rendering does not by itself re-render the child: Vue checks each child and skips one whose props and slots are unchanged. (React re-renders children by default and you opt out with memoization; Vue is the other way around.) Hand-written h() render functions give up the compiler's shortcuts, so prefer templates.
v-if vs v-show, and which is cheaper
They are not interchangeable, and which one is cheaper depends on how often you toggle.
v-if creates and destroys the block (its elements, listeners, and child components) each time the condition flips, and it is lazy: if the condition is false on first render, nothing gets built. v-show always renders the element and toggles only the CSS display property.
Wrong "v-if is always cheaper because nothing is in the DOM while it's hidden." That ignores toggle cost. A panel toggled dozens of times per session pays a full create and teardown on every flip under v-if.
Right reach for v-show when something toggles frequently, because the build happens once and each toggle is a CSS flip; reach for v-if when the condition rarely changes or is usually false at first paint. v-show also cannot wrap a <template> or pair with v-else.
Why :key="index" breaks a list
A key tells Vue which old node maps to which new item. The index is tied to position, not identity.
Wrong "The index is unique per row, so :key="index" is fine."
Right when you insert, delete, or reorder, the item at a position changes but its index key does not, so Vue reuses the DOM node or component instance already there and patches the bindings. Anything stateful in that node (an uncontrolled input's text, focus, a checkbox, a child component's own data) sticks to the position and lands on the wrong item. Omitting the key triggers the same in-place patch. Key by a stable id, :key="item.id". Index keys are safe only for append-only, stateless rows.
keep-alive and the activated/deactivated hooks
Without <KeepAlive>, switching away from a component (a dynamic <component :is> or a route change) unmounts it and loses its state. Wrapping it in <KeepAlive> caches the instance instead.
A cached component does not unmount. It fires onDeactivated when it leaves the DOM and onActivated when it returns, and onActivated also runs on the first mount. That changes where setup goes. A timer or subscription started in onMounted keeps running while the component sits in the cache, because onUnmounted does not fire until the instance is truly destroyed. Put refetch-on-entry work in onActivated and tear timers down in onDeactivated.
How the block tree makes updates cheap
The compiler tags each dynamic element with a patch flag: a bitwise number describing what can change. 1 marks dynamic text, 2 marks a dynamic class, and flags OR-combine (1 | 2 = 3 for both). At patch time the runtime does a bitwise check and updates that binding alone instead of diffing every prop.
Those flagged descendants are collected onto the nearest block as a flat dynamicChildren array. A block is a section whose structure is stable; it changes shape only through v-if and v-for, which open child blocks. On update the runtime walks that flat array and never recurses into the static structure, so update cost tracks the count of dynamic bindings, not total node count:
// _openBlock collects flagged descendants; 64 = STABLE_FRAGMENT
return (_openBlock(), _createElementBlock(_Fragment, null, [ /* children */ ], 64))
Static subtrees are cached once and reused, with consecutive statics collapsed into one static vnode holding an HTML string. Inline event handlers are cached too, so a fresh function identity each render does not read as a changed prop. This is all compiler work on templates. Hand-written h() functions carry a BAIL marker and are fully diffed, which is why templates optimize and raw render functions do not.
When v-memo helps, and how it lies
v-memo="[a, b]" takes a fixed-length array and skips a subtree's update, including vnode creation, when every value matches the previous render. v-memo="[]" never updates again, which makes it equivalent to v-once.
Wrong "v-memo tracks its dependencies like a computed."
Right you hand-author the array, and a missing dependency silently skips an update that should have applied, leaving stale UI with no error. On a v-for, v-memo must sit on the same element as v-for (it does not work on a nested child), and you leave item.id out because Vue infers it from the key. The docs scope it to lists past roughly 1000 items where recreating thousands of vnodes is the measured cost. Fix keys first and treat v-memo as a rarely needed micro-optimization.
async components and Suspense failure modes
defineAsyncComponent(() => import('./X.vue')) runs the loader only when the component renders. The options form adds a loadingComponent with a delay before it shows (default 200ms, so a fast network never flashes a spinner) and an errorComponent gated by timeout (default Infinity, so it never times out on its own).
<Suspense> is experimental and its API may still change, so production use carries churn risk. It waits on async dependencies (a component with async setup(), or <script setup> with a top-level await) and shows #fallback until they resolve. Each slot allows only one root node. It has no built-in error handling, so a rejected async dependency needs a parent onErrorCaptured (or the errorCaptured option), or the screen goes blank. Async components under a Suspense are suspensible by default and hand their loading state to it.
keep-alive leaks and LRU eviction
include and exclude match a component's name, which <script setup> infers from the filename since 3.2.34. max turns the cache into an LRU: exceed it and the least recently used instance is destroyed, so state you assumed was cached can vanish.
The lifecycle trap is the sharper one. A cached component fires onDeactivated rather than onUnmounted, so an interval or subscription started in onMounted keeps running while the component sits in the cache and duplicates on the next onActivated. Start per-entry work in onActivated and clean it up in onDeactivated.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.