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:

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

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

Tài liệu tham khảo