The Vue compiler and rendering pipeline
Vue’s biggest architectural difference from React is that templates are a compile target, not a runtime construct. The compiler knows what can change and emits code that skips everything else. This is the answer to “why does Vue need no useMemo”.
The pipeline
.vue SFC
-> @vue/compiler-sfc splits <template> / <script> / <style>
-> compiler-dom compiles the template to a render function
-> render function returns a VNode tree
-> runtime patches the real DOM
<script setup> is compiled too: it becomes the body of a setup() function, with the compiler inlining template bindings so it does not even need to return an object. Macros like defineProps, defineEmits and defineModel are compile-time only — they are erased, which is why you cannot import them conditionally or call them inside a function.
Scoped styles work by the compiler adding a data-v-xxxxxx attribute to elements and rewriting selectors to match it. That is why a scoped style does not reach into a child component’s internals without :deep().
Compile-time optimisations
These are what make Vue’s updates cheap, and naming them is a strong senior signal.
| Optimisation | What it does |
|---|---|
| Static hoisting | nodes with no dynamic bindings are created once, outside the render function, and reused every render |
| Patch flags | each dynamic node is tagged with what can change (TEXT, CLASS, PROPS), so the patcher checks only that |
| Block tree | dynamic descendants are collected into a flat array, so the diff walks only nodes that can change, not the whole tree |
| Cached handlers | inline @click="() => ..." handlers are cached so they do not invalidate a child’s props every render |
| Tree flattening | stable subtrees collapse, so the runtime skips entire static regions |
The consequence: React re-runs the component and diffs its output; Vue re-runs an effect and patches a known list of dynamic nodes. That difference is why Vue does not need React.memo, and why in Vue “the component re-rendered” is a much cheaper event.
Render functions and JSX
Templates are the default and are what the optimisations above apply to. Drop to a render function when the structure is genuinely dynamic — a component that renders h(props.tag, ...), or a recursive tree with varying shape.
import { h } from 'vue';
export default {
props: ['level'],
setup(props, { slots }) {
return () => h(`h${props.level}`, slots.default());
},
};
JSX works via @vitejs/plugin-vue-jsx and is common in component-library internals. The trade is explicit: you lose static hoisting, patch flags and block trees, because the compiler can no longer see the structure. Use it where the flexibility is worth it, not by default.
Reactivity meets rendering
Each component instance has one render effect. When a ref the render function read changes, that effect is queued; the scheduler dedupes and flushes on the next microtask, so ten mutations in one tick produce one re-render.
This is why Vue needs no dependency arrays: reading count.value during render registers the dependency automatically. It is also why mutating something the render never read changes nothing on screen — a common confusion when a shallowRef’s inner object is mutated. See 02_reactivity_internals.md.
Only the component whose reactive dependency changed re-renders. A parent re-rendering does not automatically re-render children unless their props actually changed — the opposite of React’s default.
Vapor Mode
Vue 3.6 (RC as of 2026-08, not yet stable) adds Vapor Mode: an opt-in compilation mode that emits direct DOM operations and skips the virtual DOM entirely, in the same direction as Solid and Svelte. Vapor and non-Vapor components can coexist in one app, and it supports a subset of the existing API surface. See 20_versions_and_vapor_mode.md.
Interview angle
- “Why does Vue not need
useMemoorReact.memo?” - two reasons. Reactivity is fine-grained, so only components that actually read the changed value re-render. And the compiler tags dynamic nodes with patch flags and hoists static ones, so even a re-render touches only the parts that can change. - “What is a patch flag?” - a bitmask the compiler attaches to a VNode saying which aspects are dynamic. The runtime patcher then compares only those, instead of doing a full props diff.
- “What do you give up by using JSX or render functions in Vue?” - the compile-time optimisations. The compiler cannot analyse a dynamically built tree, so you lose static hoisting, patch flags and block trees. Reach for it when the structure genuinely varies.
- “How does Vue batch updates?” - a scheduler queues render effects and flushes them on the next microtask, deduplicating by component. That is why
nextTick()exists: it is how you read the DOM after an update. - “How do scoped styles work?” - a data attribute added at compile time plus rewritten selectors. Styling a child’s internals needs
:deep(), which is a deliberate escape hatch, not an oversight.