Back to articles

Vue Vapor: Goodbye, Virtual DOM

Vue
Vue Vapor: Goodbye, Virtual DOM

This article is adapted from my talk at Frontend Nation 2026. Head over to this page for the slides and additional resources!

Today, we're going to take a look at Vue Vapor, a new compilation mode that can take your Vue apps to the next level. We're going to learn what it is, what makes it so fast, and how you can try it out today.

Plus, we're going to take a look at some benchmarks, a few interesting demos, and the code Vapor generates. Let's go!

Three Wishes For all Your Vue Apps

Let me start with a little fantasy story. I know it sounds weird, but it's gonna be fun.

Imagine you're an adventurer in the Kingdom of JavaScript, and you come across a genie lamp...

You've just defeated an ugly bug, and among the loot, you find this lamp — the lamp of the Genie of Vue. In a cloud of mist, the guy appears in front of you and says:

Hey, I'll grant three wishes for all your Vue applications.

What would you ask for?

  • Well, perhaps your first wish would be to make all your Vue applications faster. That's an obvious one, right? Let's say 50 times faster. Let's make Vue the fastest JavaScript framework on the face of the Earth. Why not?
  • Perhaps your second wish would be for all your apps to use less memory, so they run smoothly even on potato devices.
  • And last but not least, you might wish to get all this incredible efficiency without having to rewrite your codebase. You don't want to change hundreds of lines of code to make this happen. You still remember the transition from Vue 2 to Vue 3, moving from the Options API to the Composition API. You're happy with the current paradigm, so you don't want to go through another major refactor. I don't blame you.

So the genie looks at you and says:

Your wishes are granted. You only have to add a keyword to your components. A single word. What should it be?

You look around, see all the mist, and say:

Vapor... Vue Vapor.

Vue Vapor is Here!

Of course, this is just a silly fable... but Vue Vapor is very real. It's a new compilation mode available in the upcoming version of Vue, Vue 3.6. And it ticks all the boxes:

  • It's faster... way faster.
  • It uses way less memory.
  • And you can enable it without rewriting your components.

To turn Vapor on, all you need to do is add the vapor keyword to your script setup components:

<script setup vapor>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">Count is {{ count }}</button>
</template>

I know, it sounds too good to be true! Feels like magic. But it's not. It's the result of years of work by the Vue team and many contributors.

Let's review the timeline:

  • At first: The project started in a separate repository, vuejs/vue-vapor, and was later merged into the main Vue repo.
  • A year ago: We got the first alpha release of Vue 3.6, which includes Vapor Mode.
  • Now: We're on Vue 3.6 beta 13.
  • Soon... ish: We'll have the release candidate and the final 3.6 version.

Once it ships, Vapor Mode will be available to everyone.

Vue Vapor is Fast

Now, you've heard it many times: Vue Vapor is fast. But... how fast? How does it rank when compared to other frameworks? Well, let's find out.

First, let's take a look at the JavaScript Framework Benchmark.

This is a very popular benchmark that measures the speed of different JavaScript frameworks when performing common DOM operations: creating rows, updating them, swapping them, removing them, and so on.

It gives each framework a score: a weighted geometric mean across all tasks. The lower the score, the faster the framework.

  • Vanilla JS sits at 1.03 — pretty, pretty good. You can't beat the OG.
  • React, the most popular JavaScript framework in the world, comes in at 1.52.
  • Now, where's Vue? Vue 3.6 alpha comes in at 1.26, not bad.
  • But Svelte and Solid are noticeably faster, at 1.15 and 1.11.

Now, take a quick guess. Where does Vue Vapor land?

It gets a score of 1.08, sitting right next to Vanilla JS.

That is impressive. It means Vue Vapor can put Vue at the top of the pack among popular JavaScript frameworks when it comes to speed. That's outstanding.

But this benchmark used the alpha version. We have the beta now, so let's test that instead.

A quick (pun intended) demo

We'll test the same component in three different versions: Vue 3.5, Vue 3.6, and Vue 3.6 with Vapor Mode enabled.

All three demos use exactly the same component. We have a grid of 10,000 blocks and a button that updates one of them. We measure how long that update takes. We start a timer right before the change and stop it immediately after.

  • Vue 3.5 — about 16 milliseconds to update a single block.
  • Vue 3.6 — about 10 milliseconds. Pretty fast, not gonna lie.
  • Vue 3.6 + Vapor — same version, same component. The only thing we changed was adding the vapor keyword to <script setup>. Other than that, it's identical. And we get... under one millisecond.

From 10 milliseconds to around 0.2 milliseconds for the exact same update. About 50 times faster, just by adding the vapor keyword.

That's pretty cool, right? Honestly, it's incredible. It's super rare to see this kind of performance jump in a minor framework release.

This can have a huge impact on applications with lots of nodes, lots of components, or real-time updates.

Going from 10 ms to 0.2 ms is 50 times faster. If you prefer percentages, that's a 4,900% speed increase. That's the kind of number that can impress your boss!

Memory Usage

But speed isn't the only performance win here. Speed was just the first wish. Let's move on to the second one: memory usage.

Now, why does memory matter? Well, we've all used applications that consume way too much memory. They drop frames, scrolling gets choppy, everything starts to feel sluggish. It's the worst.

So I ran three experiments, took heap snapshots, and calculated the average memory usage for each one. The code is public at github.com/nicodevs/demo-vue-memory-usage, so if you want to verify the numbers yourself, you can go to the repo and run the tests.

  • First, we have a table of 10,000 rows with dynamic content. Vue 3.6 without Vapor uses around 10 MB of RAM, while Vue Vapor uses only about 3 MB. We cut memory usage down to roughly a third.
  • For the second test, I extracted each row into a Row component. It's basically the same HTML, but now we're using a component instead of rendering everything directly. Vue 3.6 jumps to more than 23 MB, while Vue Vapor stays roughly at the same level.
  • And then we can take it one step further. What happens if that Row component doesn't render dynamic content at all and just displays a static "Hello!"? In that case, the difference is even bigger. More than 10 times lighter.

Let's check the final results:

ScenarioVue 3.6Vue 3.6 + VaporReduction
10k rows~9.9 MB~2.9 MB3.4×
10k components~23.2 MB~3.2 MB7.3×
10k static components~19.2 MB~1.9 MB10.1×

You can try it yourself! Clone the repo and run the benchmark:

git clone https://github.com/nicodevs/demo-vue-memory-usage
cd demo-vue-memory-usage
node benchmark.mjs

So how can we explain this? What's making Vapor so efficient? How can adding a single keyword have such a massive impact?

Goodbye, Virtual DOM

We can explain it with a single change: Vapor renders your templates without relying on the Virtual DOM. But hold on.... what is the Virtual DOM?

Modern JS frameworks like React and Vue use the Virtual DOM to handle DOM changes. When a certain piece of state changes, you need to update the DOM accordingly. Now, touching the DOM is an expensive operation: it takes time and forces the browser to repaint that part of the page. The Virtual DOM helps us reduce the number of nodes we patch.

Consider the following component:

<script setup>
import { ref } from 'vue'

const products = ref([
  { id: 1, name: 'Keyboard', price: 49 },
  { id: 2, name: 'Mouse', price: 25 },
  { id: 3, name: 'Webcam', price: 75 },
  { id: 4, name: 'Monitor', price: 199 },
])
</script>

<template>
  <ul>
    <li v-for="product in products" :key="product.id">
      <strong>{{ product.name }}</strong>
      <span>$ {{ product.price }}</span>
    </li>
  </ul>
</template>

If a couple of prices change (inflation!), as result of an interaction or API request, we would need to reflect that change on the screen.

Now, we wouldn't want to patch the whole list. There are nodes that don't need to be updated, like all the product names and the prices that didn't change. We just need to patch two <span>s!

Then, how can we figure out which nodes need patching? Well, perhaps we can have an in-memory copy of the real DOM. This virtual tree is the Virtual DOM.

On a state change, the runtime creates a new tree and compares it to the old one: it walks through it, node by node, checking the differences. Once it gathers those differences, it patches the real DOM, affecting only those nodes.

Now, Vue 3 is already clever about this. It has some tricks to make it better:

  • Static hoisting — static nodes are created once and reused
  • Patch flags — each dynamic binding is tagged so Vue knows exactly where to look
  • Tree flattening — all dynamic nodes are collected into one flat list

But look at what's left: we still rebuild that whole tree of objects, recheck every dynamic value in the list, compare each one just to find the two that moved... All those objects, created and thrown away, on every single change. That's a lot of work.

So, what's the story with Vue Vapor? Well, Vapor doesn't use the Virtual DOM. No copy, no rebuild, no diffing.

You might be wondering: if there's no copy and nothing to compare, how does Vapor know what to update? It knows thanks to the compiler.

When Vapor compiles your component, it follows the exact path from each piece of state to the one DOM node it controls, and wires them together.

That's the reason Vapor asks for one thing: script setup. Vapor only works with single-file components using script setup, for this very reason: it gives the compiler a clear, predictable structure to read.

We call this fine-grained reactivity. Solid has been doing it for years, and now it's available in Vue.

Thanks to this paradigm shift, the updates in Vue Vapor apps are much faster and take way less memory. That's two wishes down! But the third one was the big one: get all of this, without rewriting your code. That's my favorite part. So let's turn Vapor on.

Installing Vapor

First, you'll need Vue 3.6 beta. If you create a fresh project, it will ask you if you want to use 3.6:

pnpm create vue@latest
> Select experimental features to include in your project:
> Vue 3.6 (beta)

If you are on an existing 3.5 application, here's what you can do. Open your package.json and point Vue at the beta:

{
  "dependencies": {
    "vue": "beta"
  }
}

But there's a catch. Vue isn't one package, it's a whole family of them: the runtime, the compiler, and so on. And your build tools quietly depend on some of those too. So if you only bump Vue, your compiler can stay behind on 3.5 — and a 3.5 compiler has no idea what the vapor keyword means.

So if you are using npm, you may need to use the overrides block:

{
  "overrides": {
    "@vue/compiler-core": "beta",
    "@vue/compiler-dom": "beta",
    "@vue/compiler-sfc": "beta",
    "@vue/compiler-ssr": "beta",
    "@vue/compiler-vapor": "beta",
    "@vue/reactivity": "beta",
    "@vue/runtime-core": "beta",
    "@vue/runtime-dom": "beta",
    "@vue/runtime-vapor": "beta",
    "@vue/server-renderer": "beta",
    "@vue/shared": "beta",
    "@vue/compat": "beta"
  }
}

Then delete your lock file and your node_modules, reinstall, and you're on 3.6.

A word of advice: for now, just try it out. Don't move your production applications to Vue 3.6 Vapor just yet. We are still in beta. But feel free to test it out locally and see how it works.

Using Vapor

Now you're ready to opt into Vapor. You already know how to do it: on any Single-File Component using script setup, add the vapor keyword.

<script setup vapor>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">Count is {{ count }}</button>
</template>

Just remember the one rule: it has to be a Single-File Component using script setup. No Options API, and no Composition API outside of script setup.

Now, should you convert every component? You can. But the beautiful thing is, you don't have to.

Your app can run Vapor and classic components together, no problem. A classic component can hold Vapor children, or the other way around. The compiler can handle it.

And just like that, the third wish, granted. We get a massive performance boost, keeping the code we already have, just adding a keyword.

Wrapping Up

I hope you enjoyed this post! I'm super excited about Vapor and the future of Vue.

The demos, slides, and the memory benchmark script are all at nicodevs.com/frontendnation. Give it a spin and let me know what you find.

Until next time!

More in Vue