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:

  1. Không thể phát hiện thêm/xóa thuộc tính (cần Vue.set hoặc this.$set)
  2. 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:

  1. Phát hiện thêm/xóa thuộc tính
  2. Phát hiện thay đổi trong mảng bằng index
  3. Phát hiện thay đổi trong Map, Set
  4. 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:

  1. Reactive Dependencies: Khi reactive state thay đổi, Vue đánh dấu các components bị ảnh hưởng
  2. Render Function: Component bị ảnh hưởng gọi render function để tạo Virtual DOM mới
  3. Virtual DOM Diffing: Vue so sánh Virtual DOM mới với phiên bản cũ
  4. 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  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  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ế boilerplate v-model emit/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 = 1$ref) đã bị rút lại và deprecated, không dùng nữa.

Sử dụng shallowRef và shallowReactive

Trong Vue 3, shallowRefshallowReactive 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: useAsyncDatauseFetch

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.

shallowRefref 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.

Tài liệu tham khảo