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á:

O(n) Chi phí diff mỗi lần re-render
2x Bộ nhớ (giữ cả cây cũ + mới)
~60KB Runtime VDOM engine cần ship

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:

  1. Parse: SFC được parse thành AST — bước này chung với VDOM mode
  2. 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)
  3. Template Hoisting: Các cây static được gom thành template literal, tạo bằng innerHTML một lần duy nhất — tối ưu hơn nhiều lần createElement từng node
  4. Effect Generation: Mỗi dynamic binding được wrap trong một reactive effect riêng biệt, chỉ re-run khi dependency thay đổi
  5. 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
~10 KB Bundle Vapor Mode (giảm 83% so với VDOM)
100ms Mount 100.000 components
-46% Giảm memory so với VDOM mode
+36% Tăng tốc DOM manipulation

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ị:

Giai đoạn 1 — Thử nghiệm (Ngay bây giờ)
Tạo branch thử nghiệm, bật Vapor Mode cho 2-3 leaf components (component không có children phức tạp). Đo performance trước/sau bằng Lighthouse và Vue DevTools. Mục tiêu: xác nhận compatibility với stack hiện tại.
Giai đoạn 2 — Performance-critical areas
Áp dụng Vapor cho các component render danh sách lớn (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.
Giai đoạn 3 — Mở rộng dần (Khi Vue 3.6 stable)
Sau khi Vue 3.6 ra stable release, convert phần lớn components sang Vapor — ưu tiên những component đã dùng <script setup> + Composition API. Giữ VDOM cho components phụ thuộc thư viện bên thứ ba chưa tương thích.
Giai đoạn 4 — Full Vapor (Tương lai)
Khi ecosystem hoàn toàn tương thích, chuyển toàn bộ app sang Vapor Mode. Tại điểm này, có thể tree-shake hoàn toàn VDOM runtime, đạt bundle size tối thiểu.

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: