You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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

  1. Why Lazy Loading Matters
  2. Vite Configuration for Optimal Code Splitting
  3. Lazy Loading Patterns
  4. Component-Level Lazy Loading
  5. Route-Level Lazy Loading
  6. Library-Level Lazy Loading
  7. Performance Monitoring
  8. Best Practices
  9. Common Pitfalls
  10. 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

  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

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:

  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

<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.