Vue 3 Performance 2026 - Tối ưu rendering từ component đến bundle
Posted on: 4/21/2026 12:15:55 AM
Table of contents
- 1. Hiểu Vue 3 Rendering Pipeline — Bottleneck nằm ở đâu?
- 2. Vue 3.6 Vapor Mode — Loại bỏ Virtual DOM
- 3. Reactivity Fine-tuning — Giảm chi phí tracking
- 4. Template-level Optimization — v-once, v-memo và KeepAlive
- 5. Virtual Scrolling — Render hàng nghìn item mượt mà
- 6. Lazy Loading và Code Splitting
- 7. Bundle Optimization — Giảm kích thước gửi đến trình duyệt
- 8. Memory Management — Ngăn memory leak
- 9. SSR và SSG — Tối ưu cho First Contentful Paint
- 10. Performance Profiling — Đo trước khi tối ưu
- 11. Checklist tối ưu Vue 3 Production
- 12. Kết luận
Bạn đã bao giờ deploy một ứng dụng Vue 3 lên production rồi nhận ra trang chủ mất hơn 3 giây để interactive? Danh sách 10.000 item lag khi scroll? Bundle size phình lên 2MB dù chỉ dùng vài component? Đây là những vấn đề phổ biến mà hầu hết Vue developer đều gặp — và Vue 3.6 với Vapor Mode cùng hàng loạt kỹ thuật tối ưu sẽ giúp bạn giải quyết triệt để.
Bài viết này không dừng ở lý thuyết. Chúng ta sẽ đi sâu từ cách Vue render component, tại sao Virtual DOM có thể trở thành bottleneck, cho đến từng kỹ thuật cụ thể với code thực tế — giúp bạn biến ứng dụng Vue 3 từ "chạy được" thành "chạy nhanh".
1. Hiểu Vue 3 Rendering Pipeline — Bottleneck nằm ở đâu?
Trước khi tối ưu, cần hiểu Vue 3 render component như thế nào. Mỗi khi state thay đổi, Vue đi qua một pipeline gồm nhiều bước — và mỗi bước đều có chi phí.
graph LR
A[State Change] --> B[Trigger Reactivity]
B --> C[Re-run render function]
C --> D[Generate new VNode tree]
D --> E[Diff old vs new VNode]
E --> F[Patch DOM]
style A fill:#e94560,stroke:#fff,color:#fff
style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style F fill:#e94560,stroke:#fff,color:#fff
Pipeline rendering của Vue 3 — từ state change đến DOM update
Trong pipeline này, bước Generate VNode và Diff VNode là tốn kém nhất. Với component tree lớn (hàng nghìn node), việc tạo object VNode, so sánh từng property, rồi quyết định patch — tất cả đều tiêu tốn CPU và bộ nhớ.
Vue 3 đã tối ưu hơn Vue 2 rất nhiều nhờ các compiler hint như patch flags, static hoisting, và block tree. Nhưng bản chất Virtual DOM vẫn là trung gian — và Vapor Mode trong Vue 3.6 sẽ loại bỏ hoàn toàn lớp trung gian này.
2. Vue 3.6 Vapor Mode — Loại bỏ Virtual DOM
Vapor Mode là tính năng quan trọng nhất trong Vue 3.6 (hiện đang ở beta). Thay vì compile template thành render function trả về VNode, Vapor Mode compile trực tiếp thành các lệnh DOM imperative — tương tự cách Svelte và Solid.js hoạt động.
2.1 Cách Vapor Mode hoạt động
graph TB
subgraph Traditional["Traditional Mode"]
T1[Template] --> T2[Render Function]
T2 --> T3[VNode Tree]
T3 --> T4[Diff Algorithm]
T4 --> T5[DOM Patches]
end
subgraph Vapor["Vapor Mode"]
V1[Template] --> V2[Compiled DOM Operations]
V2 --> V3[Direct DOM Updates]
end
style T1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style T2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style T3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style T4 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style T5 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style V1 fill:#e94560,stroke:#fff,color:#fff
style V2 fill:#e94560,stroke:#fff,color:#fff
style V3 fill:#e94560,stroke:#fff,color:#fff
So sánh Traditional Mode và Vapor Mode — Vapor bỏ qua hoàn toàn VNode và Diff
Với Traditional Mode, một component đơn giản hiển thị biến count sẽ tạo VNode object, so sánh với VNode cũ, rồi mới cập nhật text node. Với Vapor Mode, compiler phân tích template tại build time, biết chính xác node nào phụ thuộc vào biến nào, và sinh ra code cập nhật trực tiếp — không qua trung gian.
2.2 Bật Vapor Mode
Vapor Mode hoạt động ở cấp component — bạn có thể mix component truyền thống và Vapor trong cùng một app:
<!-- CounterVapor.vue -->
<!-- Thêm attribute vapor để bật Vapor Mode cho component này -->
<script setup vapor>
import { ref } from 'vue/vapor'
const count = ref(0)
const increment = () => count.value++
</script>
<template>
<button @click="increment">Count: {{ count }}</button>
</template>
Cấu hình Vite plugin:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
features: {
vaporMode: true // Cho phép component có attribute "vapor"
}
})
]
})
Chiến lược áp dụng Vapor Mode
Không cần chuyển toàn bộ app sang Vapor Mode ngay. Bắt đầu với các leaf component render thường xuyên (list item, card, badge) — đây là nơi Vapor Mode mang lại hiệu quả cao nhất vì giảm overhead cho mỗi lần re-render. Component gốc (layout, page) ít re-render nên lợi ích không đáng kể.
2.3 Giới hạn hiện tại
Tính đến tháng 4/2026, Vapor Mode vẫn ở trạng thái beta (v3.6.0-beta). Một số lưu ý:
- Chưa hỗ trợ đầy đủ
<Transition>và<TransitionGroup> - Plugin của một số UI library (Vuetify, PrimeVue) có thể chưa tương thích
- DevTools profiling cho Vapor component đang được hoàn thiện
- Không nên dùng trong production cho đến khi stable release
3. Reactivity Fine-tuning — Giảm chi phí tracking
Hệ thống reactivity của Vue 3 dùng Proxy để track dependency. Mặc định, ref() và reactive() sẽ deep-track toàn bộ nested object. Với dataset lớn, chi phí tracking này trở nên đáng kể.
3.1 shallowRef và shallowReactive
import { shallowRef, triggerRef } from 'vue'
// ❌ Deep ref — track mọi property trong mỗi item
const items = ref(largeDataset) // 10.000 items, mỗi item 20 properties
// Vue tạo Proxy cho: items.value, items.value[0], items.value[0].name, ...
// ✅ Shallow ref — chỉ track items.value thay đổi
const items = shallowRef(largeDataset)
// Vue chỉ tạo Proxy cho items.value — KHÔNG đi sâu vào từng item
// Khi cần update:
items.value[0].name = 'Updated' // Không trigger re-render (vì shallow)
items.value = [...items.value] // Trigger re-render (thay đổi reference)
// Hoặc dùng triggerRef để force update mà không tạo array mới:
items.value[0].name = 'Updated'
triggerRef(items) // Force re-render
Khi nào dùng shallowRef?
Dùng shallowRef khi data có nhiều nested object mà bạn thường thay thế toàn bộ (fetch API mới, pagination). Không dùng khi bạn cần reactive cho từng field trong form binding — v-model sẽ không hoạt động với property con của shallowRef.
3.2 computed vs methods — Cache hay không cache?
// ❌ Method — gọi lại mỗi lần component re-render
const getFilteredItems = () => {
return items.value.filter(item => item.active)
}
// ✅ Computed — chỉ tính lại khi dependency thay đổi
const filteredItems = computed(() => {
return items.value.filter(item => item.active)
})
// Nếu items không đổi, computed trả về kết quả cached — KHÔNG filter lại
Với danh sách 10.000 item, sự khác biệt giữa method và computed có thể là hàng chục millisecond mỗi lần re-render — đặc biệt khi component cha re-render nhưng data con không đổi.
3.3 Tránh inline object trong template
<!-- ❌ Tạo object mới mỗi lần render → child component luôn re-render -->
<ChildComponent :style="{ color: 'red', fontSize: '14px' }" />
<ChildComponent :config="{ pageSize: 20, sortBy: 'name' }" />
<!-- ✅ Khai báo ngoài template hoặc dùng computed -->
<script setup>
const cardStyle = { color: 'red', fontSize: '14px' }
const tableConfig = computed(() => ({
pageSize: pageSize.value,
sortBy: sortField.value
}))
</script>
<ChildComponent :style="cardStyle" />
<ChildComponent :config="tableConfig" />
4. Template-level Optimization — v-once, v-memo và KeepAlive
4.1 v-once — Render một lần duy nhất
Dùng v-once cho subtree hoàn toàn tĩnh (header, footer, disclaimer text). Vue sẽ skip hoàn toàn subtree này trong mọi lần re-render sau đó.
<template>
<!-- Header tĩnh — không bao giờ thay đổi -->
<div v-once class="terms-of-service">
<h2>Điều khoản sử dụng</h2>
<p>Rất nhiều nội dung tĩnh ở đây...</p>
<!-- 50+ paragraphs -->
</div>
<!-- Phần dynamic -->
<UserDashboard :user="currentUser" />
</template>
4.2 v-memo — Memoize có điều kiện
v-memo cho phép bạn chỉ định dependency array — tương tự React.memo nhưng ở template level. Rất hữu ích cho list item trong v-for:
<template>
<div v-for="item in list" :key="item.id"
v-memo="[item.id === selectedId, item.name, item.status]">
<!-- Component phức tạp với nhiều child -->
<ItemCard :item="item" :selected="item.id === selectedId" />
<ItemActions :status="item.status" />
<ItemMetadata :item="item" />
</div>
</template>
Khi scroll qua danh sách 1.000 item mà chỉ thay đổi selectedId, Vue sẽ chỉ re-render 2 item (item cũ bỏ chọn + item mới được chọn) thay vì toàn bộ 1.000 item.
4.3 KeepAlive — Cache component instance
<template>
<!-- Cache tối đa 5 tab gần nhất -->
<KeepAlive :max="5" :include="['Dashboard', 'Settings', 'Profile']">
<component :is="currentTabComponent" />
</KeepAlive>
</template>
KeepAlive và Memory
Luôn đặt :max cho KeepAlive. Không giới hạn = giữ toàn bộ component instance trong memory → memory leak. Prop max dùng chiến lược LRU (Least Recently Used) — component ít dùng nhất sẽ bị destroy trước.
5. Virtual Scrolling — Render hàng nghìn item mượt mà
Khi danh sách có hàng nghìn item, render tất cả vào DOM là cách nhanh nhất để giết performance. Virtual scrolling chỉ render các item đang hiển thị trong viewport — giảm DOM node từ hàng nghìn xuống vài chục.
graph TB
subgraph Normal["Render thông thường"]
N1["10.000 DOM nodes"]
N2["Mỗi node = event listener + style calc"]
N3["Scroll lag 200-500ms"]
end
subgraph Virtual["Virtual Scrolling"]
V1["~30 DOM nodes visible"]
V2["Recycle khi scroll"]
V3["Scroll mượt 16ms/frame"]
end
style N1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style N2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style N3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style V1 fill:#e94560,stroke:#fff,color:#fff
style V2 fill:#e94560,stroke:#fff,color:#fff
style V3 fill:#e94560,stroke:#fff,color:#fff
So sánh số lượng DOM node giữa render thường và virtual scrolling
5.1 Triển khai với @tanstack/vue-virtual
<script setup lang="ts">
import { ref } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
const parentRef = ref<HTMLElement | null>(null)
// Dataset lớn — 50.000 items
const items = shallowRef(generateItems(50_000))
const virtualizer = useVirtualizer({
count: items.value.length,
getScrollElement: () => parentRef.value,
estimateSize: () => 60, // Chiều cao ước tính mỗi item (px)
overscan: 5, // Render thêm 5 item trên/dưới viewport để scroll mượt
})
</script>
<template>
<div ref="parentRef" style="height: 600px; overflow-y: auto;">
<div :style="{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }">
<div
v-for="row in virtualizer.getVirtualItems()"
:key="row.key"
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${row.size}px`,
transform: `translateY(${row.start}px)`
}"
>
<ItemRow :item="items[row.index]" />
</div>
</div>
</div>
</template>
| Library | Kích thước | Dynamic height | Grid support | Ghi chú |
|---|---|---|---|---|
| @tanstack/vue-virtual | ~5KB gzip | ✅ | ✅ | Headless, flexible, đang được maintain tích cực |
| vue-virtual-scroller | ~8KB gzip | ✅ | ❌ | API đơn giản, phù hợp list đơn giản |
| vue-virtual-scroll-grid | ~4KB gzip | ❌ | ✅ | Chuyên cho grid layout |
5.2 Kết hợp virtual scrolling với shallowRef
Đây là combo quan trọng nhất cho danh sách lớn:
// ❌ Deep reactive + no virtual scroll = thảm họa
const items = ref(hugeArray) // Proxy cho 50.000 objects
// ✅ Shallow ref + virtual scroll = tối ưu
const items = shallowRef(hugeArray) // Không deep proxy
// + virtual scroll chỉ render 20-30 item visible
// Khi cập nhật 1 item:
const updateItem = (index: number, newData: Partial<Item>) => {
const updated = [...items.value]
updated[index] = { ...updated[index], ...newData }
items.value = updated // Trigger re-render, virtual scroll chỉ render visible items
}
6. Lazy Loading và Code Splitting
Bundle size ảnh hưởng trực tiếp đến Time to Interactive (TTI). Với Vue 3 + Vite, code splitting gần như miễn phí — chỉ cần biết đặt dynamic import đúng chỗ.
6.1 Route-level code splitting
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
// ✅ Lazy load — tạo chunk riêng cho mỗi route
component: () => import('@/pages/HomePage.vue')
},
{
path: '/dashboard',
component: () => import('@/pages/DashboardPage.vue'),
children: [
{
path: 'analytics',
// Chunk riêng cho sub-route
component: () => import('@/pages/dashboard/AnalyticsPage.vue')
},
{
path: 'settings',
component: () => import('@/pages/dashboard/SettingsPage.vue')
}
]
},
{
path: '/admin',
// Chunk chung cho nhóm admin pages
component: () => import(
/* webpackChunkName: "admin" */
'@/pages/AdminLayout.vue'
)
}
]
})
6.2 Component-level lazy loading với defineAsyncComponent
import { defineAsyncComponent } from 'vue'
// Component nặng — chỉ load khi cần
const HeavyChart = defineAsyncComponent({
loader: () => import('@/components/HeavyChart.vue'),
loadingComponent: ChartSkeleton, // Hiển thị skeleton khi đang load
delay: 200, // Chờ 200ms trước khi show loading
timeout: 10000, // Timeout sau 10s
errorComponent: ChartError, // Component hiện khi load lỗi
})
// Kết hợp với v-if — chỉ load khi user thực sự cần
const showChart = ref(false)
<template>
<button @click="showChart = true">Xem biểu đồ</button>
<!-- HeavyChart.vue chỉ được download khi showChart = true -->
<HeavyChart v-if="showChart" :data="chartData" />
</template>
6.3 Prefetch và Preload strategies
// Prefetch khi user hover — trải nghiệm mượt mà
const prefetchDashboard = () => {
import('@/pages/DashboardPage.vue')
}
// Trong template
// <router-link to="/dashboard" @mouseenter="prefetchDashboard">
// Dashboard
// </router-link>
// Hoặc dùng router.beforeResolve để prefetch tự động
router.beforeResolve((to) => {
const matched = to.matched
// Vite tự handle prefetch cho dynamic imports
})
Prefetch với Speculation Rules API
Nếu đã dùng Speculation Rules API cho navigation (Chrome 121+), Vue Router sẽ tận dụng prerender của trình duyệt — trang đích được render sẵn trước khi user click. Kết hợp với route-level code splitting, đây là combo mạnh nhất cho perceived performance.
7. Bundle Optimization — Giảm kích thước gửi đến trình duyệt
graph TD
A[Source Code] --> B[Vite Build]
B --> C[Tree Shaking]
C --> D[Code Splitting]
D --> E[Minification]
E --> F[Compression]
C -->|Loại bỏ dead code| G["~30-50% giảm"]
D -->|Chunk theo route| H["Load on demand"]
E -->|Terser/esbuild| I["~20-30% giảm"]
F -->|Brotli/Gzip| J["~70-80% giảm"]
style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style B fill:#e94560,stroke:#fff,color:#fff
style C fill:#2c3e50,stroke:#fff,color:#fff
style D fill:#2c3e50,stroke:#fff,color:#fff
style E fill:#2c3e50,stroke:#fff,color:#fff
style F fill:#2c3e50,stroke:#fff,color:#fff
style G fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style H fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style I fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
style J fill:#f8f9fa,stroke:#4CAF50,color:#2c3e50
Pipeline tối ưu bundle — mỗi bước giảm đáng kể kích thước output
7.1 Phân tích bundle với rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
visualizer({
open: true,
filename: 'stats.html',
gzipSize: true,
brotliSize: true,
})
]
})
Sau khi build, mở file stats.html — đây là bản đồ treemap giúp bạn thấy ngay thư viện nào chiếm nhiều dung lượng nhất. Thường thấy các "tội đồ": lodash (import cả library thay vì từng function), moment.js (thay bằng dayjs), icon library (import toàn bộ thay vì từng icon).
7.2 Import đúng cách — Tree Shaking hiệu quả
// ❌ Import toàn bộ — không tree-shake được
import _ from 'lodash'
import * as Icons from '@heroicons/vue/24/solid'
// ✅ Import từng function/component
import debounce from 'lodash-es/debounce'
import { ArrowLeftIcon, CheckIcon } from '@heroicons/vue/24/solid'
// ❌ UI Library — import toàn bộ
import ElementPlus from 'element-plus'
app.use(ElementPlus) // Bundle thêm ~500KB
// ✅ Import theo component (auto-import plugin)
// unplugin-vue-components + unplugin-auto-import
import { ElButton, ElInput, ElTable } from 'element-plus'
7.3 Cấu hình chunk strategy
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Tách vendor chunks — cached lâu dài
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'],
'chart-vendor': ['echarts'],
}
}
},
// Target modern browsers — nhỏ hơn, nhanh hơn
target: 'es2022',
// Chunk size warning
chunkSizeWarningLimit: 500,
}
})
| Chiến lược | Trước | Sau | Tác động |
|---|---|---|---|
| Tree shaking lodash → lodash-es | 71KB | 4KB | -94% |
| moment.js → dayjs | 67KB | 2.9KB | -96% |
| Auto-import UI components | ~500KB | ~80KB | -84% |
| Tách vendor chunks | 1 file 1.2MB | 5 files, cached | Faster subsequent loads |
| Brotli compression | 300KB gzip | 240KB brotli | -20% |
8. Memory Management — Ngăn memory leak
Memory leak trong Vue app thường âm thầm — ứng dụng chạy tốt ban đầu nhưng chậm dần sau vài phút sử dụng. Nguyên nhân phổ biến nhất:
8.1 Dọn dẹp side effects trong onUnmounted
// ❌ Quên cleanup — event listener tồn tại mãi
onMounted(() => {
window.addEventListener('resize', handleResize)
const timer = setInterval(fetchData, 5000)
})
// ✅ Cleanup đúng cách
onMounted(() => {
window.addEventListener('resize', handleResize)
const timer = setInterval(fetchData, 5000)
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
clearInterval(timer)
})
})
8.2 AbortController cho async operations
// ✅ Cancel pending requests khi component unmount
const fetchUserData = () => {
const controller = new AbortController()
onUnmounted(() => controller.abort())
return fetch('/api/users', { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name !== 'AbortError') throw err
})
}
8.3 Watch cleanup
// ✅ onCleanup trong watch — dọn dẹp effect cũ trước khi chạy effect mới
watch(searchQuery, (query, _, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(data => results.value = data)
})
Phát hiện memory leak
Dùng Chrome DevTools → Memory → Heap Snapshot. Chụp snapshot trước và sau khi navigate đi rồi quay lại một trang. So sánh 2 snapshot — nếu có object tăng lên mà không giảm, đó là memory leak. Đặc biệt chú ý Detached HTMLElement — DOM node đã bị remove nhưng vẫn bị reference giữ lại.
9. SSR và SSG — Tối ưu cho First Contentful Paint
Client-side rendering (CSR) phải đợi JavaScript download + parse + execute mới hiển thị nội dung. SSR và SSG giải quyết điều này bằng cách render HTML sẵn trên server.
graph LR
subgraph CSR["CSR (Client-Side)"]
C1[Request] --> C2[Blank HTML]
C2 --> C3[Download JS ~1-2s]
C3 --> C4[Parse + Execute]
C4 --> C5[Render content]
end
subgraph SSR["SSR (Server-Side)"]
S1[Request] --> S2[Full HTML ready]
S2 --> S3[Display immediately]
S3 --> S4[Hydrate JS async]
end
style C1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C4 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C5 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style S1 fill:#e94560,stroke:#fff,color:#fff
style S2 fill:#e94560,stroke:#fff,color:#fff
style S3 fill:#e94560,stroke:#fff,color:#fff
style S4 fill:#e94560,stroke:#fff,color:#fff
So sánh timeline CSR vs SSR — SSR hiển thị nội dung ngay lập tức
9.1 Nuxt 4 Hybrid Rendering
Nuxt 4 cho phép cấu hình rendering strategy per-route:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Trang chủ — SSG (prerender tại build time)
'/': { prerender: true },
// Blog posts — ISR (regenerate mỗi 3600s)
'/blog/**': { isr: 3600 },
// Dashboard — CSR only (cần auth, data realtime)
'/dashboard/**': { ssr: false },
// API routes — cache 60s tại CDN edge
'/api/**': { cache: { maxAge: 60 } },
}
})
9.2 Selective Hydration
Không phải mọi component đều cần interactive ngay. Dùng LazyHydration để trì hoãn hydration cho component dưới fold:
<template>
<!-- Hydrate ngay — above the fold -->
<HeroBanner />
<!-- Hydrate khi visible trong viewport -->
<LazyHydrationOnVisible>
<FeatureShowcase />
</LazyHydrationOnVisible>
<!-- Hydrate khi user interact (click, hover) -->
<LazyHydrationOnInteraction :triggers="['click', 'mouseover']">
<CommentSection />
</LazyHydrationOnInteraction>
<!-- Hydrate khi idle — lowest priority -->
<LazyHydrationWhenIdle>
<Footer />
</LazyHydrationWhenIdle>
</template>
10. Performance Profiling — Đo trước khi tối ưu
Tối ưu mà không đo lường chẳng khác gì chữa bệnh mà không chẩn đoán. Vue DevTools cung cấp profiler chi tiết cho component rendering.
10.1 Vue DevTools Performance Tab
Mở Vue DevTools → Performance → bấm Record → tương tác với app → Stop. Bạn sẽ thấy:
- Component render time — component nào tốn thời gian nhất?
- Re-render count — component nào re-render không cần thiết?
- Event timeline — thứ tự các event trigger re-render
10.2 Lighthouse và Core Web Vitals
| Metric | Mục tiêu | Kỹ thuật tối ưu |
|---|---|---|
| LCP (Largest Contentful Paint) | < 2.5s | SSR/SSG, preload critical resources, optimize images |
| INP (Interaction to Next Paint) | < 200ms | v-memo, virtual scrolling, debounce handlers, Vapor Mode |
| CLS (Cumulative Layout Shift) | < 0.1 | Skeleton loading, fixed dimensions, font preload |
| TTFB (Time to First Byte) | < 800ms | Edge caching, CDN, SSR streaming |
10.3 Custom performance markers
// Đo thời gian render component cụ thể
import { onMounted, onBeforeMount } from 'vue'
const useRenderPerf = (componentName: string) => {
onBeforeMount(() => {
performance.mark(`${componentName}-start`)
})
onMounted(() => {
performance.mark(`${componentName}-end`)
performance.measure(
`${componentName} render`,
`${componentName}-start`,
`${componentName}-end`
)
const measure = performance.getEntriesByName(`${componentName} render`)[0]
if (measure.duration > 16) {
console.warn(`⚠️ ${componentName} render took ${measure.duration.toFixed(1)}ms`)
}
})
}
11. Checklist tối ưu Vue 3 Production
Tổng hợp toàn bộ kỹ thuật đã trình bày, dưới đây là checklist theo thứ tự ưu tiên — bắt đầu từ những thay đổi có impact lớn nhất và dễ áp dụng nhất:
12. Kết luận
Performance không phải là tính năng thêm vào cuối dự án — nó là mindset xuyên suốt quá trình phát triển. Vue 3 đã cung cấp đầy đủ công cụ từ compiler-level (Vapor Mode, patch flags) đến runtime-level (shallowRef, v-memo, KeepAlive) để xây dựng ứng dụng nhanh.
Quan trọng nhất: đo trước, tối ưu sau. Không phải mọi ứng dụng đều cần virtual scrolling hay Vapor Mode. Dùng DevTools profiler và Lighthouse để tìm bottleneck thực tế, rồi áp dụng kỹ thuật phù hợp. Một shallowRef đặt đúng chỗ có thể hiệu quả hơn việc refactor cả component tree.
Theo dõi Vue 3.6 Stable
Vue 3.6 với Vapor Mode đang trong giai đoạn beta cuối. Khi stable release (dự kiến Q2-Q3 2026), đây sẽ là bước nhảy lớn nhất về performance cho Vue ecosystem — ngang hàng với Svelte 5 và Solid.js mà không cần thay đổi mental model. Theo dõi Vue.js Core GitHub để cập nhật.
Nguồn tham khảo
View Transitions API — Chuyển Trang Mượt Như Native App Không Cần Framework
AI Agent Orchestration — 6 Pattern điều phối Agent trong Production 2026
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.