jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Vue.js 3 · Phần 11 — Tối ưu Hiệu năng

Làm Vue 3 nhanh: hiểu khi nào component re-render, v-once và v-memo, shallowRef cho dữ liệu lớn, lazy-load component và route, ảo hóa danh sách dài, KeepAlive, và đọc bundle để cắt code thừa.

Vue nhanh sẵn nhờ reactivity fine-grained (Phần 3) — nó chỉ cập nhật đúng phần phụ thuộc, không diff cả cây như mặc định của React. Nhưng app lớn vẫn có thể chậm. Phần này là bộ công cụ tối ưu theo thứ tự tác động: từ đo đúng, đến render, đến bundle.


1. Đo trước, tối ưu sau

Đừng đoán. Dùng Vue DevTools (tab Performance + component inspector) và Chrome Performance panel để tìm component re-render nhiều hay render lâu. Quy tắc vàng: tối ưu cái đo được là nút thắt, không tối ưu phỏng đoán.

Reactivity của Vue đã rất hiệu quả — phần lớn “chậm” thật ra là: render danh sách quá dài, dữ liệu reactive quá sâu, hoặc bundle quá to. Ba thứ đó là trọng tâm bên dưới.


2. v-once & v-memo

v-once render một phần đúng một lần rồi không bao giờ cập nhật — cho nội dung tĩnh tốn kém:

<header v-once>
  <ExpensiveLogo />
  <h1>{{ siteTitle }}</h1>  <!-- chốt giá trị lần đầu -->
</header>

v-memo chỉ render lại khi mảng phụ thuộc đổi — như memo có điều kiện, đắc dụng trong v-for danh sách lớn:

<div
  v-for="item in list"
  :key="item.id"
  v-memo="[item.id, item.selected]"
>
  <!-- chỉ render lại dòng này khi id hoặc selected đổi -->
  <HeavyRow :item="item" />
</div>

Dùng v-memo có chọn lọc — đặt sai mảng phụ thuộc gây bug “UI không cập nhật”. Chỉ dùng khi đo thấy render danh sách là nút thắt.


3. shallowRef / shallowReactive cho dữ liệu lớn

Mặc định reactive là deep — Vue proxy đệ quy mọi tầng. Với dataset lớn (bảng nghìn dòng, kết quả GraphQL khổng lồ, instance thư viện), deep proxy tốn cả bộ nhớ lẫn CPU.

import { shallowRef, triggerRef } from 'vue';

const rows = shallowRef<Row[]>([]); // chỉ track .value, không proxy từng row

function load(data: Row[]) {
  rows.value = data;        // gán cả mảng → trigger (OK)
}
function mutateInPlace() {
  rows.value.push(newRow);  // KHÔNG trigger (shallow)
  triggerRef(rows);         // ép cập nhật thủ công
}

Quy tắc: dữ liệu chỉ thay nguyên cục (thay cả mảng) và không cần reactivity sâu → shallowRef. Đây là một trong những tối ưu tác động lớn nhất cho app data-heavy.


4. Lazy-load component & route

Không bắt người dùng tải code họ chưa cần. Route lazy-load (Phần 7) là quan trọng nhất; component nặng (modal, editor, chart) cũng nên defineAsyncComponent:

import { defineAsyncComponent } from 'vue';

const HeavyChart = defineAsyncComponent({
  loader: () => import('@/components/HeavyChart.vue'),
  loadingComponent: Spinner,
  delay: 200,        // chờ 200ms mới hiện spinner (tránh nhấp nháy)
  timeout: 10_000,
});

Vite tách mỗi import() động thành chunk riêng — chỉ tải khi component thực sự được render.


5. Ảo hóa danh sách dài

Render 10.000 <li> thật vào DOM là chí mạng dù Vue nhanh tới đâu — chi phí nằm ở chính DOM. Virtualization chỉ render những item đang trong viewport:

<script setup lang="ts">
import { useVirtualList } from '@vueuse/core';
const { list, containerProps, wrapperProps } = useVirtualList(items, { itemHeight: 40 });
</script>

<template>
  <div v-bind="containerProps" style="height: 400px">
    <div v-bind="wrapperProps">
      <div v-for="{ data, index } in list" :key="index" style="height: 40px">
        {{ data.name }}
      </div>
    </div>
  </div>
</template>

@vueuse/core (useVirtualList) hoặc vue-virtual-scroller cho danh sách/bảng cực lớn. Đây là khác biệt giữa “mượt” và “treo” khi dữ liệu lớn.


6. <KeepAlive> — cache component giữa các lần ẩn/hiện

Khi chuyển tab/route, component bị hủy và dựng lại — mất state và phải fetch lại. <KeepAlive> giữ instance trong bộ nhớ:

<KeepAlive :max="10">
  <component :is="currentTab" />
</KeepAlive>

<!-- với router -->
<RouterView v-slot="{ Component }">
  <KeepAlive>
    <component :is="Component" />
  </KeepAlive>
</RouterView>

Component được cache có hook onActivated/onDeactivated thay cho mount/unmount. :max giới hạn số instance cache để khỏi rò bộ nhớ.


7. Cắt bundle

  • Phân tích: rollup-plugin-visualizer (Vite) vẽ treemap bundle — tìm thư viện to bất ngờ.
  • Tree-shaking: import đúng thứ cần (import { debounce } from 'lodash-es' thay vì cả lodash).
  • Thư viện nặng: thay moment bằng date-fns/Temporal, cân nhắc chart lib nhẹ.
  • v-if cho khối nặng thay v-show để không dựng DOM khi chưa cần.
npm run build -- --report   # hoặc cấu hình visualizer trong vite.config.ts

8. Bài tập

1. Vì sao reactivity của Vue thường không phải nút thắt, và ba nút thắt thật là gì?

Lời giải

Vue cập nhật fine-grained, chỉ chạy effect phụ thuộc. Nút thắt thật thường là: danh sách DOM quá dài, reactive quá sâu trên dữ liệu lớn, và bundle quá to.

2. Khi nào shallowRef đáng dùng và rủi ro của nó là gì?

Lời giải

Khi dữ liệu lớn thay nguyên cục, không cần reactivity sâu. Rủi ro: mutate in-place không trigger; phải gán mới cả .value hoặc gọi triggerRef.

3. 50.000 dòng cần render — cách tiếp cận?

Lời giải

Virtualization (chỉ render item trong viewport) qua useVirtualList/vue-virtual-scroller, kèm shallowRef cho mảng dữ liệu. Không render hết vào DOM.


Điểm chính

  • Đo trước bằng Vue DevTools + Performance panel; tối ưu nút thắt thật.
  • v-once/v-memo cắt render thừa; dùng v-memo có chọn lọc.
  • shallowRef cho dữ liệu lớn thay nguyên cục — tối ưu lớn cho app data-heavy.
  • Lazy-load route & component nặng; ảo hóa danh sách dài; <KeepAlive> cache component.
  • Phân tích bundle, tree-shake, thay thư viện nặng.

Phần tiếp theo

Phần cuối gom mọi thứ lại. Phần 12 — Testing & Capstone dạy test với Vitest + Vue Test Utils (mount component, kích sự kiện, mock store/router, test composable), rồi dựng một feature hoàn chỉnh kết hợp toàn bộ series: router, Pinia, form validate, data fetching, và test bao phủ.