Chào mừng bạn đến với Cafedev! Trong thế giới lập trình web ngày nay, hiệu suất luôn là một yếu tố quan trọng mà các nhà phát triển không thể bỏ qua. Trong bài viết này, chúng ta sẽ cùng khám phá về cách tối ưu hiệu suất trong ứng dụng VueJS. Từ cách tối ưu hóa các vòng lặp đến việc giảm thiểu các render không cần thiết, chúng ta sẽ tìm hiểu các chiến lược và kỹ thuật để tăng cường hiệu suất cho ứng dụng VueJS của bạn trên Cafedev. Hãy bắt đầu khám phá ngay!

Hiệu suất

1.Tổng quan

Vue được thiết kế để đạt hiệu suất tốt trong hầu hết các trường hợp sử dụng thông thường mà không cần nhiều tinh chỉnh thủ công. Tuy nhiên, luôn có những tình huống thách thức nơi cần phải tinh chỉnh cẩn thận hơn. Trong phần này, chúng ta sẽ thảo luận về những điều bạn nên chú ý khi nói đến hiệu suất trong một ứng dụng Vue.

Trước tiên, hãy thảo luận về hai khía cạnh chính của hiệu suất web:

  • Hiệu suất Tải trang: tốc độ mà ứng dụng hiển thị nội dung và trở nên tương tác trong lần truy cập ban đầu. Thông thường, điều này được đo bằng các chỉ số quan trọng của web như Largest Contentful Paint (LCP)First Input Delay (FID).
  • Hiệu suất Cập nhật: tốc độ mà ứng dụng cập nhật phản hồi cho đầu vào của người dùng. Ví dụ, tốc độ cập nhật của một danh sách khi người dùng gõ vào ô tìm kiếm, hoặc tốc độ mà trang chuyển khi người dùng nhấp vào một liên kết điều hướng trong ứng dụng Một Trang (SPA).
    Mặc dù sẽ lý tưởng nếu tối đa hóa cả hai, các kiến ​​trúc frontend khác nhau thường ảnh hưởng đến việc đạt được hiệu suất mong muốn trong các khía cạnh này một cách dễ dàng. Ngoài ra, loại ứng dụng bạn đang xây dựng đồng thời ảnh hưởng đến điều gì bạn nên ưu tiên về hiệu suất. Do đó, bước đầu tiên để đảm bảo hiệu suất tối ưu là chọn đúng kiến ​​trúc cho loại ứng dụng bạn đang xây dựng:
  • Tham khảo Cách Sử dụng Vue để xem làm thế nào bạn có thể tận dụng Vue theo các cách khác nhau.
  • Jason Miller thảo luận về các loại ứng dụng web và cách triển khai / phân phối lý tưởng tương ứng trong Các Loại Ứng Dụng Holotypes.

2. Các tùy chọn lập hồ sơ(Profiling)

Để cải thiện hiệu suất, chúng ta cần biết cách đo lường nó trước tiên. Có một số công cụ tuyệt vời có thể hỗ trợ trong việc này:
Đối với việc lập hồ sơ hiệu suất tải của các triển khai sản xuất(production deployments), chúng ta dùng:

Đối với việc lập hồ sơ hiệu suất trong quá trình phát triển cục bộ, chúng ta dùng:

3. Tối ưu Hóa Tải Trang

Có nhiều khía cạnh không phụ thuộc vào framework để tối ưu hiệu suất tải trang – hãy xem hướng dẫn web.dev này để có một tổng kết toàn diện. Ở đây, chúng ta sẽ tập trung chủ yếu vào các kỹ thuật cụ thể cho Vue.

3.1 Lựa chọn Kiến trúc Đúng

Nếu trường hợp sử dụng của bạn nhạy cảm với hiệu suất tải trang, tránh giao hàng dưới dạng SPA chỉ trên phía client. Bạn muốn máy chủ của mình trực tiếp gửi HTML chứa nội dung mà người dùng muốn xem. Việc chỉ hiển thị trên phía client dẫn đến thời gian hiển thị nội dung chậm. Điều này có thể được giảm bớt bằng cách sử dụng Server-Side Rendering (SSR) hoặc Static Site Generation (SSG). Hãy xem Hướng dẫn SSR để tìm hiểu về thực hiện SSR với Vue. Nếu ứng dụng của bạn không có yêu cầu tương tác phức tạp, bạn cũng có thể sử dụng một máy chủ backend truyền thống để hiển thị HTML và cải thiện nó với Vue trên client.

Nếu ứng dụng chính của bạn phải là một SPA, nhưng có trang marketing (trang chào mừng, giới thiệu, blog), hãy giao hàng chúng một cách riêng biệt! Trang marketing của bạn nên được triển khai dưới dạng HTML tĩnh với JS tối thiểu, bằng cách sử dụng SSG.

3.2 Kích thước Bundle và Tree-shaking

Một trong những cách hiệu quả nhất để cải thiện hiệu suất tải trang là giao hàng các bản bundle JavaScript nhỏ hơn. Dưới đây là một số cách để giảm kích thước bundle khi sử dụng Vue:

Sử dụng bước xây dựng nếu có thể.

  • Nhiều trong số các API của Vue là “có thể tách ra” nếu được gói thông qua một công cụ xây dựng hiện đại. Ví dụ, nếu bạn không sử dụng thành phần tích hợp sẵn, nó sẽ không được bao gồm trong bản bundle sản xuất cuối cùng. Tree-shaking cũng có thể loại bỏ các module không sử dụng khác trong mã nguồn của bạn.
  • Khi sử dụng bước xây dựng, các mẫu được biên dịch trước nên chúng ta không cần gửi trình biên dịch Vue đến trình duyệt. Điều này tiết kiệm 14kb JavaScript sau khi đã nén và tránh chi phí biên dịch thời gian chạy.
  • Hãy cẩn thận về kích thước khi giới thiệu các phụ thuộc mới! Trong các ứng dụng thực tế, các bản bundle phình to thường là kết quả của việc giới thiệu các phụ thuộc nặng mà không nhận ra điều đó.
  • Nếu sử dụng bước xây dựng, ưu tiên các phụ thuộc cung cấp định dạng ES module và thân thiện với tree-shaking. Ví dụ, ưu tiên lodash-es hơn lodash.
  • Kiểm tra kích thước của một phụ thuộc và đánh giá xem nó có đáng giá cho chức năng mà nó cung cấp không. Lưu ý nếu phụ thuộc thân thiện với tree-shaking, sự tăng kích thước thực sự sẽ phụ thuộc vào các API bạn thực sự nhập từ nó. Công cụ như bundlejs.com có thể được sử dụng cho các kiểm tra nhanh, nhưng đo lường với cấu hình xây dựng thực tế của bạn sẽ luôn là chính xác nhất.
  • Nếu bạn chủ yếu sử dụng Vue cho cải thiện dần dần và muốn tránh bước xây dựng, xem xét sử dụng petite-vue (chỉ 6kb).

3.3 Tách mã

Tách mã là quá trình mà công cụ xây dựng chia nhỏ bản bundle ứng dụng thành nhiều phần nhỏ hơn, có thể được tải khi cần hoặc song song. Với việc tách mã đúng đắn, các tính năng cần thiết khi tải trang có thể được tải ngay lập tức, với các phần mở rộng được tải lười biếng chỉ khi cần, từ đó cải thiện hiệu suất.

Các công cụ gói như Rollup (mà Vite dựa trên) hoặc webpack có thể tự động tạo các phần mở rộng bằng cách phát hiện cú pháp nhập động ESM:

// lazy.js and its dependencies will be split into a separate chunk
// and only loaded when `loadLazy()` is called.
function loadLazy() {
  return import('./lazy.js')
}

Lazy loading thường được sử dụng cho các tính năng không cần thiết ngay sau khi tải trang ban đầu. Trong các ứng dụng Vue, điều này có thể được kết hợp với tính năng Component Async của Vue để tạo ra các phần mở rộng được chia nhỏ cho cây thành phần:

import { defineAsyncComponent } from 'vue'

// a separate chunk is created for Foo.vue and its dependencies.
// it is only fetched on demand when the async component is
// rendered on the page.
const Foo = defineAsyncComponent(() => import('./Foo.vue'))

Đối với các ứng dụng sử dụng Vue Router, nên sử dụng lazy loading cho các thành phần tuyến đường. Vue Router hỗ trợ rõ ràng cho lazy loading, riêng biệt với defineAsyncComponent. Xem Lazy Loading Routes để biết thêm chi tiết.

4. Tối ưu Cập nhật

4.1 Sự Ổn định của Props

Trong Vue, một thành phần con chỉ cập nhật khi ít nhất một trong số các props nhận được của nó đã thay đổi. Xem xét ví dụ sau:

<ListItem
  v-for="item in list"
  :id="item.id"
  :active-id="activeId" />

Bên trong thành phần , nó sử dụng các props idactiveId của mình để xác định xem nó có phải là mục hiện tại đang hoạt động không. Mặc dù điều này hoạt động, vấn đề là mỗi khi activeId thay đổi, mọi trong danh sách phải cập nhật!

Lý tưởng nhất, chỉ các mục có trạng thái hoạt động thay đổi mới cần cập nhật. Chúng ta có thể đạt được điều đó bằng cách di chuyển tính toán trạng thái hoạt động vào phần tử cha và làm cho chấp nhận trực tiếp một prop active :

<ListItem
  v-for="item in list"
  :id="item.id"
  :active="item.id === activeId" />

Bây giờ, đối với hầu hết các thành phần, prop active sẽ giữ nguyên khi activeId thay đổi, vì vậy chúng không còn cần phải cập nhật nữa. Nói chung, ý tưởng là giữ các props được chuyển đến các thành phần con càng ổn định càng tốt.

4.2 v-once

v-once là một chỉ thị tích hợp sẵn có thể được sử dụng để hiển thị nội dung dựa trên dữ liệu thời gian chạy nhưng không bao giờ cần phải cập nhật. Toàn bộ cây con nó được sử dụng trên sẽ được bỏ qua cho tất cả các cập nhật trong tương lai. Xem Tài liệu API của nó để biết thêm chi tiết.

4.3v-memo

v-memo là một chỉ thị tích hợp sẵn có thể được sử dụng để bỏ qua cập nhật của các cây con lớn hoặc các danh sách v-for theo điều kiện. Xem Tài liệu API của nó để biết thêm chi tiết.

4.4 Sự Ổn định của Computed

Bắt đầu từ phiên bản 3.4, một thuộc tính computed chỉ kích hoạt hiệu ứng khi giá trị computed đã thay đổi so với giá trị trước đó. Ví dụ, computed isEven sau chỉ kích hoạt hiệu ứng nếu giá trị trả về đã thay đổi từ true thành false, hoặc ngược lại:

const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)

watchEffect(() => console.log(isEven.value)) // true

// will not trigger new logs because the computed value stays `true`
count.value = 2
count.value = 4

Điều này giảm thiểu các kích hoạt hiệu ứng không cần thiết, nhưng không may thay, không hoạt động nếu computed tạo một đối tượng mới trong mỗi lần tính toán:

const computedObj = computed(() => {
  return {
    isEven: count.value % 2 === 0
  }
})

Bởi vì một đối tượng mới được tạo ra mỗi lần, giá trị mới kỹ thuật là luôn khác với giá trị cũ. Ngay cả khi thuộc tính isEven vẫn giữ nguyên, Vue sẽ không thể biết trừ khi nó thực hiện so sánh sâu của giá trị cũ và giá trị mới. So sánh như vậy có thể tốn kém và có thể không đáng giá.

Thay vào đó, chúng ta có thể tối ưu hóa điều này bằng cách so sánh giá trị mới với giá trị cũ và điều kiện trả về giá trị cũ nếu chúng ta biết không có gì thay đổi:

const computedObj = computed((oldValue) => {
  const newValue = {
    isEven: count.value % 2 === 0
  }
  if (oldValue && oldValue.isEven === newValue.isEven) {
    return oldValue
  }
  return newValue
})

Run Ví dụ
Lưu ý rằng bạn nên luôn thực hiện tính toán đầy đủ trước khi so sánh và trả về giá trị cũ, để các phụ thuộc cùng được thu thập trong mỗi lần chạy.

5. Tối ưu Hóa Tổng Quát

Những mẹo sau ảnh hưởng đến cả hiệu suất tải trang và cập nhật.

5.1 Ảo hóa Danh sách Lớn

Một trong những vấn đề hiệu suất phổ biến nhất trong tất cả các ứng dụng frontend là hiển thị danh sách lớn. Bất kể một framework có hiệu suất ra sao, việc hiển thị một danh sách với hàng nghìn mục sẽ chậm do số lượng nút DOM mà trình duyệt cần xử lý.

Tuy nhiên, chúng ta không nhất thiết phải hiển thị tất cả các nút này từ đầu. Trong hầu hết các trường hợp, màn hình của người dùng chỉ có thể hiển thị một phần nhỏ của danh sách lớn của chúng ta. Chúng ta có thể cải thiện đáng kể hiệu suất với ảo hóa danh sách, kỹ thuật chỉ hiển thị các mục hiện tại hoặc gần với viewport trong một danh sách lớn.
Việc triển khai ảo hóa danh sách không dễ dàng, may mắn thay có các thư viện cộng đồng hiện có mà bạn có thể sử dụng trực tiếp:

5.2 Giảm Overhead Reactivity cho Cấu trúc Bất biến Lớn

Hệ thống reactivity của Vue là sâu theo mặc định. Mặc dù điều này làm cho quản lý trạng thái trở nên trực quan, nhưng nó tạo ra một mức độ overhead nhất định khi kích thước dữ liệu lớn, vì mỗi lần truy cập thuộc tính đều kích hoạt các trap proxy thực hiện theo dõi phụ thuộc. Điều này thường trở nên rõ ràng khi xử lý các mảng lớn của các đối tượng lồng nhau sâu, nơi một lần render cần truy cập hơn 100,000 thuộc tính, vì vậy nó chỉ nên ảnh hưởng đến các trường hợp sử dụng rất cụ thể.

Vue cung cấp một cách thoát ra khỏi reactivity sâu bằng cách sử dụng shallowRef()shallowReactive(). Các API shallow tạo ra trạng thái chỉ phản ứng ở mức gốc và hiển thị tất cả các đối tượng lồng nhau không chạm. Điều này giữ cho việc truy cập thuộc tính lồng nhau nhanh chóng, với sự đánh đổi là chúng ta bây giờ phải xem xét tất cả các đối tượng lồng nhau như bất biến và các cập nhật chỉ có thể được kích hoạt bằng cách thay thế trạng thái gốc:

const shallowArray = shallowRef([
  /* big list of deep objects */
])

// this won't trigger updates...
shallowArray.value.push(newObject)
// this does:
shallowArray.value = [...shallowArray.value, newObject]

// this won't trigger updates...
shallowArray.value[0].foo = 1
// this does:
shallowArray.value = [
  {
    ...shallowArray.value[0],
    foo: 1
  },
  ...shallowArray.value.slice(1)
]

5.3 Tránh Sử dụng Các Trừu tượng Component Không Cần Thiết

Đôi khi chúng ta có thể tạo ra các component không hiển thị hoặc các component cao cấp (tức là các component hiển thị các component khác với các prop bổ sung) để có tính trừu tượng hoặc tổ chức mã tốt hơn. Mặc dù không có gì sai trong việc này, nhưng hãy nhớ rằng các instance component đắt hơn nhiều so với các nút DOM đơn giản, và việc tạo ra quá nhiều chúng do các mẫu trừu tượng sẽ gây ra chi phí hiệu suất.

Lưu ý rằng việc giảm số lượng các trường hợp chỉ ít ít sẽ không tạo ra hiệu ứng đáng kể, vì vậy đừng quá lo lắng nếu thành phần chỉ được render một vài lần trong ứng dụng. Tình huống tốt nhất để xem xét tối ưu hóa này lại là trong các danh sách lớn. Hãy tưởng tượng một danh sách gồm 100 mục trong đó mỗi mục chứa nhiều thành phần con. Việc loại bỏ một trừơng hợp trừu tượng không cần thiết ở đây có thể dẫn đến việc giảm hàng trăm trường hợp thành phần.

Như vậy, chúng ta đã cùng nhau khám phá về cách tối ưu hiệu suất trong ứng dụng VueJS trên Cafedev. Hy vọng rằng những kiến thức và kỹ thuật mà chúng ta đã tìm hiểu sẽ giúp ích cho việc phát triển các ứng dụng web của bạn trở nên mạnh mẽ và linh hoạt hơn. Đừng ngần ngại áp dụng những gì bạn đã học vào dự án của mình và tiếp tục theo dõi Cafedev để cập nhật thêm nhiều kiến thức mới và hữu ích về VueJS và hiệu suất phát triển web!

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!