# How Vue Renders

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Rendering Modes
Source: https://www.devtutorials.ai/learn/vue/rendering-modes/how-vue-renders

> What rendering means, the virtual DOM and patching, compiler hints such as static hoisting and patch flags, how an app starts and mounts, the update cycle and batching, what makes a component render again, using a key to force a fresh component, and client-side rendering with its benefits, limits and SEO.

## From State to Pixels

- You change state. A moment later, the page changes. What happens in between?
- **Rendering** = working out what the page should look like from the current state.
- **Patching** = changing the real page to match, touching as little as possible.
- Knowing this helps you write fast apps and fix strange bugs.

**Simple lesson:** Vue renders a picture of the page, compares it with the last one, and patches only the difference.

### A Real-World Example

- Meera's kitchen screen has a clock that ticks every second, and a long order list.
- Does the order list redraw every second too?
- Watch the Console. Only the clock component renders again.

const orders = ref<string[]>(["Dosa", "Idli"]);
</script>

<template>
    <button @click="orders.push('Vada')">New order</button>
</template>
`,
    "KitchenClock.vue": `<script setup lang="ts">

const time = ref<string>(new Date().toLocaleTimeString());
const timer = window.setInterval(() => (time.value = new Date().toLocaleTimeString()), 1000);
onUnmounted(() => window.clearInterval(timer));
onUpdated(() => console.log("KitchenClock rendered again"));
</script>

<template>
    <p>🕒 {{ time }}</p>
</template>
`,
    "OrderList.vue": `<script setup lang="ts">

defineProps({ orders: { type: Array, required: true } });
onUpdated(() => console.log("OrderList rendered again"));
</script>

<template>
    <ul>
      <li v-for="o in orders" :key="String(o)">{{ o }}</li>
    </ul>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** the clock rendered every second. The list did not.
- "New order" changed `orders`, so only then the list rendered again.

### What You Will Learn

1. What the virtual DOM is.
2. How a render function, VNodes and patching fit together.
3. What hints the template compiler adds.
4. What happens when an app starts.
5. The update cycle, and why updates are batched.
6. Exactly what makes a component render again.
7. How a `key` forces a fresh component.
8. What client-side rendering is, and its limits.

## The Virtual DOM

- Changing the real page (the **DOM**) is slow compared with plain JavaScript.
- So Vue keeps a light copy of the page in memory: the **virtual DOM**, a tree of VNodes.
- On each update, the component's render function makes a **new** VNode tree.
- Vue compares the new tree with the old one. This is **diffing**.
- Then it patches only what changed in the real DOM.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 170" role="img" aria-labelledby="fig-vue-pipeline" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-pipeline">Animation: state changes, the update is scheduled, the render function makes a new VNode tree, Vue diffs it against the old tree, and patches one text node in the real DOM</title>
  <defs><marker id="pp-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <rect x="5" y="50" width="105" height="44" rx="9" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} /><text x="57" y="76" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>state change</text>
    <rect x="132" y="50" width="105" height="44" rx="9" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="184" y="76" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>schedule</text>
    <rect x="259" y="50" width="105" height="44" rx="9" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="311" y="76" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>render → VNodes</text>
    <rect x="386" y="50" width="105" height="44" rx="9" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} /><text x="438" y="76" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>diff old/new</text>
    <rect x="513" y="50" width="120" height="44" rx="9" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }} /><text x="573" y="76" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>patch real DOM</text>
  </g>
  <path d="M110 72 H130 M237 72 H257 M364 72 H384 M491 72 H511" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <circle r="8" cy="120" style={{ fill: "var(--ngc-yellow)", stroke: "var(--ngc-slate)" }}><animate attributeName="cx" values="57;184;311;438;573;573" keyTimes="0;0.2;0.4;0.6;0.8;1" dur="6s" repeatCount="indefinite" /></circle>
  <text x="320" y="155" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 12 }}>Many changes in one tick share one render. Only the differences reach the real page.</text>
</svg>
<figcaption>Change → schedule → render → diff → patch.</figcaption>
</figure>

### Watch the Patch

- A `MutationObserver` reports every real DOM change.
- Changing one price in a list of four produces **one** text change, not a rebuild.

const dishes = ref([
    { id: 1, name: "Dosa", price: 90 },
    { id: 2, name: "Idli", price: 50 },
    { id: 3, name: "Vada", price: 40 },
    { id: 4, name: "Pongal", price: 70 },
]);

onMounted(() => {
    const list = document.getElementById("menu")!;
    new MutationObserver((changes) => {
      console.log(\`\${changes.length} real DOM change(s):\`, changes.map((c) => c.type).join(", "));
    }).observe(list, { subtree: true, childList: true, characterData: true, attributes: true });
});
</script>

<template>
    <ul id="menu">
      <li v-for="d in dishes" :key="d.id">{{ d.name }}: ₹{{ d.price }}</li>
    </ul>
    <button @click="dishes[1].price += 5">Idli +₹5</button>
    <button @click="dishes.push({ id: Date.now(), name: 'Upma', price: 60 })">Add Upma</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** Vue compares a new virtual tree with the old one and patches only the differences.

## Compiler Hints

- Templates are compiled into render functions, before the app runs.
- The compiler sees which parts can **never** change and which can.
- **Static hoisting:** parts that never change are created once and reused.
- **Patch flags:** each dynamic element gets a note, such as `TEXT` or `CLASS`. Updates check only that.
- **Block tree:** Vue keeps a flat list of the dynamic nodes, so it can skip the static ones.
- The full build of Vue can compile at run time. Look at the code it makes below.

const template = \`<div>
    <h3>Meera's Kitchen</h3>
    <p :class="{ hot: spicy }">{{ dish }}</p>
</div>\`;

// compile() returns the render function. Its source shows the compiled code.
const code = compile(template).toString();
</script>

<template>
    <p>Template:</p>
    <pre>{{ template }}</pre>
    <p>Compiled render function:</p>
    <pre style="font-size: 11px; white-space: pre-wrap">{{ code }}</pre>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** the `h3` is marked `CACHED`. It is made once and reused.
- The `p` is marked with `TEXT, CLASS`. On updates, Vue checks only its text and class.

Real projects compile templates at build time with Vite, so the browser never ships the compiler. This lab uses Vue's full build, which includes `compile`, so you can see the output. Do not use `compile` in app code.

**Vapor Mode** is a new, opt-in way to compile Vue components without a virtual DOM, arriving with Vue 3.6. It was at release-candidate stage in September 2026. Components you write today stay the same. Check the Vue blog for its current status before using it in production.

**Simple lesson:** the compiler marks what can change, so updates skip everything else.

## Start-Up and the Update Cycle

- **Start-up:** `createApp` makes the app. `mount` runs the root render function, creates the real DOM, and runs `onMounted` hooks.
- While rendering, Vue records which reactive values each component **reads**.
- **Update:** when one of those values changes, Vue puts that component in a queue.
- Before the next screen paint, Vue runs the queue once. This is **batching**.
- Three changes in one click = one render. `nextTick()` waits for that flush, as you saw in **Reactive State**.

const plates = ref<number>(0);
const note = ref<string>("");
const table = ref<number>(1);

onUpdated(() => console.log("App rendered"));

async function threeChanges(): Promise<void> {
    plates.value++;
    note.value = "extra chutney";
    table.value = 4;
    console.log("Changed three values. DOM text still:", document.getElementById("line")?.textContent);
    await nextTick();
    console.log("After nextTick:", document.getElementById("line")?.textContent);
}
</script>

<template>
    <p id="line">Table {{ table }}: {{ plates }} plates {{ note }}</p>
    <button @click="threeChanges">Make three changes</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

## What Triggers a Re-Render

- A component renders again when a reactive value it **read during its last render** changes.
- State used only inside a click handler does not cause a render.
- A parent rendering again **may** re-render a child, but only if the child's props really changed.
- Same type in the same place = Vue **reuses** the component. A different `key` = Vue makes a new one.

const shown = ref<number>(0);
const hidden = ref<number>(0);

onUpdated(() => console.log("rendered: shown =", shown.value));
</script>

<template>
    <p>Shown: {{ shown }}</p>
    <button @click="shown++">Change shown</button>
    <button @click="hidden++; console.log('hidden is', hidden)">Change hidden (not in template)</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

Use `:key` on purpose to throw a component away and start fresh, for example when the dish changes and a form inside must reset. A new key means a new instance.

**Simple lesson:** a component renders again only when state it read last time changes. A new `key` makes a new component.

## Client-Side Rendering

- **Client-side rendering (CSR)** = the server sends an almost empty HTML page plus JavaScript. The browser builds everything.
- Every Vite app you built in this course is CSR.

    <h2>Meera's Kitchen</h2>
    <p>Fresh dosa all day.</p>
</template>
`,
  }}
/>

<div className="tscroll">

| CSR strength | CSR limit |
|---|---|
| Simple hosting: static files on any CDN | First visit shows nothing until JavaScript runs |
| Fast page changes after the first load | Slow on weak phones and networks |
| Rich, app-like interactions | Some search engines and link previews see an empty page |
| No server code to run | Big bundles hurt the first load |

</div>

- CSR fits dashboards, admin panels and apps behind a login.
- For public pages that must load fast and be found by search, use server or static rendering, as in **Server and Static Rendering**.

"View page source" on a CSR site shows `<div id="app"></div>` and little else. Modern Google can run JavaScript, but many other crawlers and social previews cannot. Do not rely on CSR for pages that must be shared and found.

**Simple lesson:** CSR builds the page in the browser. Great for apps, weaker for first load and search.

## TL;DR

- **Rendering** = working out the page from the current state.
- **Virtual DOM** = a light in-memory tree of VNodes.
- **Diff and patch** = compare new and old trees, change only the differences.
- **Compiler hints** = static hoisting, patch flags, block tree.
- **Dependency tracking** = Vue records what each component reads while rendering.
- **Batching** = many changes in one tick → one render. `nextTick()` waits for it.
- **Re-render trigger** = a value read in the last render changed, or props changed.
- **`:key` change** = throws the component away and makes a new one.
- **CSR** = the browser builds the page. Easy hosting, weaker first load and SEO.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Thinking every change rebuilds the page | Vue redraws everything | Vue patches only differences | Needless worry, or premature hacks |
| Reading the DOM right after a change | The DOM updates at once | `await nextTick()` | Old text is read |
| Expecting a child form to reset on new data | New props reset local state | Change its `:key` | Old input stays in the form |
| Using `compile` in app code | Runtime compiling is free | Compile at build time with Vite | Bigger bundles and slower start |
| CSR for public marketing pages | Search engines see everything | Use SSR or SSG for such pages | Poor previews and search results |
| Changing state inside `onUpdated` | It is a good place to fix values | Use `computed` or a watcher | Endless render loops |

</div>

## Check Yourself

## Exercises

## What to Remember

- Vue renders a virtual tree, diffs it and patches only the differences.
- The compiler marks static and dynamic parts, so updates do less work.
- A component renders again only when state it read changes. Updates are batched.
- Use `nextTick()` to read the DOM after an update, and `:key` to start fresh.
- CSR is great for apps, weaker for first load and search engines.

Next: **Server and Static Rendering**
