Vue 3.6 Vapor Mode — Loại bỏ Virtual DOM, hiệu năng ngang Solid.js
Posted on: 4/21/2026 7:08:12 AM
Vấn đề với Virtual DOM
Kể từ khi React giới thiệu Virtual DOM vào năm 2013, hầu hết các framework frontend hiện đại đều áp dụng mô hình này: mỗi lần state thay đổi, framework tạo một cây Virtual DOM mới (lightweight JavaScript object), so sánh (diff) với cây cũ, rồi mới patch các thay đổi lên Real DOM.
Mô hình này mang lại DX tuyệt vời — developer viết code declarative, framework lo phần còn lại. Nhưng "sự tiện lợi" này có giá:
Vue 3 đã tối ưu đáng kể với compiler-informed VDOM — compiler phân tích template tại build-time để đánh dấu (patch flags) những phần nào thực sự dynamic, giúp runtime skip những node static. Nhưng bản chất vẫn là diff tại runtime, vẫn tạo VNode objects, vẫn tốn memory allocation.
Câu hỏi đặt ra
Nếu compiler đã biết chính xác phần nào dynamic tại build-time, tại sao không bỏ qua VDOM hoàn toàn và sinh code cập nhật DOM trực tiếp?
Đó chính xác là ý tưởng đằng sau Vapor Mode.
Vapor Mode là gì?
Vapor Mode là một chiến lược biên dịch mới trong Vue 3.6, cho phép compiler chuyển đổi template thành các thao tác DOM trực tiếp — không qua bất kỳ lớp Virtual DOM nào. Tên gọi "Vapor" ám chỉ sự "bay hơi" của lớp abstraction VDOM.
Điểm đặc biệt: developer không cần thay đổi cách viết code. Cùng một <template>, cùng Composition API, cùng ref() / computed() — chỉ khác ở output mà compiler sinh ra.
Cơ chế biên dịch compile-time
graph LR
A["<template>
SFC Source"] --> B["Vue Compiler
Parse + Analyze"]
B --> C{"Vapor
enabled?"}
C -->|No| D["VDOM Render
Function"]
C -->|Yes| E["Direct DOM
Operations"]
D --> F["Runtime Diff
+ Patch"]
E --> G["Reactive
DOM Updates"]
F --> H["Real DOM"]
G --> H
style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style B fill:#e94560,stroke:#fff,color:#fff
style C fill:#ff9800,stroke:#fff,color:#fff
style D fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style E fill:#4CAF50,stroke:#fff,color:#fff
style F fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style G fill:#4CAF50,stroke:#fff,color:#fff
style H fill:#2c3e50,stroke:#fff,color:#fff
Pipeline biên dịch: Virtual DOM mode vs Vapor Mode
So sánh output: Virtual DOM vs Vapor
Xét một component đơn giản:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div class="counter">
<h1>Bộ đếm</h1>
<p>Giá trị: {{ count }}</p>
<button @click="count++">Tăng</button>
</div>
</template>
Virtual DOM mode sinh render function trả về VNode tree:
// Simplified output — VDOM mode
function render(_ctx) {
return createVNode("div", { class: "counter" }, [
createVNode("h1", null, "Bộ đếm"),
createVNode("p", null, "Giá trị: " + _ctx.count),
createVNode("button", { onClick: () => _ctx.count++ }, "Tăng")
])
}
// Mỗi re-render: tạo lại TOÀN BỘ cây VNode → diff → patch
Vapor Mode sinh code tạo DOM thật một lần, rồi chỉ cập nhật đúng phần thay đổi:
// Simplified output — Vapor mode
function setup() {
const count = ref(0)
// Tạo DOM thật — chạy MỘT lần
const div = document.createElement("div")
div.className = "counter"
const h1 = document.createElement("h1")
h1.textContent = "Bộ đếm" // static — không bao giờ update
const p = document.createElement("p")
const btn = document.createElement("button")
btn.textContent = "Tăng"
btn.addEventListener("click", () => count.value++)
div.append(h1, p, btn)
// Reactive effect — CHỈ update đúng text node bị ảnh hưởng
watchEffect(() => {
p.textContent = "Giá trị: " + count.value
})
return div
}
Điểm khác biệt cốt lõi
Virtual DOM: tạo lại cây object MỖI lần re-render → diff toàn bộ → patch. Vapor: tạo DOM thật MỘT lần → reactive effect chỉ chạy trên đúng expression thay đổi. Không có object allocation, không có diffing.
Kiến trúc bên trong Vapor Mode
Compiler pipeline
Vapor Mode tận dụng chung frontend của Vue compiler (parse SFC → AST), nhưng từ bước code generation thì rẽ sang một backend hoàn toàn khác:
graph TD
A["SFC Source Code"] --> B["@vue/compiler-sfc
Parse SFC"]
B --> C["Template AST"]
C --> D["Static Analysis
Phân loại node"]
D --> E["Tách Static vs Dynamic"]
E --> F["Static Nodes
→ Template Literal
(innerHTML một lần)"]
E --> G["Dynamic Bindings
→ Fine-grained
Reactive Effects"]
F --> H["Compiled Output"]
G --> H
H --> I["Minimal Runtime
~3KB"]
style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style B fill:#e94560,stroke:#fff,color:#fff
style C fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style D fill:#e94560,stroke:#fff,color:#fff
style E fill:#ff9800,stroke:#fff,color:#fff
style F fill:#4CAF50,stroke:#fff,color:#fff
style G fill:#4CAF50,stroke:#fff,color:#fff
style H fill:#2c3e50,stroke:#fff,color:#fff
style I fill:#e94560,stroke:#fff,color:#fff
Pipeline biên dịch của Vapor Mode
Các bước chính:
- Parse: SFC được parse thành AST — bước này chung với VDOM mode
- Static Analysis: Compiler phân loại từng node thành static (không bao giờ thay đổi) hoặc dynamic (phụ thuộc reactive state)
- Template Hoisting: Các cây static được gom thành template literal, tạo bằng
innerHTMLmột lần duy nhất — tối ưu hơn nhiều lầncreateElementtừng node - Effect Generation: Mỗi dynamic binding được wrap trong một reactive effect riêng biệt, chỉ re-run khi dependency thay đổi
- Minimal Runtime: Vapor không cần VDOM runtime (~45KB), chỉ cần một runtime nhỏ (~3KB) cho reactive scheduling
Tích hợp Reactivity System
Vapor Mode sử dụng chính reactivity system của Vue 3 (ref, computed, watch, effect) — không phải hệ thống mới. Điểm khác biệt nằm ở cách compiler kết nối reactive primitives với DOM:
VDOM mode: Component re-render = chạy lại toàn bộ render function → tạo VNode tree mới → diff
Vapor mode: Mỗi dynamic expression = một effect riêng → khi count.value thay đổi, CHỈ effect của p.textContent chạy lại
Điều này tạo ra granularity ở mức expression thay vì component. Trong một component có 50 dynamic bindings, nếu chỉ 1 binding thay đổi, chỉ 1 effect chạy — không phải tạo lại 50 VNodes rồi diff.
Benchmark: Vue 3.6 vs React 19 vs Svelte 5
Dựa trên các benchmark từ cộng đồng và số liệu chính thức từ team Vue (Q1/2026):
| Tiêu chí | React 19 | Vue 3.6 (VDOM) | Vue 3.6 (Vapor) | Svelte 5 |
|---|---|---|---|---|
| Ops/giây (DOM benchmark) | 28.4 | ~27 | 31.2 | 39.5 |
| Bundle size (baseline) | 72 KB | 58 KB | ~10 KB | 28 KB |
| Mount 100K components | ~350 ms | ~200 ms | ~100 ms | ~100 ms |
| First Contentful Paint | 1.2s | 0.9s | 0.7s | 0.8s |
| Lighthouse Score | 92 | 93 | 94 | 96 |
| Memory (100K nodes) | ~180 MB | ~120 MB | ~65 MB | ~70 MB |
| Kiểu rendering | VDOM + auto memo | VDOM + patch flags | Direct DOM | Compiled reactivity |
Nhận xét
Vapor Mode đưa Vue từ vị trí "ngang React" lên "ngang Solid.js/Svelte" về raw performance, trong khi vẫn giữ nguyên API quen thuộc. Đặc biệt ấn tượng ở bundle size — giảm từ 58KB xuống ~10KB, nhỏ hơn cả Svelte.
Sử dụng Vapor Mode trong dự án
Cấu hình Vite
Vapor Mode yêu cầu Vue 3.6+ và Vite với plugin Vue cập nhật:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
features: {
vaporMode: true // Bật Vapor Mode compiler
}
})
]
})
Viết component với Vapor
Opt-in bằng cách thêm từ khóa vapor vào <script setup>:
<!-- ProductCard.vue — Vapor component -->
<script setup vapor>
import { ref, computed } from 'vue'
interface Props {
name: string
price: number
discount?: number
}
const props = defineProps<Props>()
const quantity = ref(1)
const finalPrice = computed(() => {
const base = props.price * quantity.value
return props.discount
? base * (1 - props.discount / 100)
: base
})
const addToCart = () => {
// emit event hoặc gọi store
}
</script>
<template>
<div class="product-card">
<h3>{{ name }}</h3>
<p class="price">{{ finalPrice.toLocaleString('vi-VN') }}đ</p>
<div class="quantity">
<button @click="quantity = Math.max(1, quantity - 1)">−</button>
<span>{{ quantity }}</span>
<button @click="quantity++">+</button>
</div>
<button class="add-btn" @click="addToCart">Thêm vào giỏ</button>
</div>
</template>
Không cần viết lại code
Composition API, ref(), computed(), defineProps, defineEmits — tất cả hoạt động y hệt. Chỉ cần thêm từ khóa vapor vào <script setup>.
Kết hợp Vapor và Virtual DOM
Vapor Mode hoạt động ở cấp component — trong cùng một ứng dụng, bạn có thể có cả component VDOM và component Vapor:
graph TD
A["App.vue
(VDOM mode)"] --> B["Header.vue
(VDOM mode)"]
A --> C["ProductList.vue
(Vapor ⚡)"]
A --> D["Sidebar.vue
(VDOM mode)"]
C --> E["ProductCard.vue
(Vapor ⚡)"]
C --> F["FilterBar.vue
(Vapor ⚡)"]
D --> G["CartWidget.vue
(VDOM mode)"]
style A fill:#f8f9fa,stroke:#2c3e50,color:#2c3e50
style B fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style C fill:#e94560,stroke:#fff,color:#fff
style D fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
style E fill:#e94560,stroke:#fff,color:#fff
style F fill:#e94560,stroke:#fff,color:#fff
style G fill:#f8f9fa,stroke:#e0e0e0,color:#2c3e50
Kết hợp VDOM và Vapor components trong cùng ứng dụng
Điều này cho phép migrate dần dần: bắt đầu bằng các component performance-critical (danh sách lớn, animations, real-time data), sau đó mở rộng khi Vapor Mode ổn định hơn.
Hạn chế và lưu ý
Tính đến tháng 4/2026, Vapor Mode vẫn ở trạng thái beta. Một số hạn chế cần biết:
| Hạn chế | Chi tiết | Tác động |
|---|---|---|
| Chỉ hỗ trợ <script setup> | Options API và <script> (không setup) chưa được hỗ trợ |
Cần migrate sang Composition API trước |
| Không hỗ trợ Suspense | Tính năng Suspense vẫn đang experimental và chưa có trong Vapor | Async component patterns bị ảnh hưởng |
| Ecosystem chưa sẵn sàng | Một số thư viện UI (Vuetify, PrimeVue) chưa tương thích hoàn toàn | Dùng tốt nhất cho custom components |
| DevTools hạn chế | Vue DevTools chưa hỗ trợ đầy đủ inspection cho Vapor components | Debug khó hơn trong giai đoạn beta |
| Render function thủ công | Nếu dùng h() function thay vì template, Vapor không áp dụng được |
Phải dùng <template> block |
Lưu ý về production
Vue team khuyến nghị dùng Vapor Mode như một opt-in experiment trong các bounded regions, chưa nên dùng làm default cho toàn bộ mission-critical app. Theo dõi changelog tại github.com/vuejs/core/releases để cập nhật trạng thái ổn định.
Chiến lược áp dụng thực tế
Dựa trên đặc điểm của Vapor Mode, đây là chiến lược áp dụng khuyến nghị:
v-for với 100+ items), real-time dashboards, hoặc animation-heavy components. Đây là nơi Vapor mang lại lợi ích rõ rệt nhất: ít allocation → ít GC pause → smoother UX.
<script setup> + Composition API. Giữ VDOM cho components phụ thuộc thư viện bên thứ ba chưa tương thích.
Một số patterns phù hợp nhất với Vapor Mode:
<!-- ✅ Lý tưởng cho Vapor: large list rendering -->
<script setup vapor>
import { ref } from 'vue'
const items = ref(generateLargeDataset(10_000))
const filter = ref('')
const filtered = computed(() =>
items.value.filter(item =>
item.name.toLowerCase().includes(filter.value.toLowerCase())
)
)
</script>
<template>
<input v-model="filter" placeholder="Tìm kiếm..." />
<div class="virtual-list">
<div v-for="item in filtered" :key="item.id" class="list-item">
<span>{{ item.name }}</span>
<span class="meta">{{ item.category }}</span>
</div>
</div>
</template>
Kết luận
Vue 3.6 Vapor Mode đánh dấu một bước tiến lớn trong kiến trúc frontend framework. Bằng cách chuyển công việc diff từ runtime sang compile-time, Vapor loại bỏ overhead của Virtual DOM mà không đánh đổi developer experience.
Với bundle size giảm 83% (từ 58KB xuống ~10KB), hiệu năng DOM manipulation tăng 36%, và khả năng mount 100.000 components trong 100ms, Vapor Mode đưa Vue vào nhóm framework hiệu năng cao nhất — cùng tier với Solid.js và Svelte 5.
Điều quan trọng nhất: đây là một evolution, không phải revolution. Cùng codebase, cùng Composition API, cùng hệ sinh thái — chỉ cần thêm một từ khóa vapor vào <script setup>. Chiến lược migrate dần dần (per-component opt-in) cho phép team áp dụng mà không cần rewrite.
Nguồn tham khảo:
ClickHouse: Cỗ máy phân tích real-time xử lý hàng tỷ rows trong mili-giây
Pulumi — Infrastructure as Code bằng C# trên .NET 10: Quản lý Cloud như viết phần mềm
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.