Giới thiệu
Trong phần trước, chúng ta đã tìm hiểu về cách tối ưu hiệu suất render trong React. Phần này sẽ tập trung vào Vue - một framework frontend phổ biến khác 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.set
hoặ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 -->
import { reactive } from 'vue' export const store = reactive({ user: { name:
'John Doe' } })
<!-- GrandChild.vue -->
<template>
<div>{{ store.user.name }}</div>
</template>
<script>
import { store } from "./store";
export default {
setup() {
return { store };
},
};
</script>
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>
Functional Components
Functional components là stateless và instanceless, giúp giảm overhead.
<!-- Functional component trong Vue 3 -->
<script setup>
// Không cần định nghĩa là functional, tất cả các components
// sử dụng <script setup> đều rất nhẹ
defineProps({
item: Object,
});
</script>
<template>
<div class="item">
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
</div>
</template>
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.js
Nuxt.js là framework dựa trên Vue cung cấp SSR, SSG và nhiều tính năng tối ưu hiệu suất khác.
SSR và SSG trong Nuxt
Server-side Rendering (SSR):
// nuxt.config.js
export default {
// Bật SSR (mặc định)
ssr: true,
};
Static Site Generation (SSG):
// nuxt.config.js
export default {
// Bật SSG
target: "static",
// Tạo các routes động
generate: {
async routes() {
const { data } = await axios.get("https://api.example.com/posts");
return data.map((post) => `/posts/${post.id}`);
},
},
};
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
Kết luận
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. Tuy nhiên, để đạt được hiệu suất tối đa, vẫn cần hiểu rõ cách Vue hoạt động và áp dụng các kỹ thuật tối ưu phù hợp.
Từ việc sử dụng v-once và v-memo để tối ưu render, đến lazy loading, keep-alive và virtual scrolling, Vue cung cấp nhiều công cụ để cải thiện trải nghiệm người dùng. Nuxt.js mở rộng khả năng này với SSR, SSG và nhiều tính năng tối ưu hiệu suất khác.
Trong phần tiếp theo, chúng ta sẽ khám phá các công cụ và kỹ thuật đo lường hiệu suất frontend, giúp bạn xác định và giải quyết các vấn đề hiệu suất trong ứng dụng của mình.