forked from jsnbuchanan/crowd-funder-for-time-pwa
- Change build:* naming from hyphen to colon (build:web-dev → build:web:dev) - Add missing build:web:test and build:web:prod scripts - Update build:electron:dev to include electron startup (build + start) - Remove hardcoded --mode electron to allow proper mode override - Add comprehensive mode override behavior documentation - Fix mode conflicts between hardcoded and passed --mode arguments The plan now properly supports: - Development builds with default --mode development - Testing builds with explicit --mode test override - Production builds with explicit --mode production override - Consistent naming across all platforms (web, capacitor, electron)
662 lines
16 KiB
Markdown
662 lines
16 KiB
Markdown
# 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
|
|
|
|
1. [Why Lazy Loading Matters](#why-lazy-loading-matters)
|
|
2. [Vite Configuration for Optimal Code Splitting](#vite-configuration-for-optimal-code-splitting)
|
|
3. [Lazy Loading Patterns](#lazy-loading-patterns)
|
|
4. [Component-Level Lazy Loading](#component-level-lazy-loading)
|
|
5. [Route-Level Lazy Loading](#route-level-lazy-loading)
|
|
6. [Library-Level Lazy Loading](#library-level-lazy-loading)
|
|
7. [Performance Monitoring](#performance-monitoring)
|
|
8. [Best Practices](#best-practices)
|
|
9. [Common Pitfalls](#common-pitfalls)
|
|
10. [Examples](#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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
1. **Manual Chunks**: Group related dependencies for better caching
|
|
2. **Platform-Specific Chunks**: Separate native and web dependencies
|
|
3. **Vendor Separation**: Keep third-party libraries separate from app code
|
|
4. **Dynamic Imports**: Enable automatic code splitting for dynamic imports
|
|
|
|
## Lazy Loading Patterns
|
|
|
|
### 1. Component-Level Lazy Loading
|
|
|
|
#### Basic Pattern with defineAsyncComponent
|
|
|
|
```typescript
|
|
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
|
|
|
|
```vue
|
|
<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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```bash
|
|
# Analyze bundle size
|
|
npm run build
|
|
npx vite-bundle-analyzer dist
|
|
|
|
# Monitor chunk sizes
|
|
npx vite-bundle-analyzer dist --mode=treemap
|
|
```
|
|
|
|
### Runtime Performance Monitoring
|
|
|
|
```typescript
|
|
@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:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
@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:
|
|
|
|
```typescript
|
|
// 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:**
|
|
```typescript
|
|
// Bad: Lazy loading small components
|
|
const SmallButton = defineAsyncComponent(() => import('./SmallButton.vue'));
|
|
```
|
|
|
|
✅ **Do lazy load strategically:**
|
|
```typescript
|
|
// Good: Lazy load heavy components only
|
|
const HeavyDataProcessor = defineAsyncComponent(() => import('./HeavyDataProcessor.vue'));
|
|
```
|
|
|
|
### 2. Missing Loading States
|
|
|
|
❌ **Don't leave users hanging:**
|
|
```vue
|
|
<template>
|
|
<LazyComponent v-if="show" />
|
|
</template>
|
|
```
|
|
|
|
✅ **Do provide loading feedback:**
|
|
```vue
|
|
<template>
|
|
<Suspense>
|
|
<template #default>
|
|
<LazyComponent v-if="show" />
|
|
</template>
|
|
<template #fallback>
|
|
<LoadingSpinner />
|
|
</template>
|
|
</Suspense>
|
|
</template>
|
|
```
|
|
|
|
### 3. Ignoring Error States
|
|
|
|
❌ **Don't ignore loading failures:**
|
|
```typescript
|
|
const LazyComponent = defineAsyncComponent(() => import('./Component.vue'));
|
|
```
|
|
|
|
✅ **Do handle errors gracefully:**
|
|
```typescript
|
|
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:
|
|
|
|
1. **`src/components/LazyLoadingExample.vue`** - Main example component
|
|
2. **`src/components/sub-components/HeavyComponent.vue`** - Data processing component
|
|
3. **`src/components/sub-components/QRScannerComponent.vue`** - Camera-dependent component
|
|
4. **`src/components/sub-components/ThreeJSViewer.vue`** - 3D rendering component
|
|
5. **`vite.config.optimized.mts`** - Optimized Vite configuration
|
|
|
|
### Usage Example
|
|
|
|
```vue
|
|
<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](https://vuejs.org/guide/components/async.html) and the Vite documentation on [Code Splitting](https://vitejs.dev/guide/features.html#code-splitting). |