16 KiB
Vue 3 + Vite + vue-facing-decorator: Lazy Loading Patterns & Best Practices
Overview
This document provides comprehensive guidance on implementing lazy loading and code splitting in Vue 3 applications using Vite and vue-facing-decorator
. The patterns demonstrated here optimize bundle size, improve initial load times, and enhance user experience through progressive loading.
Author: Matthew Raymer
Version: 1.0.0
Last Updated: 2024
Table of Contents
- Why Lazy Loading Matters
- Vite Configuration for Optimal Code Splitting
- Lazy Loading Patterns
- Component-Level Lazy Loading
- Route-Level Lazy Loading
- Library-Level Lazy Loading
- Performance Monitoring
- Best Practices
- Common Pitfalls
- Examples
Why Lazy Loading Matters
Performance Benefits
- Faster Initial Load: Only load code needed for the current view
- Reduced Bundle Size: Split large applications into smaller chunks
- Better Caching: Independent chunks can be cached separately
- Improved User Experience: Progressive loading with loading states
When to Use Lazy Loading
✅ Good Candidates for Lazy Loading:
- Heavy components (data processing, 3D rendering)
- Feature-specific components (QR scanner, file uploader)
- Route-based components
- Large third-party libraries (ThreeJS, Chart.js)
- Components with conditional rendering
❌ Avoid Lazy Loading:
- Core UI components used everywhere
- Small utility components
- Components needed for initial render
- Components with frequent usage patterns
Vite Configuration for Optimal Code Splitting
Enhanced Build Configuration
// vite.config.optimized.mts
export async function createOptimizedBuildConfig(mode: string): Promise<UserConfig> {
return {
build: {
rollupOptions: {
output: {
// Enhanced manual chunks for better code splitting
manualChunks: {
// Vendor chunks for better caching
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['@fortawesome/fontawesome-svg-core', '@fortawesome/vue-fontawesome'],
'crypto-vendor': ['@ethersproject/wallet', '@ethersproject/hdnode'],
'sql-vendor': ['@jlongster/sql.js', 'absurd-sql', 'dexie'],
'qr-vendor': ['qrcode', 'jsqr', 'vue-qrcode-reader'],
'three-vendor': ['three', '@tweenjs/tween.js'],
'utils-vendor': ['luxon', 'ramda', 'zod', 'axios'],
// Platform-specific chunks
...(isCapacitor && {
'capacitor-vendor': ['@capacitor/core', '@capacitor/app']
})
}
}
}
}
};
}
Key Configuration Features
- Manual Chunks: Group related dependencies for better caching
- Platform-Specific Chunks: Separate native and web dependencies
- Vendor Separation: Keep third-party libraries separate from app code
- Dynamic Imports: Enable automatic code splitting for dynamic imports
Lazy Loading Patterns
1. Component-Level Lazy Loading
Basic Pattern with defineAsyncComponent
import { Component, Vue } from 'vue-facing-decorator';
import { defineAsyncComponent } from 'vue';
@Component({
name: 'LazyLoadingExample',
components: {
LazyHeavyComponent: defineAsyncComponent({
loader: () => import('./sub-components/HeavyComponent.vue'),
loadingComponent: {
template: '<div class="loading">Loading...</div>'
},
errorComponent: {
template: '<div class="error">Failed to load</div>'
},
delay: 200,
timeout: 10000
})
}
})
export default class LazyLoadingExample extends Vue {
// Component logic
}
Advanced Pattern with Suspense
<template>
<Suspense>
<template #default>
<LazyHeavyComponent v-if="showComponent" />
</template>
<template #fallback>
<div class="loading-fallback">
<div class="spinner"></div>
<p>Loading component...</p>
</div>
</template>
</Suspense>
</template>
2. Conditional Loading
@Component({
name: 'ConditionalLazyLoading'
})
export default class ConditionalLazyLoading extends Vue {
showHeavyComponent = false;
showQRScanner = false;
// Lazy load based on user interaction
async toggleHeavyComponent(): Promise<void> {
this.showHeavyComponent = !this.showHeavyComponent;
if (this.showHeavyComponent) {
// Preload component for better UX
await this.preloadComponent(() => import('./HeavyComponent.vue'));
}
}
private async preloadComponent(loader: () => Promise<any>): Promise<void> {
try {
await loader();
} catch (error) {
console.warn('Preload failed:', error);
}
}
}
3. Library-Level Lazy Loading
@Component({
name: 'LibraryLazyLoading'
})
export default class LibraryLazyLoading extends Vue {
private threeJS: any = null;
async loadThreeJS(): Promise<void> {
if (!this.threeJS) {
// Lazy load ThreeJS only when needed
this.threeJS = await import('three');
console.log('ThreeJS loaded successfully');
}
}
async initialize3DScene(): Promise<void> {
await this.loadThreeJS();
// Use ThreeJS after loading
const scene = new this.threeJS.Scene();
// ... rest of 3D setup
}
}
Component-Level Lazy Loading
Heavy Data Processing Component
// HeavyComponent.vue
@Component({
name: 'HeavyComponent'
})
export default class HeavyComponent extends Vue {
@Prop({ required: true }) readonly data!: any;
async processData(): Promise<void> {
// Process data in batches to avoid blocking UI
const batchSize = 10;
const items = this.data.items;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await this.processBatch(batch);
// Allow UI to update
await this.$nextTick();
await new Promise(resolve => setTimeout(resolve, 10));
}
}
}
Camera-Dependent Component
// QRScannerComponent.vue
@Component({
name: 'QRScannerComponent'
})
export default class QRScannerComponent extends Vue {
async mounted(): Promise<void> {
// Initialize camera only when component is mounted
await this.initializeCamera();
}
async initializeCamera(): Promise<void> {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
this.cameras = devices.filter(device => device.kind === 'videoinput');
this.hasCamera = this.cameras.length > 0;
} catch (error) {
console.error('Camera initialization failed:', error);
this.hasCamera = false;
}
}
}
Route-Level Lazy Loading
Vue Router Configuration
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/HomeView.vue')
},
{
path: '/heavy-feature',
name: 'HeavyFeature',
component: () => import('@/views/HeavyFeatureView.vue'),
// Preload on hover for better UX
beforeEnter: (to, from, next) => {
if (from.name) {
// Preload component when navigating from another route
import('@/views/HeavyFeatureView.vue');
}
next();
}
}
];
export default createRouter({
history: createWebHistory(),
routes
});
Route Guards with Lazy Loading
// router/guards.ts
export async function lazyLoadGuard(to: any, from: any, next: any): Promise<void> {
if (to.meta.requiresHeavyFeature) {
try {
// Preload heavy feature before navigation
await import('@/components/HeavyFeature.vue');
next();
} catch (error) {
console.error('Failed to load heavy feature:', error);
next('/error');
}
} else {
next();
}
}
Library-Level Lazy Loading
Dynamic Library Loading
@Component({
name: 'DynamicLibraryLoader'
})
export default class DynamicLibraryLoader extends Vue {
private libraries: Map<string, any> = new Map();
async loadLibrary(name: string): Promise<any> {
if (this.libraries.has(name)) {
return this.libraries.get(name);
}
let library: any;
switch (name) {
case 'three':
library = await import('three');
break;
case 'chart':
library = await import('chart.js');
break;
case 'qr':
library = await import('jsqr');
break;
default:
throw new Error(`Unknown library: ${name}`);
}
this.libraries.set(name, library);
return library;
}
}
Conditional Library Loading
@Component({
name: 'ConditionalLibraryLoader'
})
export default class ConditionalLibraryLoader extends Vue {
async loadPlatformSpecificLibrary(): Promise<void> {
if (process.env.VITE_PLATFORM === 'capacitor') {
// Load Capacitor-specific libraries
await import('@capacitor/camera');
await import('@capacitor/filesystem');
} else {
// Load web-specific libraries
await import('file-saver');
await import('jszip');
}
}
}
Performance Monitoring
Bundle Analysis
# Analyze bundle size
npm run build
npx vite-bundle-analyzer dist
# Monitor chunk sizes
npx vite-bundle-analyzer dist --mode=treemap
Runtime Performance Monitoring
@Component({
name: 'PerformanceMonitor'
})
export default class PerformanceMonitor extends Vue {
private performanceMetrics = {
componentLoadTime: 0,
renderTime: 0,
memoryUsage: 0
};
private measureComponentLoad(componentName: string): void {
const startTime = performance.now();
return () => {
const loadTime = performance.now() - startTime;
this.performanceMetrics.componentLoadTime = loadTime;
console.log(`${componentName} loaded in ${loadTime.toFixed(2)}ms`);
// Send to analytics
this.trackPerformance(componentName, loadTime);
};
}
private trackPerformance(component: string, loadTime: number): void {
// Send to analytics service
if (window.gtag) {
window.gtag('event', 'component_load', {
component_name: component,
load_time: loadTime
});
}
}
}
Best Practices
1. Loading States
Always provide meaningful loading states:
<template>
<Suspense>
<template #default>
<LazyComponent />
</template>
<template #fallback>
<div class="loading-state">
<div class="spinner"></div>
<p>Loading {{ componentName }}...</p>
<p class="loading-tip">{{ loadingTip }}</p>
</div>
</template>
</Suspense>
</template>
2. Error Handling
Implement comprehensive error handling:
const LazyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
errorComponent: {
template: `
<div class="error-state">
<h3>Failed to load component</h3>
<p>{{ error.message }}</p>
<button @click="retry">Retry</button>
</div>
`,
props: ['error'],
methods: {
retry() {
this.$emit('retry');
}
}
},
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
console.warn(`Retrying component load (attempt ${attempts})`);
retry();
} else {
console.error('Component failed to load after 3 attempts');
fail();
}
}
});
3. Preloading Strategy
Implement intelligent preloading:
@Component({
name: 'SmartPreloader'
})
export default class SmartPreloader extends Vue {
private preloadQueue: Array<() => Promise<any>> = [];
private isPreloading = false;
// Preload based on user behavior
onUserHover(componentLoader: () => Promise<any>): void {
this.preloadQueue.push(componentLoader);
this.processPreloadQueue();
}
private async processPreloadQueue(): Promise<void> {
if (this.isPreloading || this.preloadQueue.length === 0) return;
this.isPreloading = true;
while (this.preloadQueue.length > 0) {
const loader = this.preloadQueue.shift();
if (loader) {
try {
await loader();
} catch (error) {
console.warn('Preload failed:', error);
}
}
// Small delay between preloads
await new Promise(resolve => setTimeout(resolve, 100));
}
this.isPreloading = false;
}
}
4. Bundle Optimization
Optimize bundle splitting:
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
// Group by feature
if (id.includes('qr')) return 'qr-feature';
if (id.includes('three')) return '3d-feature';
if (id.includes('chart')) return 'chart-feature';
// Group by vendor
if (id.includes('node_modules')) {
if (id.includes('vue')) return 'vue-vendor';
if (id.includes('lodash')) return 'utils-vendor';
return 'vendor';
}
}
}
}
}
});
Common Pitfalls
1. Over-Lazy Loading
❌ Don't lazy load everything:
// Bad: Lazy loading small components
const SmallButton = defineAsyncComponent(() => import('./SmallButton.vue'));
✅ Do lazy load strategically:
// Good: Lazy load heavy components only
const HeavyDataProcessor = defineAsyncComponent(() => import('./HeavyDataProcessor.vue'));
2. Missing Loading States
❌ Don't leave users hanging:
<template>
<LazyComponent v-if="show" />
</template>
✅ Do provide loading feedback:
<template>
<Suspense>
<template #default>
<LazyComponent v-if="show" />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
3. Ignoring Error States
❌ Don't ignore loading failures:
const LazyComponent = defineAsyncComponent(() => import('./Component.vue'));
✅ Do handle errors gracefully:
const LazyComponent = defineAsyncComponent({
loader: () => import('./Component.vue'),
errorComponent: ErrorFallback,
onError: (error, retry, fail) => {
console.error('Component load failed:', error);
// Retry once, then fail
retry();
}
});
Examples
Complete Lazy Loading Example
See the following files for complete working examples:
src/components/LazyLoadingExample.vue
- Main example componentsrc/components/sub-components/HeavyComponent.vue
- Data processing componentsrc/components/sub-components/QRScannerComponent.vue
- Camera-dependent componentsrc/components/sub-components/ThreeJSViewer.vue
- 3D rendering componentvite.config.optimized.mts
- Optimized Vite configuration
Usage Example
<template>
<div class="app">
<h1>Lazy Loading Demo</h1>
<LazyLoadingExample
:initial-load-heavy="false"
@qr-detected="handleQRCode"
@model-loaded="handleModelLoaded"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-facing-decorator';
import LazyLoadingExample from '@/components/LazyLoadingExample.vue';
@Component({
name: 'App',
components: {
LazyLoadingExample
}
})
export default class App extends Vue {
handleQRCode(data: string): void {
console.log('QR code detected:', data);
}
handleModelLoaded(info: any): void {
console.log('3D model loaded:', info);
}
}
</script>
Conclusion
Lazy loading with Vue 3 + Vite + vue-facing-decorator
provides powerful tools for optimizing application performance. By implementing these patterns strategically, you can significantly improve initial load times while maintaining excellent user experience.
Remember to:
- Use lazy loading for heavy components and features
- Provide meaningful loading and error states
- Monitor performance and bundle sizes
- Implement intelligent preloading strategies
- Handle errors gracefully
For more information, see the Vue 3 documentation on Async Components and the Vite documentation on Code Splitting.