Performance optimization techniques

This guide covers advanced performance optimization techniques for Aurelia applications, including framework-specific optimizations, build configuration, and best practices for high-performance applications.

Framework-Specific Optimizations

Task Queue Performance

The Aurelia task queue provides several performance optimization features:

Task Batching

Batch DOM updates to improve rendering performance:

import { PLATFORM } from 'aurelia';

// Batch multiple DOM updates in a single frame
PLATFORM.domQueue.queueTask(() => {
  // All DOM changes execute in the same animation frame
  element1.style.left = '100px';
  element2.style.top = '200px';
  element3.textContent = 'Updated';
});

Preemptive Task Execution

Use preempt for critical tasks that need immediate execution:

// Runs synchronously if queue is currently flushing
PLATFORM.taskQueue.queueTask(() => {
  updateCriticalUI();
}, { preempt: true });

Suspend Queue for Critical Operations

Use suspend to ensure critical async operations complete before other tasks:

// Blocks subsequent tasks until this completes
PLATFORM.taskQueue.queueTask(async () => {
  await criticalDataOperation();
}, { suspend: true });

State Management Performance

Memoization System

Use the built-in memoization system for expensive computations:

import { createStateMemoizer } from '@aurelia/state';

// Single selector memoization
const selectTotal = createStateMemoizer(
  (state: AppState) => state.items,
  (items) => items.reduce((sum, item) => sum + item.value, 0)
);

// Usage in component
@customElement({ name: 'dashboard' })
export class Dashboard {
  @fromState(selectTotal) total: number;
}

Shared Memoization

Share memoized selectors across components for better performance:

// Define once, use everywhere
const selectFilteredItems = createStateMemoizer(
  (state: AppState) => state.items,
  (state: AppState) => state.filter,
  (items, filter) => items.filter(item => item.category === filter)
);

// Multiple components share the same computation
@customElement({ name: 'item-list' })
export class ItemList {
  @fromState(selectFilteredItems) items: Item[];
}

@customElement({ name: 'item-count' })
export class ItemCount {
  @fromState(selectFilteredItems) items: Item[];
  
  get count(): number {
    return this.items.length;
  }
}

Computed Observer Performance

Sync vs Async Flush Modes

Choose the appropriate flush mode for computed properties:

// Async mode (default) - better performance for most cases
@computed()
get expensiveCalculation(): number {
  return this.complexComputation();
}

// Sync mode - for critical computations that need immediate updates
@computed({ flush: 'sync' })
get criticalValue(): number {
  return this.criticalComputation();
}

Computed Property Optimization

Optimize computed properties for better performance:

export class OptimizedComponent {
  private _memoizedResult: number | null = null;
  private _lastInputs: [number, number] | null = null;

  @computed()
  get optimizedCalculation(): number {
    const inputs: [number, number] = [this.input1, this.input2];
    
    // Manual memoization for expensive calculations
    if (this._lastInputs && 
        this._lastInputs[0] === inputs[0] && 
        this._lastInputs[1] === inputs[1]) {
      return this._memoizedResult!;
    }
    
    this._lastInputs = inputs;
    this._memoizedResult = this.expensiveComputation(inputs[0], inputs[1]);
    return this._memoizedResult;
  }
}

Watch Performance Optimization

Efficient Watch Expressions

Use efficient watch expressions to minimize performance impact:

export class OptimizedWatching {
  // Good: Watch specific properties
  @watch('user.profile.name')
  onUserNameChange(newName: string): void {
    this.updateDisplay(newName);
  }

  // Better: Use computed properties for complex expressions
  @computed()
  get userDisplayName(): string {
    return `${this.user.profile.firstName} ${this.user.profile.lastName}`;
  }

  @watch('userDisplayName')
  onDisplayNameChange(newName: string): void {
    this.updateDisplay(newName);
  }
}

Watch Flush Timing

Control when watch callbacks execute:

// Async flush (default) - better performance
@watch('counter')
onCounterChange(newValue: number): void {
  // Executes in next microtask
  this.performUpdate(newValue);
}

// Sync flush - for critical updates
@watch('criticalValue', { flush: 'sync' })
onCriticalValueChange(newValue: number): void {
  // Executes immediately
  this.updateCriticalUI(newValue);
}

Virtual Repeat Performance

Optimize Virtual Repeat for Large Collections

@customElement({
  name: 'optimized-list',
  template: `
    <div virtual-repeat.for="item of items" 
         virtual-repeat.with="optimizedConfig">
      <item-view item.bind="item"></item-view>
    </div>
  `
})
export class OptimizedList {
  items: Item[] = [];
  
  optimizedConfig = {
    // Increase buffer size for smoother scrolling
    bufferSize: 20,
    
    // Use fixed item height for better performance
    itemHeight: 60,
    
    // Enable scrolling optimization
    scrollThrottle: 16
  };
}

Virtual Repeat with Dynamic Heights

@customElement({ name: 'dynamic-list' })
export class DynamicList {
  items: Item[] = [];
  
  // Pre-calculate heights for performance
  itemHeights = new Map<string, number>();
  
  getItemHeight(item: Item): number {
    if (!this.itemHeights.has(item.id)) {
      this.itemHeights.set(item.id, this.calculateHeight(item));
    }
    return this.itemHeights.get(item.id)!;
  }
  
  private calculateHeight(item: Item): number {
    // Calculate height based on content
    return item.content.length > 100 ? 120 : 60;
  }
}

Build Optimization

Bundle Size Optimization

Tree Shaking Configuration

Configure your bundler for optimal tree shaking:

// webpack.config.js
module.exports = {
  optimization: {
    usedExports: true,
    sideEffects: false,
    minimize: true,
  },
  resolve: {
    mainFields: ['module', 'main']
  }
};

Selective Imports

Import only what you need from Aurelia packages:

// Good: Import specific functions
import { observable, computed } from 'aurelia';

// Better: Import from specific modules
import { observable } from '@aurelia/runtime';
import { computed } from '@aurelia/runtime';

// Best: Use direct imports for better tree shaking
import { observable } from '@aurelia/runtime/dist/esm/observation/observable';

Code Splitting

Route-Based Code Splitting

Split your application by routes for better loading performance:

// Lazy load route components
@route({
  routes: [
    { path: '', component: () => import('./home') },
    { path: 'dashboard', component: () => import('./dashboard') },
    { path: 'settings', component: () => import('./settings') }
  ]
})
export class App { }

Component-Based Code Splitting

Split large components into separate chunks:

// Dynamic component loading
@customElement({ name: 'lazy-component' })
export class LazyComponent {
  private heavyComponent: Promise<any>;
  
  binding(): void {
    this.heavyComponent = import('./heavy-component');
  }
}

Production Optimization

Minification and Compression

Configure production builds for optimal performance:

// vite.config.js
export default defineConfig({
  build: {
    minify: 'terser',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['aurelia'],
          utils: ['lodash', 'date-fns']
        }
      }
    }
  }
});

Service Worker Integration

Implement caching strategies for better performance:

// service-worker.ts
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    // Cache API responses
    event.respondWith(
      caches.open('api-cache').then(cache => {
        return cache.match(event.request).then(response => {
          if (response) {
            return response;
          }
          return fetch(event.request).then(response => {
            cache.put(event.request, response.clone());
            return response;
          });
        });
      })
    );
  }
});

Memory Management

Component Cleanup

Proper Event Listener Cleanup

@customElement({ name: 'event-component' })
export class EventComponent {
  private resizeHandler = this.onResize.bind(this);
  
  attached(): void {
    window.addEventListener('resize', this.resizeHandler);
  }
  
  detached(): void {
    window.removeEventListener('resize', this.resizeHandler);
  }
  
  private onResize(): void {
    // Handle resize
  }
}

Subscription Management

@customElement({ name: 'subscription-component' })
export class SubscriptionComponent {
  private subscriptions: Subscription[] = [];
  
  attached(): void {
    this.subscriptions.push(
      this.eventAggregator.subscribe('event', this.handleEvent.bind(this))
    );
  }
  
  detached(): void {
    this.subscriptions.forEach(sub => sub.dispose());
    this.subscriptions = [];
  }
}

Memory Leak Prevention

Avoid Circular References

// Avoid this pattern
export class Parent {
  children: Child[] = [];
  
  addChild(child: Child): void {
    child.parent = this; // Circular reference
    this.children.push(child);
  }
}

// Better approach
export class Parent {
  children: Child[] = [];
  
  addChild(child: Child): void {
    child.setParent(this);
    this.children.push(child);
  }
  
  dispose(): void {
    this.children.forEach(child => child.setParent(null));
    this.children = [];
  }
}

WeakMap for Metadata

Use WeakMap for storing metadata that should be garbage collected:

const componentMetadata = new WeakMap<object, ComponentMetadata>();

export class MetadataManager {
  static setMetadata(component: object, metadata: ComponentMetadata): void {
    componentMetadata.set(component, metadata);
  }
  
  static getMetadata(component: object): ComponentMetadata | undefined {
    return componentMetadata.get(component);
  }
}

Large Data Handling

Pagination Strategies

Virtual Pagination

@customElement({ name: 'virtual-pagination' })
export class VirtualPagination {
  private allItems: Item[] = [];
  private pageSize = 50;
  private currentPage = 0;
  
  get visibleItems(): Item[] {
    const start = this.currentPage * this.pageSize;
    const end = start + this.pageSize;
    return this.allItems.slice(start, end);
  }
  
  loadMoreItems(): void {
    if (this.hasMoreItems) {
      this.currentPage++;
    }
  }
  
  get hasMoreItems(): boolean {
    return (this.currentPage + 1) * this.pageSize < this.allItems.length;
  }
}

Infinite Scroll

@customElement({ name: 'infinite-scroll' })
export class InfiniteScroll {
  private loadingMore = false;
  private hasMore = true;
  
  @observable items: Item[] = [];
  
  attached(): void {
    this.scrollContainer.addEventListener('scroll', this.onScroll.bind(this));
  }
  
  private onScroll(): void {
    if (this.loadingMore || !this.hasMore) return;
    
    const { scrollTop, scrollHeight, clientHeight } = this.scrollContainer;
    const threshold = 200;
    
    if (scrollTop + clientHeight >= scrollHeight - threshold) {
      this.loadMoreItems();
    }
  }
  
  private async loadMoreItems(): Promise<void> {
    this.loadingMore = true;
    
    try {
      const newItems = await this.dataService.getItems(this.items.length, 20);
      this.items.push(...newItems);
      this.hasMore = newItems.length === 20;
    } finally {
      this.loadingMore = false;
    }
  }
}

Data Streaming

Streaming Large Datasets

@customElement({ name: 'data-stream' })
export class DataStream {
  private items: Item[] = [];
  private processingQueue: Item[] = [];
  
  async loadData(): Promise<void> {
    const stream = this.dataService.getStreamingData();
    
    for await (const chunk of stream) {
      this.processingQueue.push(...chunk);
      
      // Process in batches to avoid blocking the UI
      if (this.processingQueue.length >= 100) {
        await this.processBatch();
      }
    }
    
    // Process remaining items
    if (this.processingQueue.length > 0) {
      await this.processBatch();
    }
  }
  
  private async processBatch(): Promise<void> {
    const batch = this.processingQueue.splice(0, 100);
    
    // Process batch in task queue to avoid blocking
    await new Promise(resolve => {
      PLATFORM.taskQueue.queueTask(() => {
        this.items.push(...batch);
        resolve(void 0);
      });
    });
  }
}

Performance Monitoring

Runtime Performance Profiling

Task Queue Monitoring

// Enable task queue debugging
PLATFORM.taskQueue._tracer.enabled = true;

// Monitor queue performance
class TaskQueueMonitor {
  private taskCounts = new Map<string, number>();
  
  startMonitoring(): void {
    const originalQueueTask = PLATFORM.taskQueue.queueTask;
    
    PLATFORM.taskQueue.queueTask = (callback, options) => {
      const name = callback.name || 'anonymous';
      this.taskCounts.set(name, (this.taskCounts.get(name) || 0) + 1);
      
      return originalQueueTask.call(PLATFORM.taskQueue, callback, options);
    };
  }
  
  getTaskStatistics(): Map<string, number> {
    return new Map(this.taskCounts);
  }
}

Performance Metrics Collection

class PerformanceMetrics {
  private metrics: Map<string, number[]> = new Map();
  
  measure<T>(name: string, fn: () => T): T {
    const start = performance.now();
    const result = fn();
    const end = performance.now();
    
    if (!this.metrics.has(name)) {
      this.metrics.set(name, []);
    }
    
    this.metrics.get(name)!.push(end - start);
    return result;
  }
  
  getAverageTime(name: string): number {
    const times = this.metrics.get(name) || [];
    return times.reduce((sum, time) => sum + time, 0) / times.length;
  }
  
  getPercentile(name: string, percentile: number): number {
    const times = this.metrics.get(name) || [];
    const sorted = times.sort((a, b) => a - b);
    const index = Math.floor(sorted.length * percentile / 100);
    return sorted[index];
  }
}

Best Practices Summary

1. Framework Usage

  • Use task queue for DOM updates and async operations

  • Implement memoization for expensive computations

  • Choose appropriate flush modes for computed properties

  • Optimize watch expressions and use computed properties

2. Memory Management

  • Always clean up event listeners and subscriptions

  • Use WeakMap for metadata storage

  • Avoid circular references

  • Monitor memory usage during development

3. Data Handling

  • Implement virtual scrolling for large lists

  • Use pagination or infinite scroll for large datasets

  • Process data in batches to avoid blocking the UI

  • Stream large datasets when possible

4. Build Optimization

  • Configure tree shaking properly

  • Use code splitting for routes and components

  • Optimize bundle size with selective imports

  • Implement service worker caching

5. Performance Monitoring

  • Profile task queue usage

  • Monitor component lifecycle performance

  • Track memory usage patterns

  • Use performance metrics to identify bottlenecks

These optimization techniques will help you build high-performance Aurelia applications that scale well and provide excellent user experiences.

Last updated

Was this helpful?