iT邦幫忙

2024 iThome 鐵人賽

DAY 26
2
Modern Web

Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器系列 第 26

Day 26: 在 Vue 應用中實現懶加載與代碼分割以提升性能

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20240926/20117461scd3vbAFHm.jpg

簡介

在現代 Web 應用開發中,隨著應用規模的增長,初始加載時間可能會變得越來越長,影響用戶體驗。本文將深入探討如何在 Vue 3 應用中實現懶加載和代碼分割,以顯著提升應用性能。我們將結合 TypeScript、Pinia、Zod、Vee-Validate 等工具,展示如何在實際開發中應用這些技術。

正文

步驟 1: 理解懶加載和代碼分割

懶加載和代碼分割是兩種密切相關的優化技術:

  • 懶加載:僅在需要時才加載資源(如組件、圖片或數據)。
  • 代碼分割:將應用代碼分割成smaller塊,按需加載。

這些技術可以顯著減少初始加載時間,提高應用性能。

步驟 2: 使用 defineAsyncComponent 實現組件懶加載

Vue 3 提供了 defineAsyncComponent 函數,允許我們輕鬆創建異步組件。

import { defineAsyncComponent } from 'vue'
import type { AsyncComponentLoader } from 'vue'

const AsyncComponent = defineAsyncComponent({
  loader: (() => import('./HeavyComponent.vue')) as AsyncComponentLoader,
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000
})

這裡,我們使用 TypeScript 的類型註解來確保類型安全。AsyncComponentLoader 類型確保 loader 函數返回一個有效的組件。
這部分比較少用在於,據我觀察,很少在台灣的 vue 開發人員開發到超大型組件的經驗。
再加上 vue 在處理相關事務本身的效能非常好。

步驟 3: 實現路由懶加載

在 Vue Router 中,我們可以使用動態 import 來實現路由懶加載:

(這也是我個人最常用的方式,過去無數多個範例有展示過)

import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

步驟 4: Pinia Store 的懶加載

雖然 Pinia store 通常在應用啟動時就全部加載,但我們可以實現按需加載特定的 store:

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const user = ref(null)
  const isLoggedIn = computed(() => !!user.value)

  async function fetchUser() {
    const module = await import('./userApi')
    user.value = await module.fetchUserData()
  }

  return { user, isLoggedIn, fetchUser }
})

然後在組件中:

import { defineComponent } from 'vue'

export default defineComponent({
  async setup() {
    const { useUserStore } = await import('../stores/user')
    const userStore = useUserStore()
    await userStore.fetchUser()
    return { userStore }
  }
})

步驟 5: 使用 Suspense 處理異步組件

Suspense 組件可以優雅地處理異步組件的加載狀態:

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'))
</script>

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

補充: 在 vue 的社群中有許多討論到 onMounted, 處理非同步的加載 component 比起用 Suspense,效能更好,
這點是正確的,但為了更好的維護性,我個人還是會用 Suspense 去處理非同步 component 的狀況,理由有幾個合在一起說

我認為 onMounted 的任務是在畫面已經成功渲染食藥做的事情,
我認為他的理念和 Suspense 在非同步做事的目的不一樣,如果程式碼標準一致
那尋找非同步基本上根據 suspense 內部的 component 去尋找,讓 onMounted 本身以及未來要擴展的邏輯和非同步 component
的邏輯分開,況且 Suspense 實際上增加的系統開銷不多,但在程式碼的分層上起到了優雅且關鍵的作用

步驟 6: 實現圖片懶加載

使用 @vueuse/coreuseIntersectionObserver 來實現圖片懶加載:

<script setup lang="ts">
  import { onMounted, useTemplateRef } from 'vue';
  import { useIntersectionObserver } from '@vueuse/core';

  const { src, alt = '' } = defineProps<{
    src: string;
    alt?: string;
  }>();
  
  const imgRef = useTemplateRef<HTMLImageElement>('imgRef')

  onMounted(() => {
    const { stop } = useIntersectionObserver(
      imgRef,
      ([{ isIntersecting }]) => {
        if (isIntersecting) {
          imgRef.value!.src = src!
          stop()
        }
      }
    )
  })
</script>

<template>
  <img ref="imgRef" :src :alt /> 
</template>

步驟 7: 使用 Composables 實現可重用的邏輯

創建一個用於懶加載的 composable:

import { ref, onMounted } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'

export function useLazyLoad(elementRef: Ref<HTMLElement | null>, callback: () => void) {
  const isLoaded = ref(false)

  onMounted(() => {
    const { stop } = useIntersectionObserver(
      elementRef,
      ([{ isIntersecting }]) => {
        if (isIntersecting && !isLoaded.value) {
          callback()
          isLoaded.value = true
          stop()
        }
      }
    )
  })

  return { isLoaded }
}

在組件中使用:

<script lang="ts">
  import { defineComponent, useTemplateRef } from 'vue'
  import { useLazyLoad } from './useLazyLoad'
  import HeavyComponent from './HeavyComponent.vue'

  const sectionRef = useTemplateRef<HTMLDivElement>('sectionRef');
  const { isLoaded } = useLazyLoad(sectionRef, () => {
    console.log('Section is visible, loading heavy component')
  })

</script>

<template>
  <div ref="sectionRef">
    <heavy-component v-if="isLoaded" />
  </div>
</template>

步驟 8: 使用 Vitest 和 @vue/test-utils 進行測試

為懶加載組件編寫測試:

import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import LazyLoadComponent from './LazyLoadComponent.vue'

describe('LazyLoadComponent', () => {
  it('loads content when visible', async () => {
    const wrapper = mount(LazyLoadComponent)
    
    // 模擬 Intersection Observer
    const [callback] = (window as any).IntersectionObserver.mock.calls[0]
    callback([{ isIntersecting: true }])

    await wrapper.vm.$nextTick()

    expect(wrapper.find('.loaded-content').exists()).toBe(true)
  })
})

結論

通過實現懶加載和代碼分割,我們可以顯著提升 Vue 應用的性能。這些技術不僅可以減少初始加載時間,還能優化資源使用。我們可以創建高性能、類型安全且易於維護的應用。

性能優化是一個持續的過程。定期審查和測試您的應用,找出可以進一步優化的地方。通過採用本文介紹的技術和最佳實踐,您將為用戶提供更快、更流暢的體驗,同時保持代碼的可維護性和可擴展性。


上一篇
Day 25: 使用 Vitest 測試異步行為與 API 請求邏輯
下一篇
Day 27: 初探 Nuxt3:如何利用 Nuxt3 與 TypeScript 打造伺服器端渲染應用
系列文
Vue 和 TypeScript 的最佳實踐:成為前端工程師的進階利器30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言