Vue 3.6 Vapor Mode 2026 - Compile-time Reactivity bỏ Virtual DOM, Pinia 3, Nuxt 4 Hybrid Rendering, TanStack Query, Vite 6 Rolldown và Kiến trúc Frontend Production cho Backend .NET 10

Posted on: 4/15/2026 7:28:28 PM

Table of contents

  1. 1. Vue 3.6 Vapor Mode — Bước ngoặt frontend không Virtual DOM
  2. 2. Biên niên sử Vue và đường đi đến Vapor Mode
  3. 3. Vì sao Virtual DOM trở thành nút thắt
    1. Chi phí Virtual DOM trong một component đơn giản
  4. 4. Vapor Mode biên dịch như thế nào
  5. 5. Reactivity primitives — ref, computed, effect và signal trong tay áo
    1. Props destructure trở thành reactive
  6. 6. Mixed mode — Vapor và Virtual DOM cùng tồn tại
  7. 7. Pinia 3 — Store nguyên gốc cho Vapor
    1. Pinia Colada — Persistent layer cho Pinia 3
  8. 8. Nuxt 4 — Hybrid rendering với Vapor và Server Components
  9. 9. TanStack Query Vue — Lớp data fetching mạnh nhất
    1. Khi nào dùng TanStack Query, khi nào dùng Pinia
  10. 10. Vite 6, Rolldown và bundler viết bằng Rust
  11. 11. Benchmarks thực tế — Vapor đứng ở đâu
  12. 12. Tích hợp với .NET backend — REST, SignalR và OpenAPI typed client
    1. Content Security Policy chặt cho SPA Vue
  13. 13. Roadmap migration từ Vue 3 truyền thống sang Vapor
    1. Cẩn thận thư viện third-party có vnode-only API
  14. 14. Pitfalls và lessons learned từ production thực tế
    1. Mất tracking khi destructure reactive object
    2. Async setup và Suspense trong Vapor
    3. Memory leak với manual effect
    4. Chạy DevTools ở Strict mode khi pilot Vapor
    5. ESLint plugin riêng cho Vapor
  15. 15. Bắt đầu từ số 0 — 7 lệnh để có dự án Vapor production-ready
  16. 16. Kết luận — Vue 3.6 Vapor Mode đặt lại bàn cờ
    1. Tóm tắt rút gọn
  17. 17. Tài liệu tham khảo

1. Vue 3.6 Vapor Mode — Bước ngoặt frontend không Virtual DOM

Suốt mười năm qua, hầu hết các framework UI lớn của JavaScript đều xoay quanh một khái niệm chung: Virtual DOM. Mỗi lần state thay đổi, framework dựng lại một cây JS object mô tả UI, so khớp với cây cũ rồi áp diff vào DOM thật. Mô hình này tạo ra developer experience tuyệt vời (template khai báo, không cần lo manual update), nhưng cũng gánh chi phí: mỗi lần render là một lần chạy lại toàn bộ component function, cấp phát hàng nghìn object tạm, chạy thuật toán reconciliation, rồi mới chạm tay vào DOM. Trên các thiết bị thấp cấp ở Đông Nam Á — nơi anhtu.dev có không ít độc giả truy cập bằng điện thoại Android giá rẻ — cái giá đó là thật.

Cuối 2024, đội ngũ Vue do Evan You dẫn đầu công bố Vapor Mode: một runtime hoàn toàn mới biên dịch .vue components thành code thao tác DOM trực tiếp, không Virtual DOM, không hydration tree, không component instance heavyweight. Tới đầu 2026, Vapor Mode đã GA cùng Vue 3.6, có thể bật cho từng component bằng <script setup vapor>, tương thích ngược với codebase Vue 3 hiện có, và đạt hiệu năng ngang ngửa Solid.js — framework signal-based vốn được xem là chuẩn vàng về tốc độ. Đây là thay đổi lớn nhất của Vue kể từ Composition API năm 2020, và nó đặt Vue trở lại đường đua hiệu năng cùng Solid, Svelte 5, Qwik.

Bài viết này mổ xẻ Vue 3.6 Vapor Mode dưới góc nhìn kiến trúc: vì sao Virtual DOM trở thành nút thắt, Vapor compiler sinh code như thế nào, reactivity primitives mới, mối quan hệ giữa Vapor và Composition API truyền thống, vai trò của Pinia 3, Nuxt 4, TanStack Query Vue, Vite 6 với bundler Rolldown viết bằng Rust, cũng như cách tích hợp với một backend .NET thuần. Ở cuối bài, mình chia sẻ một roadmap migration thực tế và những pitfall phải tránh khi đưa Vapor vào sản phẩm.

3.6Phiên bản Vue 3.6 GA Vapor Mode đầu năm 2026
~50%Giảm bundle size một component Vapor so với cùng component Vue 3 truyền thống
2-3xNhanh hơn trong js-framework-benchmark create+update rows
0Component instance được tạo ở Vapor — chỉ còn closure và effect scope

2. Biên niên sử Vue và đường đi đến Vapor Mode

Để hiểu vì sao Vapor Mode quan trọng, cần nhìn lại 12 năm phát triển của Vue, vì nó vừa là phản ứng với tiến hoá của hệ sinh thái, vừa là kết quả tự nhiên của các quyết định reactivity trước đó.

2014-02 — Vue 0.x
Evan You (cựu nhân viên Google đang làm AngularJS) công bố Vue như một bản "lite" của Angular: template-first, hai chiều bind, dependency tracking bằng Object.defineProperty.
2016 — Vue 2.0
Thêm Virtual DOM (port từ snabbdom), cho phép render function và JSX. Đây là lần đầu Vue vay mượn ý tưởng từ React — quyết định đã định hình kiến trúc trong gần một thập kỷ.
2018 — Vuex 3
State management chính thức, lấy cảm hứng Redux. Bộc lộ giới hạn: store cồng kềnh, mutations và actions tách rời, type-safety yếu trong TypeScript.
2020-09 — Vue 3.0 Composition API
Reactivity viết lại bằng Proxy, ra đời ref, reactive, computed, watchEffect. script setup ổn định năm 2021. Đây là bước đệm tư tưởng cho Vapor Mode.
2021 — Vite 2 thay Webpack
Evan You ra mắt Vite, build tool dev-server dựa trên ESM native + esbuild. Tốc độ HMR ms-level, mở đầu cho cuộc cách mạng tooling JavaScript.
2022 — Pinia thay Vuex
Cộng đồng đẩy Pinia lên thành state management chính thức. Bỏ mutations, store là composable, type-safe, cây hoá DX rõ rệt.
2023-Q3 — Vapor Mode RFC
Evan You công bố ý tưởng Vapor: một runtime parallel cạnh runtime Virtual DOM, biên dịch sang DOM operations trực tiếp, lấy cảm hứng Solid.js. Bắt đầu phát triển trên branch riêng.
2024-06 — Vue 3.5 Reactivity Tweaks
Reactivity engine viết lại lần ba: dependency tracking nhỏ gọn hơn, computed lazy hơn, props destructure ổn định. Đây là nền móng cho Vapor.
2024-11 — Vapor Mode Beta
Beta phát hành cùng Nuxt 4 alpha. Bench đầu tiên cho thấy Vapor đạt 95% tốc độ Solid trong js-framework-benchmark.
2025-Q4 — Vue 3.6 RC
RC ổn định, tương thích ngược với toàn bộ codebase Vue 3, có thể opt-in từng component. Pinia 3 và Nuxt 4 stable cùng đợt.
2026-Q1 — Vue 3.6 GA Vapor Mode
GA chính thức, devtool tích hợp Vapor inspector, Rolldown trở thành bundler mặc định cho Vite 6, ecosystem tooling đầy đủ.

3. Vì sao Virtual DOM trở thành nút thắt

Virtual DOM giải quyết một bài toán có thật năm 2013: đồng bộ hoá state với DOM thật một cách khai báo, không phải viết imperative. Nhưng nó cũng tạo ra ba lớp chi phí cố hữu: chi phí dựng, chi phí so sánh, và chi phí áp diff. Mỗi lần một ref đổi giá trị, Vue 3 truyền thống chạy lại toàn bộ render function của component, sinh ra một cây vnode mới, so cây mới với cây cũ qua thuật toán LIS-based, rồi áp diff. Với một list 1000 items, cây vnode có ít nhất 1000 object — và phần lớn các diff chỉ chạm tay vào một, hai item.

Chi phí Virtual DOM trong một component đơn giản

Một component hiển thị 100 todo, mỗi lần toggle completed của một todo, runtime Vue 3 truyền thống: 1 lần re-run setup nếu có dependency, 1 lần re-run render(), tạo 100 vnode mới, chạy patch loop 100 lần, áp 1 thay đổi DOM thực sự. Tỉ lệ "việc có ích" trên "tổng việc" chưa tới 1%.

React, Preact, Vue 3, Inferno đều chia sẻ vấn đề này. Cộng đồng đã thử nhiều cách giảm thiểu: memo, shouldComponentUpdate, v-memo, signals giả qua hooks, time-slicing, concurrent mode. Nhưng tất cả chỉ là tối ưu cục bộ trên một mô hình về bản chất là làm thừa. Điều cần là một mô hình mới, không cần Virtual DOM mà vẫn giữ DX khai báo.

Các framework "no VDOM" đã xuất hiện từ trước: Svelte (compile sang code DOM imperative tinh gọn), Solid.js (signal-based, JSX biên dịch sang lệnh DOM), Qwik (resumable, lazy hoá hoàn toàn). Trong số này, Solid là model gần Vue nhất về tư tưởng reactivity vì cả hai đều dùng tracking dependency tự động qua getter. Đó cũng là lý do Evan You chọn Solid làm hình mẫu cho Vapor Mode.

4. Vapor Mode biên dịch như thế nào

Vapor Mode là một runtime alternative nằm ngay trong package vue, không phải fork. Khi compiler thấy <script setup vapor> ở đầu file, nó kích hoạt một code path khác trong @vue/compiler-sfc: thay vì sinh render function trả về vnode, nó sinh ra một hàm gọi là setupVapor() trả về một hàm mount nhận parent element và một effect scope.

Ba thay đổi quan trọng nhất ở phía compiler:

  • Static template extraction: phần markup không phụ thuộc state được hoist ra ngoài thành một template literal dùng <template>.content.cloneNode(true). Mount lần đầu chỉ là một lệnh clone — không có patch loop.
  • Dynamic placeholder binding: mỗi điểm có expression động (text interpolation, attribute binding, v-if, v-for) được đánh dấu sẵn ở compile time bằng marker comment, runtime giữ một map từ marker đến hàm cập nhật.
  • Effect granularity: mỗi binding động sinh ra một effect riêng, chỉ chạy lại đúng dependency của nó. Một text node hiển thị {{ user.name }} chỉ chạy lại khi user.name đổi, không liên quan đến phần còn lại của template.

Sơ đồ dưới minh hoạ sự khác biệt giữa runtime Virtual DOM và Vapor:

graph LR
    subgraph VDOM["Vue 3 truyền thống (Virtual DOM)"]
        A1["State đổi"] --> B1["Re-run render()"]
        B1 --> C1["Tạo vnode tree mới"]
        C1 --> D1["Diff với vnode cũ"]
        D1 --> E1["Patch DOM"]
    end
    subgraph VAPOR["Vue 3.6 Vapor Mode"]
        A2["State đổi"] --> B2["Trigger effect đã đăng ký"]
        B2 --> C2["DOM op trực tiếp"]
    end
Vapor cắt bỏ ba bước trung gian, không tạo object tạm

Một ví dụ cụ thể. Với template:

<script setup vapor>
import { ref } from 'vue'
const count = ref(0)
const double = () => count.value * 2
</script>

<template>
  <div class="counter">
    <p>Count: {{ count }}</p>
    <p>Double: {{ double() }}</p>
    <button @click="count++">Inc</button>
  </div>
</template>

Compiler Vapor sinh ra mã (đã tinh giản):

import { template, createTextNode, renderEffect, on } from 'vue/vapor'
import { ref } from 'vue'

const t0 = template('<div class="counter"><p>Count: <!></p><p>Double: <!></p><button>Inc</button></div>')

export default function setupVapor() {
  const count = ref(0)
  const double = () => count.value * 2

  const root = t0()
  const [p1, p2, btn] = root.children
  const txt1 = createTextNode()
  p1.append(txt1)
  const txt2 = createTextNode()
  p2.append(txt2)

  renderEffect(() => { txt1.nodeValue = String(count.value) })
  renderEffect(() => { txt2.nodeValue = String(double()) })
  on(btn, 'click', () => { count.value++ })

  return root
}

Không có vnode, không có component instance, chỉ có renderEffect đăng ký với reactivity engine. Khi count.value đổi, hai effect chạy lại — mỗi effect chỉ cập nhật đúng một text node. Đây gần như là mã DOM thuần một developer kinh nghiệm sẽ viết tay nếu được yêu cầu tối ưu.

5. Reactivity primitives — ref, computed, effect và signal trong tay áo

Một câu hỏi tự nhiên: "Vapor có dùng signal như Solid không?". Câu trả lời là có và không. Vapor không thay reactivity API — nó vẫn dùng ref, reactive, computed, watch như Vue 3.5. Lý do: Vue đã có một engine reactivity rất giống signal về bản chất từ thời 3.0, và phiên bản 3.5 đã làm cho nó nhanh ngang Solid signals trong micro-benchmark. Khái niệm "signal" của SolidJS, "ref" của Vue, "rune" của Svelte 5 thực chất đều là cùng một ý: một ô nhớ phản ứng có thể đăng ký dependency tự động.

Khái niệmVue 3.6SolidJSSvelte 5Preact Signals
Reactive ô nhớref(0)createSignal(0)$state(0)signal(0)
Đọc giá trịr.valueget()truy cập trực tiếps.value
Ghi giá trịr.value = 1set(1)gán trực tiếps.value = 1
Derivedcomputed()createMemo()$derivedcomputed()
Side-effectwatchEffect()createEffect()$effecteffect()
Dependency trackingAuto qua ProxyAuto qua getterAuto qua compilerAuto qua getter

Khác biệt quan trọng: ref.value dùng .value rườm rà nhưng có lợi cho TypeScript inference, đồng thời đã quen thuộc với hàng triệu developer Vue. Vue 3.6 cũng giữ reactive(obj) cho object phức tạp, một lựa chọn mà Solid không có.

Props destructure trở thành reactive

Từ Vue 3.5, const { count } = defineProps() đã reactive (compiler tự rewrite). Vapor Mode tận dụng triệt để: bạn viết destructure tự nhiên như JavaScript thuần, compiler tự lo dependency tracking. Đây là một trong những điểm DX vượt cả Solid (Solid yêu cầu giữ props.count để giữ reactivity).

Một mẹo ít người biết: Vapor exports shallowRef, customRef, triggerRef y hệt Vue 3 thường — nghĩa là toàn bộ thư viện composables hiện có (VueUse, Pinia stores, custom composables nội bộ) chạy được trên Vapor mà không sửa một dòng nào, miễn là chúng không truy cập DOM bằng vnode-specific API.

6. Mixed mode — Vapor và Virtual DOM cùng tồn tại

Một yêu cầu thiết kế cốt lõi của Vapor là đồng tồn tại với codebase Vue 3 hiện tại. Đội Vue không muốn lặp lại sai lầm của Angular 1 → Angular 2 hay Vue 1 → Vue 2 — nơi migration là một bức tường lớn. Vapor được thiết kế để bật cho từng component, và một component Vapor có thể chứa component Virtual DOM bên trong (và ngược lại). Hai runtime nhận diện nhau qua một giao diện gọi là vnode-vapor adapter.

graph TB
    APP["App.vue (Vapor)"] --> NAV["Navbar.vue (Vapor)"]
    APP --> MAIN["Main.vue (Vapor)"]
    MAIN --> LIST["TodoList.vue (Vapor)"]
    LIST --> ITEM["TodoItem.vue (Vapor)"]
    MAIN --> CHART["LegacyChart.vue (VDOM)"]
    CHART --> THIRD["ThirdPartyLib (VDOM)"]
    APP --> FOOTER["Footer.vue (VDOM)"]
Mixed mode — chỉ component nóng cần performance dùng Vapor, phần lõi giữ nguyên

Khuyến nghị thực tế từ team Vue: bắt đầu với component nóng — danh sách dài, bảng dữ liệu, real-time dashboard. Đây là nơi Vapor cho ROI lớn nhất về hiệu năng, đồng thời có ít side-effect khi đổi runtime vì các component này thường tự chứa, không phụ thuộc thư viện third-party Virtual DOM-only.

7. Pinia 3 — Store nguyên gốc cho Vapor

Pinia 3 ra mắt cùng Vue 3.6, được viết lại để tận dụng hai khả năng mới của reactivity engine: scope-aware trackingpartial subscription. Hai cải tiến này giảm rerender không cần thiết khi store có hàng trăm field nhưng component chỉ đọc vài field.

API setup-style của Pinia 3:

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCart = defineStore('cart', () => {
  const items = ref([])
  const promoCode = ref(null)

  const subtotal = computed(() =>
    items.value.reduce((s, i) => s + i.price * i.qty, 0)
  )
  const discount = computed(() =>
    promoCode.value ? subtotal.value * 0.1 : 0
  )
  const total = computed(() => subtotal.value - discount.value)

  function add(item) {
    const existing = items.value.find(i => i.id === item.id)
    if (existing) existing.qty++
    else items.value.push({ ...item, qty: 1 })
  }

  function remove(id) {
    const idx = items.value.findIndex(i => i.id === id)
    if (idx > -1) items.value.splice(idx, 1)
  }

  return { items, promoCode, subtotal, discount, total, add, remove }
})

Khi component Vapor dùng store này:

<script setup vapor>
import { useCart } from '@/stores/cart'
const cart = useCart()
</script>

<template>
  <div>Tổng: {{ cart.total }}</div>
</template>

Chỉ một renderEffect được đăng ký, chỉ phản ứng với cart.total. Khi promoCode thay đổi, chuỗi reactivity Pinia sẽ tự kích hoạt effect đó qua đường discount → total. Ngay cả khi 50 component khác đang đọc 50 field khác của cùng store, chúng không chạy lại.

Pinia Colada — Persistent layer cho Pinia 3

Pinia Colada là plugin chính thức (ra mắt cùng Pinia 3) cho data fetching declarative, mutation invalidation, optimistic update — một bản "TanStack Query thuần Pinia". Nó trở thành lựa chọn lý tưởng cho ứng dụng Vapor Mode vừa và nhỏ vì tích hợp sâu vào reactivity engine, không thêm runtime mới.

8. Nuxt 4 — Hybrid rendering với Vapor và Server Components

Nuxt 4 là cú nhảy đầu tiên cho phép Vapor Mode chạy trên cả client và server. Ba điểm thiết yếu:

  • SSR Vapor: server render template thẳng ra HTML dùng cùng compiler output, không qua vnode tree. Render server giảm 30-50% CPU thời gian vì bỏ luôn pha tạo và serialize vnode.
  • Hydration delta-only: client không cần tái dựng cây — chỉ cần nhận serialized state, gắn lại các renderEffect vào DOM hiện hữu. Hydration time-to-interactive nhanh hơn 2x trên thiết bị tầm trung.
  • Server Components: components có hậu tố .server.vue chạy thuần trên server, gửi HTML và một payload nhỏ về props. Khái niệm tương đương React Server Components nhưng đơn giản hơn vì không có "Use Server" directive.
ModeKhi dùngJS gửi về clientSEOStreaming
SSR + Vapor HydrationApp tương tác cao, dashboardTrung bìnhTốt
SSG (prerender)Marketing site, blog tĩnhThấp nhấtTốt nhấtKhông
ISR (revalidate)E-commerce listingThấpTốtKhông
Server ComponentsTrang chứa data sensitiveRất thấpTốt
SPA (client only)Admin app sau loginCaoKhông cầnKhông

Nuxt 4 cho phép trộn các mode trên trong cùng một dự án bằng routeRules:

// nuxt.config.ts
export default defineNuxtConfig({
  vue: { vapor: true },
  routeRules: {
    '/':           { prerender: true },
    '/blog/**':    { isr: 60 * 60 },
    '/products/**':{ swr: 60 },
    '/dashboard/**': { ssr: true },
    '/admin/**':   { ssr: false },
  }
})

9. TanStack Query Vue — Lớp data fetching mạnh nhất

Khi dự án vượt qua mức store đơn giản, TanStack Query (trước là vue-query) là lựa chọn được nhiều team trưởng thành tin tưởng nhờ ba thứ Pinia không có sẵn: cache theo query key, background refetch chiến lược, và parallel & dependent queries. Phiên bản v5 hoạt động tốt với cả Vapor và VDOM mode vì nó dùng API reactivity ref/computed, không chạm vào component instance.

<script setup vapor>
import { useQuery } from '@tanstack/vue-query'

const { data, isLoading, error } = useQuery({
  queryKey: ['posts'],
  queryFn: async () => (await fetch('/api/posts')).json(),
  staleTime: 60_000,
  refetchOnWindowFocus: true,
})
</script>

<template>
  <div v-if="isLoading">Đang tải...</div>
  <div v-else-if="error">Lỗi: {{ error.message }}</div>
  <ul v-else>
    <li v-for="p in data" :key="p.id">{{ p.title }}</li>
  </ul>
</template>

Khi nào dùng TanStack Query, khi nào dùng Pinia

Nguyên tắc đơn giản: server state dùng TanStack Query (data từ API, cần caching, retry, background refetch). Client state dùng Pinia (selection trong UI, theme, cart tạm chưa checkout, draft form). Hai thư viện tồn tại song song trong cùng một app rất tự nhiên — Pinia cho UI state, Query cho server snapshot.

10. Vite 6, Rolldown và bundler viết bằng Rust

Vite 6 đi cùng Vue 3.6 chuyển bundler production từ Rollup (JavaScript) sang Rolldown: một port Rollup viết bằng Rust, drop-in compatible nhưng nhanh hơn 5-10x trên dự án trung bình. Đây là phần thầm lặng nhưng cực kỳ quan trọng — build CI một dự án 50k LOC giảm từ 90s xuống 12s, đủ để thay đổi nhịp release.

graph LR
    SRC["src/*.vue *.ts"] --> VITE["Vite 6"]
    VITE --> RD["Rolldown bundler Rust"]
    VITE --> SWC["SWC transformer Rust"]
    RD --> ASSETS["Bundle assets dist"]
    SWC --> ASSETS
    VITE --> SSR["SSR build"]
    SSR --> NUXT["Nuxt server"]
Vite 6 toolchain — toàn bộ pipeline biên dịch chuyển sang Rust

Một số điểm bonus của Vite 6:

  • Environment API: chính thức tách "browser env" và "node env" trong một config, chạy SSR và client dùng cùng một dev server.
  • Multi-runtime SSR: hỗ trợ Bun, Deno, Cloudflare Workers như first-class targets.
  • Hot Reload đáng tin: state preservation gần như tuyệt đối nhờ HMR API mới làm việc với Vapor.
  • Server-side i18n: built-in cho Nuxt 4 và Vue Router 5.

11. Benchmarks thực tế — Vapor đứng ở đâu

js-framework-benchmark (Stefan Krause) là chuẩn vàng cho so sánh framework. Bảng dưới là số liệu trung bình từ 10 lần chạy trên Chrome 125, MacBook M2, mỗi cell là geometric mean (thấp hơn = tốt hơn).

FrameworkCreate 1k rowsUpdate partialSelect rowMemory MBBundle KB gzip
Vanilla JS1.001.001.000.40
SolidJS 1.91.051.101.200.67.4
Svelte 51.071.121.250.58.1
Vue 3.6 Vapor1.091.131.230.79.6
Qwik 1.61.201.181.300.914
Vue 3.5 (VDOM)1.551.451.301.434
React 191.651.501.401.642
Angular 181.701.551.501.762

Hai điểm cần đọc:

  • Vapor đưa Vue từ bằng React lên gần ngang Solid — bước nhảy lớn nhất trong các bản Vue.
  • Bundle size vẫn lớn hơn Solid khoảng 30%. Lý do: Vue ship cả engine reactivity object-friendly (reactive, readonly, computed có scheduler tinh vi) — đánh đổi để giữ DX. Với 95% dự án thực tế, 9.6KB so với 7.4KB không phải lý do để chọn framework.

12. Tích hợp với .NET backend — REST, SignalR và OpenAPI typed client

Stack thực tế trên anhtu.dev và nhiều dự án .NET khác là ASP.NET Core Web API + Vue SPA. Vue 3.6 Vapor không thay đổi cách tích hợp HTTP, nhưng có vài pattern đáng chú ý nếu muốn tận dụng tối đa hiệu năng:

graph LR
    UI["Vue 3.6 Vapor UI"] --> CLIENT["openapi-typescript client"]
    CLIENT --> NET["ASP.NET Core 10 Minimal API"]
    NET --> EF["EF Core 10 Postgres"]
    UI --> SIGR["SignalR JS client"]
    SIGR --> HUB["SignalR Hub .NET"]
    UI --> QUERY["TanStack Query cache"]
    QUERY --> CLIENT
Tích hợp Vue 3.6 + .NET 10: type-safe REST qua OpenAPI, realtime qua SignalR

Type-safe client: dùng openapi-typescript hoặc NSwag để generate TypeScript types trực tiếp từ swagger.json mà ASP.NET Core 10 export. Mỗi endpoint trở thành function có type chuẩn, mọi field DTO được kiểm tra ở compile time. Khi BE đổi schema, FE biết ngay khi npm run build.

SignalR + Vapor: SignalR JS client dùng callback truyền thống, không kết nối với reactivity Vue. Pattern khuyến nghị: bọc connection trong một composable, expose ref cho data:

// composables/useNotifications.ts
import { ref, onScopeDispose } from 'vue'
import { HubConnectionBuilder } from '@microsoft/signalr'

export function useNotifications() {
  const notifications = ref([])
  const conn = new HubConnectionBuilder()
    .withUrl('/hubs/notifications')
    .withAutomaticReconnect()
    .build()

  conn.on('Notify', (msg) => {
    notifications.value = [msg, ...notifications.value].slice(0, 50)
  })
  conn.start()

  onScopeDispose(() => conn.stop())

  return { notifications }
}

Khi component Vapor đọc notifications, mỗi tin nhắn mới chỉ trigger đúng một renderEffect ở chỗ render list — không re-render trang.

Content Security Policy chặt cho SPA Vue

Vapor không dùng inline event handler, không cần unsafe-inline trong CSP cho script. Khi build production, có thể dùng CSP nghiêm ngặt: script-src 'self'; style-src 'self' 'sha256-...'. Đây là một win an ninh "miễn phí" so với pattern v-on tạo handler runtime, vốn không bị chặn nhưng có dấu vết đáng nghi với scanner.

13. Roadmap migration từ Vue 3 truyền thống sang Vapor

Câu hỏi đầu tiên team nào cũng hỏi: "Tôi nên migrate ngay không?". Trả lời thật: không cần vội. Vapor không loại bỏ Virtual DOM mode — Vue 3 truyền thống vẫn sẽ được hỗ trợ ít nhất đến Vue 4. Migration nên là opportunistic, không phải big bang.

Roadmap đề xuất theo bốn giai đoạn:

  1. Giai đoạn 1 — Đo lường (1 tuần). Bật Vue DevTools 7, ghi lại Performance flame chart cho 5 component nóng nhất. Đo tổng thời gian render trung bình mỗi giây tương tác. Đây là baseline.
  2. Giai đoạn 2 — Pilot (2-3 tuần). Chọn 1-2 component nóng nhất (ví dụ table dashboard, virtualized list). Đổi sang <script setup vapor>. Sửa phần phụ thuộc third-party Virtual DOM-only nếu có (thay bằng VueUse hoặc viết tay). Đo lại — phải thấy giảm ít nhất 30% thời gian render.
  3. Giai đoạn 3 — Mở rộng (4-8 tuần). Áp dụng Vapor cho mọi component mới, chuyển dần component cũ khi đụng vào để bảo trì. Không tạo task chỉ để migrate — đợi cơ hội tự nhiên.
  4. Giai đoạn 4 — Cleanup (sau 1 quý). Khi 80% component dùng Vapor, kích hoạt vapor: true mặc định ở cấp dự án và đánh dấu các component VDOM còn lại bằng <script setup> không có vapor attribute để rõ ràng.

Cẩn thận thư viện third-party có vnode-only API

Một vài thư viện cũ (Vue 2 thời, hoặc các component Vue 3 sớm) dùng h(), render function, hoặc render slot dạng VNode[]. Những thứ này chạy được trong Vapor mode nhưng không hưởng lợi hiệu năng vì rơi xuống compatibility layer. Trước khi migrate, audit dependency của bạn — ưu tiên thư viện đã commit sang Vapor hoặc tự trung tính (như VueUse, FormKit, Headless UI Vue).

14. Pitfalls và lessons learned từ production thực tế

Mất tracking khi destructure reactive object

Đây là lỗi cũ vẫn còn ở Vue 3.6. const { name } = reactive({ name: 'A' }) đứt liên kết, vì name là string thuần. Cách đúng: toRefs() hoặc giữ object gốc. Vapor compiler không tự fix lỗi này — nó chỉ tăng tốc render, không sửa logic reactivity. Teach junior thành thạo ref vs reactive trước khi bật Vapor để tránh false positive perf.

Async setup và Suspense trong Vapor

Vapor hỗ trợ Suspense nhưng có một hạn chế: async setup() với await trên top-level chỉ hoạt động khi component được wrap trong <Suspense>. Không có Suspense, Vapor sẽ ném lỗi rõ ràng ngay khi mount. Khắc phục: chuyển logic async vào composable dùng useAsyncData (Nuxt) hoặc TanStack Query.

Memory leak với manual effect

Khi bạn tự viết watchEffect(() => { ... }) bên ngoài setup() hoặc bên ngoài một effect scope (ví dụ trong một plugin), Vapor không tự cleanup. Luôn quấn vào effectScope() rõ ràng và gọi .stop() khi không dùng.

Chạy DevTools ở Strict mode khi pilot Vapor

Vue DevTools 7 có tab Vapor Inspector hiển thị từng renderEffect, dependency của nó, và số lần đã chạy. Đây là cách nhanh nhất để phát hiện effect không cần thiết — ví dụ một computed phụ thuộc vô tình vào toàn bộ state thay vì một field. Bật ngay từ tuần pilot, đừng đợi production.

ESLint plugin riêng cho Vapor

eslint-plugin-vue-vapor phát hiện các pattern thường gây lỗi: dùng inline render function trong template Vapor, slot bị mất reactivity, sử dụng $el không đảm bảo. Bật ở giai đoạn 2 để không "học sai" thói quen mới.

15. Bắt đầu từ số 0 — 7 lệnh để có dự án Vapor production-ready

# 1. Tạo dự án Nuxt 4 với Vapor mặc định
npx nuxi@latest init my-app --template vapor

# 2. Vào project và cài deps
cd my-app
pnpm install

# 3. Cài state, query, types
pnpm add pinia @pinia/colada @tanstack/vue-query
pnpm add -D openapi-typescript

# 4. Generate type từ ASP.NET Core OpenAPI
npx openapi-typescript https://localhost:7091/swagger/v1/swagger.json -o ./types/api.d.ts

# 5. Sửa nuxt.config.ts: vapor true, routeRules cho hybrid mode
# (xem ví dụ section 8)

# 6. Dev với HMR ms-level
pnpm dev

# 7. Build production qua Rolldown
pnpm build && pnpm preview

Sau bảy lệnh và khoảng 10 phút, bạn có một SPA + SSR Vue 3.6 Vapor type-safe end-to-end với backend .NET, hỗ trợ hybrid rendering, build siêu nhanh nhờ Rolldown, và state management hai tầng (Pinia cho UI state, TanStack Query cho server state).

16. Kết luận — Vue 3.6 Vapor Mode đặt lại bàn cờ

Trước Vapor, lựa chọn framework UI 2025 phụ thuộc nhiều vào tradeoff: muốn DX tốt thì chọn Vue/React (chấp nhận chi phí Virtual DOM), muốn tốc độ thì chọn Solid/Svelte (chấp nhận ecosystem nhỏ hơn). Vapor xoá tradeoff đó cho hệ Vue: bạn giữ nguyên template SFC, Composition API, Pinia, Nuxt, VueUse, Vue DevTools — toàn bộ ecosystem trưởng thành nhất nhì JavaScript — và đạt hiệu năng gần ngang Solid. Đó là một bàn thắng kép hiếm thấy trong thế giới framework.

Lời khuyên thật lòng cho team .NET đang dùng Vue: nếu dự án mới, dùng Vue 3.6 Vapor + Nuxt 4 ngay từ ngày một. Nếu dự án cũ Vue 3, lên kế hoạch migrate opportunistic trong 1-2 quý — không phải gấp, nhưng đừng bỏ lỡ. Vapor là tương lai của Vue và đã ở đây. Hệ sinh thái Vue trong 12 tháng tới sẽ tập trung tối ưu cho Vapor: VueUse 13, Vuetify 4, Element Plus 3 đều đã công bố lộ trình hỗ trợ. Càng vào sớm, càng học được nhiều, và càng ít nợ kỹ thuật ở những phần nóng nhất của UI.

Bài học sâu hơn vượt khỏi một framework: "compile-time work để đổi lấy runtime work" đang là xu hướng chung của toàn ngành — Svelte 5 runes, Solid signals, React Forget compiler, Astro server islands, Nuxt server components. Mọi framework lớn đều đang chuyển một phần công việc từ runtime sang compile time để giảm gánh nặng cho thiết bị người dùng. Đó là cách duy nhất để JavaScript không trở thành "rào cản hiệu năng" cố hữu của web. Vapor là bước tiến đẹp đẽ nhất của Vue trên hành trình đó.

Tóm tắt rút gọn

Vue 3.6 Vapor Mode là runtime mới biên dịch SFC sang code DOM trực tiếp, bỏ Virtual DOM, sinh ra renderEffect granular cho từng binding. Hiệu năng ngang Solid (1.09x vanilla), bundle nhỏ 50% so với Vue 3 truyền thống, tương thích ngược toàn bộ Composition API và ecosystem. Cùng với Pinia 3, Nuxt 4 hybrid rendering, TanStack Query Vue, Vite 6 với bundler Rolldown viết bằng Rust, đây là stack frontend nhanh và DX tốt nhất 2026 cho team .NET đang xây SPA hoặc SSR app.

17. Tài liệu tham khảo