在現代 Web 應用開發中,隨著應用規模的增長,初始加載時間可能會變得越來越長,影響用戶體驗。本文將深入探討如何在 Vue 3 應用中實現懶加載和代碼分割,以顯著提升應用性能。我們將結合 TypeScript、Pinia、Zod、Vee-Validate 等工具,展示如何在實際開發中應用這些技術。
懶加載和代碼分割是兩種密切相關的優化技術:
這些技術可以顯著減少初始加載時間,提高應用性能。
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 在處理相關事務本身的效能非常好。
在 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
})
雖然 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 }
}
})
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
實際上增加的系統開銷不多,但在程式碼的分層上起到了優雅且關鍵的作用
使用 @vueuse/core
的 useIntersectionObserver
來實現圖片懶加載:
<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>
創建一個用於懶加載的 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>
為懶加載組件編寫測試:
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 應用的性能。這些技術不僅可以減少初始加載時間,還能優化資源使用。我們可以創建高性能、類型安全且易於維護的應用。
性能優化是一個持續的過程。定期審查和測試您的應用,找出可以進一步優化的地方。通過採用本文介紹的技術和最佳實踐,您將為用戶提供更快、更流暢的體驗,同時保持代碼的可維護性和可擴展性。