Vue: Proxy reactivity, v-memo, Nuxt 3 và Vapor Mode
Phần này tập trung vào Vue, framework frontend với cách tiếp cận riêng về hiệu suất và tối ưu hóa.
Vue.js nổi tiếng với hệ thống phản ứng (reactivity system) mạnh mẽ và hiệu quả, cho phép theo dõi chính xác các dependencies và chỉ cập nhật những phần thực sự cần thiết của DOM. Tuy nhiên, việc sử dụng Vue không đúng cách cũng có thể dẫn đến các vấn đề hiệu suất.
Trong bài viết này, chúng ta sẽ khám phá:
- Cơ chế render của Vue (Reactivity system, Virtual DOM)
- Các vấn đề hiệu suất phổ biến trong Vue
- Kỹ thuật tối ưu hiệu suất Vue
- Server-side Rendering với Nuxt.js
- So sánh hiệu suất giữa Vue và React
1. Cơ chế render của Vue
Để tối ưu hiệu suất Vue, trước tiên chúng ta cần hiểu cách Vue render UI.
Reactivity System
Hệ thống phản ứng (reactivity system) là trái tim của Vue, cho phép nó theo dõi các dependencies và cập nhật DOM một cách hiệu quả.
Vue 2 (Object.defineProperty):
Trong Vue 2, hệ thống phản ứng dựa trên Object.defineProperty để chặn các thao tác get/set trên các thuộc tính.
// Cách Vue 2 thực hiện reactivity (đơn giản hóa)
function defineReactive(obj, key, val) {
const dep = new Dep(); // Dependency tracking
Object.defineProperty(obj, key, {
get() {
// Theo dõi dependency khi thuộc tính được đọc
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
if (val === newVal) return;
val = newVal;
// Thông báo cho tất cả các dependencies khi giá trị thay đổi
dep.notify();
},
});
}
Hạn chế của Vue 2:
- Không thể phát hiện thêm/xóa thuộc tính (cần
Vue.sethoặcthis.$set) - Không thể phát hiện thay đổi trực tiếp trong mảng bằng index (cần các phương thức mảng như
push,splice)
Vue 3 (Proxy):
Vue 3 sử dụng Proxy để khắc phục các hạn chế của Vue 2, cung cấp khả năng theo dõi toàn diện hơn.
// Cách Vue 3 thực hiện reactivity (đơn giản hóa)
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
// Theo dõi dependency
track(target, key);
const value = Reflect.get(target, key, receiver);
// Nếu giá trị là object, trả về reactive version của nó
if (isObject(value)) {
return reactive(value);
}
return value;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
// Chỉ trigger update nếu giá trị thực sự thay đổi
if (hasChanged(value, oldValue)) {
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
// Có thể phát hiện xóa thuộc tính
const hadKey = hasOwn(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key);
}
return result;
},
});
}
Ưu điểm của Vue 3:
- Phát hiện thêm/xóa thuộc tính
- Phát hiện thay đổi trong mảng bằng index
- Phát hiện thay đổi trong Map, Set
- Hiệu suất tốt hơn
Virtual DOM và Render Pipeline
Giống như React, Vue cũng sử dụng Virtual DOM để tối ưu hóa cập nhật DOM.
Quá trình render trong Vue:
- Reactive Dependencies: Khi reactive state thay đổi, Vue đánh dấu các components bị ảnh hưởng
- Render Function: Component bị ảnh hưởng gọi render function để tạo Virtual DOM mới
- Virtual DOM Diffing: Vue so sánh Virtual DOM mới với phiên bản cũ
- Patch: Chỉ những thay đổi thực sự cần thiết được áp dụng vào DOM thật
// Component Vue đơn giản
const app = createApp({
data() {
return {
count: 0,
};
},
methods: {
increment() {
this.count++; // Trigger reactivity
// 1. Vue biết count đã thay đổi
// 2. Component re-render, tạo Virtual DOM mới
// 3. Vue so sánh với Virtual DOM cũ
// 4. Chỉ cập nhật phần DOM hiển thị count
},
},
template: `<div>Count: {{ count }}</div>`,
});
Compiler Optimizations
Vue có một số tối ưu hóa ở cấp độ compiler mà React không có.
Static Hoisting:
Vue tự động nâng các nội dung tĩnh ra khỏi render function để tránh tạo lại chúng mỗi lần render.
<template>
<div>
<h1>Tiêu đề tĩnh</h1>
<p>{{ dynamicContent }}</p>
</div>
</template>
Được biên dịch thành (đơn giản hóa):
const _hoisted_1 = /*#__PURE__*/ createElementVNode(
"h1",
null,
"Tiêu đề tĩnh",
-1
);
function render() {
return (
openBlock(),
createElementBlock("div", null, [
_hoisted_1, // Phần tĩnh được tái sử dụng
createElementVNode("p", null, toDisplayString(this.dynamicContent), 1),
])
);
}
Patch Flags:
Vue 3 thêm “patch flags” vào Virtual DOM nodes để chỉ ra loại cập nhật mà node có thể cần.
<template>
<div>
<span>Tĩnh</span>
<span>{{ dynamic }}</span>
<span :class="dynamicClass">Text</span>
</div>
</template>
Được biên dịch thành (đơn giản hóa):
function render() {
return (
openBlock(),
createElementBlock("div", null, [
createElementVNode("span", null, "Tĩnh"),
createElementVNode("span", null, toDisplayString(dynamic), 1 /* TEXT */),
createElementVNode(
"span",
{ class: dynamicClass },
"Text",
2 /* CLASS */
),
])
);
}
Patch flags (1: TEXT, 2: CLASS) giúp Vue biết chính xác phần nào cần cập nhật khi re-render.
2. Các vấn đề hiệu suất phổ biến trong Vue
Hiểu các vấn đề hiệu suất phổ biến sẽ giúp bạn tránh chúng trong ứng dụng Vue của mình.
Quá nhiều reactive data
Một trong những vấn đề phổ biến nhất là đặt quá nhiều dữ liệu vào reactive state.
<script>
export default {
data() {
return {
// Không tốt: Dữ liệu lớn không cần reactivity
hugeList: Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
})),
// Dữ liệu tĩnh không thay đổi
constants: { API_URL: "https://api.example.com", MAX_ITEMS: 100 },
};
},
};
</script>
Giải pháp:
<script>
// Dữ liệu tĩnh ở ngoài component
const API_CONSTANTS = { API_URL: "https://api.example.com", MAX_ITEMS: 100 };
export default {
data() {
return {
// Chỉ giữ ID trong reactive state
selectedIds: [],
};
},
created() {
// Lưu dữ liệu lớn không cần reactivity vào instance
this.hugeList = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
}));
},
computed: {
// Chỉ tạo reactive data khi cần
selectedItems() {
return this.selectedIds.map((id) =>
this.hugeList.find((item) => item.id === id)
);
},
},
};
</script>
Computed properties không tối ưu
Computed properties là một tính năng mạnh mẽ của Vue, nhưng nếu sử dụng không đúng cách có thể gây ra vấn đề hiệu suất.
<script>
export default {
data() {
return {
items: Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
})),
searchTerm: "",
};
},
computed: {
// Không tốt: Tính toán nặng mỗi khi component re-render
filteredItems() {
console.log("Filtering items..."); // Sẽ chạy quá nhiều lần
return this.items.filter((item) =>
item.name.toLowerCase().includes(this.searchTerm.toLowerCase())
);
},
},
};
</script>
Giải pháp:
<script>
import { debounce } from "lodash-es";
export default {
data() {
return {
items: Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
})),
searchTerm: "",
debouncedSearchTerm: "",
};
},
watch: {
// Debounce searchTerm để tránh tính toán quá nhiều lần
searchTerm: debounce(function (value) {
this.debouncedSearchTerm = value;
}, 300),
},
computed: {
// Chỉ tính toán lại khi debouncedSearchTerm thay đổi
filteredItems() {
console.log("Filtering items...");
return this.items.filter((item) =>
item.name.toLowerCase().includes(this.debouncedSearchTerm.toLowerCase())
);
},
},
};
</script>
Watchers không cần thiết
Watchers có thể gây ra vòng lặp cập nhật và tính toán không cần thiết.
<script>
export default {
data() {
return {
firstName: "",
lastName: "",
fullName: "",
};
},
// Không tốt: Sử dụng watchers khi computed property phù hợp hơn
watch: {
firstName(newVal) {
this.fullName = `${newVal} ${this.lastName}`;
},
lastName(newVal) {
this.fullName = `${this.firstName} ${newVal}`;
},
},
};
</script>
Giải pháp:
<script>
export default {
data() {
return {
firstName: "",
lastName: "",
};
},
// Tốt: Sử dụng computed property
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
},
},
};
</script>
Props drilling và quản lý state
Giống như React, Vue cũng gặp vấn đề với props drilling qua nhiều cấp component.
<!-- App.vue -->
<template>
<Parent :user="user" />
</template>
<!-- Parent.vue -->
<template>
<Child :user="user" />
</template>
<!-- Child.vue -->
<template>
<GrandChild :user="user" />
</template>
<!-- GrandChild.vue -->
<template>
<div>{{ user.name }}</div>
</template>
Giải pháp:
// store.js, provide/inject hoặc reactive module dùng chung
import { reactive } from "vue";
export const store = reactive({
user: { name: "John Doe" },
});
<!-- GrandChild.vue (script setup, cách viết 2025+) -->
<script setup>
import { store } from "./store";
</script>
<template>
<div>{{ store.user.name }}</div>
</template>
Gợi ý tốt hơn nữa: dùng Pinia cho state chia sẻ thực sự (TypeScript, DevTools, hydration SSR sẵn sàng) thay vì global reactive object.
3. Kỹ thuật tối ưu hiệu suất Vue
Sau khi hiểu các vấn đề, hãy xem các kỹ thuật để tối ưu hiệu suất Vue.
v-once và v-memo
Vue cung cấp các directives để tối ưu render.
v-once: Chỉ render element hoặc component một lần, sau đó bỏ qua tất cả các lần cập nhật.
<template>
<!-- Chỉ render một lần và cache lại -->
<div v-once>
<h1>{{ title }}</h1>
<expensive-component :data="staticData"></expensive-component>
</div>
<!-- Vẫn re-render khi data thay đổi -->
<div>
<p>{{ message }}</p>
</div>
</template>
v-memo: Memoise một phần của template, chỉ re-render khi dependencies thay đổi.
<template>
<div>
<!-- Chỉ re-render khi id thay đổi -->
<div v-memo="[item.id]">
<p>{{ item.name }}</p>
<p>{{ item.description }}</p>
<ExpensiveComponent :item="item" />
</div>
</div>
</template>
Lazy loading components
Lazy loading giúp giảm kích thước bundle ban đầu và tải components khi cần.
Với Vue Router:
// Thay vì import trực tiếp
import UserDashboard from "./components/UserDashboard.vue";
// Sử dụng dynamic import
const UserDashboard = () => import("./components/UserDashboard.vue");
const routes = [
{
path: "/dashboard",
component: UserDashboard,
},
];
Với Async Components:
<script>
import { defineAsyncComponent } from "vue";
export default {
components: {
// Lazy load component với loading và error states
HeavyComponent: defineAsyncComponent({
loader: () => import("./HeavyComponent.vue"),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000,
}),
},
};
</script>
Keep-alive
<keep-alive> giúp cache các component instances không hoạt động, tránh re-render không cần thiết.
<template>
<div>
<button @click="currentTab = 'Tab1'">Tab 1</button>
<button @click="currentTab = 'Tab2'">Tab 2</button>
<keep-alive>
<component :is="currentTab"></component>
</keep-alive>
</div>
</template>
<script>
import Tab1 from "./Tab1.vue";
import Tab2 from "./Tab2.vue";
export default {
components: { Tab1, Tab2 },
data() {
return {
currentTab: "Tab1",
};
},
};
</script>
Với include/exclude:
<template>
<!-- Chỉ cache Tab1 và Tab2 -->
<keep-alive include="Tab1,Tab2">
<component :is="currentTab"></component>
</keep-alive>
<!-- Cache tất cả trừ HeavyTab -->
<keep-alive exclude="HeavyTab">
<component :is="currentTab"></component>
</keep-alive>
</template>
Virtual Scrolling
Khi hiển thị danh sách dài, virtual scrolling giúp chỉ render các items đang trong viewport.
<template>
<RecycleScroller
class="scroller"
:items="items"
:item-size="32"
key-field="id"
v-slot="{ item }"
>
<div class="user-item">{{ item.name }}</div>
</RecycleScroller>
</template>
<script>
import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
export default {
components: { RecycleScroller },
data() {
return {
items: Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `User ${i}`,
})),
};
},
};
</script>
Component nhẹ với <script setup>
Trong Vue 3, khái niệm “functional component” của Vue 2 không còn là trụ chính; SFC với <script setup> đã tự compile ra code rất nhẹ và có performance tương đương. Chỉ dùng defineComponent({ functional: true }) (qua render function thuần) khi thực sự cần tuyệt đối không có instance (ví dụ component render nhiều triệu nhánh lá).
<!-- Component nhẹ, idiom 2025+ -->
<script setup>
defineProps({
item: Object,
});
</script>
<template>
<div class="item">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
</div>
</template>
Vue 3.4+ API đáng quan tâm cho performance:
defineModel(), thay thế boilerplatev-modelemit/props, ít code cần compile hơn.- Vapor Mode (experimental từ 3.5+): compile SFC thành code không dùng Virtual DOM, tương tự Solid/Svelte, hứa hẹn bundle nhỏ hơn và update nhanh hơn. Bật per-component qua compiler flag, không phải toàn app.
- Lưu ý: Reactivity Transform (từng có
ref: x = 1và$ref) đã bị rút lại và deprecated, không dùng nữa.
Sử dụng shallowRef và shallowReactive
Trong Vue 3, shallowRef và shallowReactive giúp giảm overhead của reactivity system.
<script setup>
import { shallowRef, shallowReactive } from "vue";
// Chỉ theo dõi thay đổi ở cấp cao nhất của object
const state = shallowReactive({
user: { name: "John", settings: { theme: "dark" } },
});
// Thay đổi này sẽ trigger update
state.user = { name: "Jane", settings: { theme: "light" } };
// Thay đổi này sẽ KHÔNG trigger update
state.user.settings.theme = "light";
// Tương tự với shallowRef
const data = shallowRef({ count: 0, items: [] });
// Chỉ trigger update khi thay đổi .value
data.value = { count: 1, items: [] };
// Thay đổi này sẽ KHÔNG trigger update
data.value.count = 2;
</script>
4. Server-side Rendering với Nuxt 3
Nuxt 3 (và Nuxt 4 RC, 2025) là framework dựa trên Vue cung cấp SSR, SSG, hybrid rendering và nhiều tính năng tối ưu hiệu suất. Cách cấu hình khác đáng kể với Nuxt 2, phần lớn docs cũ trên internet không còn áp dụng được.
Render mode trong Nuxt 3, routeRules (hybrid rendering)
Nuxt 3 chuyển từ “chọn target toàn app” sang cấu hình theo route bằng routeRules. Bạn có thể mix SSR, SSG, ISR, SWR, static prerender… cho các path khác nhau trong cùng một app:
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true, // SSR là mặc định
routeRules: {
// Landing page: prerender ở build time, phục vụ từ CDN
"/": { prerender: true },
// Trang marketing: cache 1 giờ ở edge (ISR)
"/blog/**": { isr: 3600 },
// SPA thuần cho admin
"/admin/**": { ssr: false },
// API nội bộ: không cache
"/api/**": { cors: true, headers: { "Cache-Control": "no-store" } },
// Stale-while-revalidate (SWR)
"/products/**": { swr: 600 },
},
});
Lấy data: useAsyncData và useFetch
Thay cho asyncData / fetch của Nuxt 2, Nuxt 3 có hai composable chính:
<script setup>
// useFetch: wrapper tiện lợi cho gọi API
const {
data: products,
pending,
error,
refresh,
} = await useFetch("/api/products", {
key: "products", // dedupe cache
transform: (list) => list.filter((p) => p.inStock),
lazy: true, // không block navigation
server: true, // chạy cả trên server (SSR)
});
// useAsyncData: linh hoạt hơn, tự viết fetcher
const { data: user } = await useAsyncData("user", () => $fetch("/api/me"));
</script>
Nitro, server engine của Nuxt 3
Nuxt 3 chạy trên Nitro, cho phép deploy cùng codebase sang nhiều target: Node, Vercel, Netlify, Cloudflare Workers, AWS Lambda, Deno Deploy… chỉ cần đổi preset:
NITRO_PRESET=cloudflare-pages npx nuxt build
Prerender động (SSG) với Nuxt 3
Thay cho generate.routes, dùng nitro.prerender hoặc route rule prerender: true:
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: true, // tự crawl link nội bộ
routes: ["/", "/sitemap.xml"], // route khởi đầu
},
},
});
Automatic Code Splitting
Nuxt tự động chia nhỏ code theo routes và components.
<template>
<div>
<!-- Components được tự động lazy-loaded -->
<LazyHeavyComponent v-if="showHeavy" />
<button @click="showHeavy = true">Load Heavy Component</button>
</div>
</template>
<script>
export default {
data() {
return {
showHeavy: false,
};
},
};
</script>
Image Optimization
Nuxt Image giúp tối ưu hình ảnh tự động.
<template>
<div>
<!-- Tự động tối ưu hình ảnh -->
<nuxt-img
src="/large-image.jpg"
width="300"
height="200"
format="webp"
loading="lazy"
placeholder
/>
</div>
</template>
5. So sánh hiệu năng giữa Vue và React
Cả Vue và React đều có điểm mạnh về hiệu suất, nhưng có những khác biệt quan trọng.
Hệ thống Reactivity
Vue:
- Hệ thống reactivity tự động và chi tiết
- Biết chính xác những gì thay đổi và cần cập nhật
- Ít re-render không cần thiết hơn
React:
- Sử dụng mô hình “pull” thay vì “push”
- Cần memo/useMemo/useCallback để tránh re-render
- Cần nhiều sự tham gia của developer hơn để tối ưu
Tối ưu hóa Compiler
Vue:
- Tối ưu hóa ở thời điểm biên dịch (static hoisting, patch flags)
- Template-based nên compiler có thể phân tích tĩnh
- Ít phụ thuộc vào runtime optimizations
React:
- JSX không cho phép nhiều tối ưu hóa ở thời điểm biên dịch
- Phụ thuộc nhiều vào runtime optimizations
- Cần developer tối ưu thủ công nhiều hơn
Bundle Size
Vue:
- Bundle size nhỏ hơn (khoảng 20KB gzipped cho Vue 3 core)
- Tree-shaking tốt, chỉ đưa vào những gì được sử dụng
React:
- Bundle size lớn hơn một chút (khoảng 40KB gzipped cho React + ReactDOM)
- Ít khả năng tree-shaking hơn
Hiệu suất render
Vue:
- Hiệu quả hơn với updates nhỏ và thường xuyên
- Template-based giúp tối ưu hóa render
- Ít re-render không cần thiết
React:
- Hiệu quả hơn với updates lớn và ít thường xuyên
- Concurrent Mode cho phép render có thể ngắt
- Cần nhiều tối ưu thủ công hơn
Vue reactivity tự động giảm gánh nặng tối ưu thủ công
Vue cung cấp một hệ thống reactivity mạnh mẽ và nhiều tối ưu hóa tự động, giúp giảm bớt công việc tối ưu hiệu suất cho developers.
Câu hỏi hay gặp
Vue tự động optimize nhiều hơn React, vậy có cần tối ưu thủ công không?
Trả lời: Vue compiler và Proxy-based reactivity tự track dependency chính xác hơn React (không cần memo/useMemo). Nhưng vẫn cần tối ưu thủ công khi: (1) danh sách dài không dùng virtual scrolling; (2) computed phụ thuộc deep object; (3) watch quá nhiều side effect.
shallowRef và ref khác gì?
Trả lời: ref theo dõi deep (mọi property của object bên trong). shallowRef chỉ theo dõi reference thay đổi (không deep track). Dùng shallowRef cho large object (1000+ properties, tree data) để tránh overhead của deep reactive proxy. Khi update: phải replace toàn bộ object.
Nuxt 3 ISR và SSG khác nhau sao?
Trả lời: SSG (prerender: true): generate HTML tại build-time, deploy static. ISR (isr: true trong routeRules): serve cached HTML, revalidate sau TTL. ISR tốt cho content thay đổi vài phút-giờ (blog, product); SSG cho content hiếm thay đổi (landing page, docs).
Bài tiếp theo: So sánh hiệu suất giữa React và Vue, benchmark, trade-offs, chọn framework theo dự án.