Performance optimization techniques | The Aurelia 2 Docs
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
The Aurelia task queue provides several performance optimization features:
Batch DOM updates to improve rendering performance:
import { batch } from 'aurelia';
// Batch multiple DOM updates in a single frame
batch(() => {
// both assignment will not immediately trigger rerendering
component.prop = someValue;
component2.prop = someOtherValue;
});
// With mordern browser implementation, normally all DOM changes execute in the same task
// and without triggering layout-ing or reflow unless there's a DOM property read in between
// that triggers those.
element1.style.left = '100px';
element2.style.top = '200px';
element3.textContent = 'Updated';State Management Performance
Use the built-in memoization system for expensive computations:
Share memoized selectors across components for better performance:
Computed Observer Performance
Sync vs Async Flush Modes
Choose the appropriate flush mode for computed properties:
Computed Property Optimization
Optimize computed properties for better performance:
Manual computed dependencies declaration
Read more on @computed decorator here.
Watch Performance Optimization
Efficient Watch Expressions
Use efficient watch expressions to minimize performance impact:
Control when watch callbacks execute:
Binding Behaviors for Performance
Throttle Binding Behavior
Limit how often a binding updates, useful for expensive operations triggered by user input:
Debounce Binding Behavior
Delay binding updates until user stops typing, ideal for search-as-you-type:
Performance Tips:
Use throttle for continuous events (scroll, mousemove, resize)
Use debounce for discrete user input (typing, form fields)
Throttle allows periodic updates; debounce waits for quiet period
Default delay is 200ms if not specified
Virtual Repeat Performance
Optimize Virtual Repeat for Large Collections
Configure virtual-repeat inline for optimal performance with large datasets:
Virtual Repeat with Variable Heights
For items with varying heights, enable the variable-height option:
Performance Note: Variable sizing has more overhead than fixed sizing. Use fixed heights when possible for best performance.
Tree Shaking Configuration
Configure your bundler for optimal tree shaking:
Import only what you need from Aurelia packages:
Route-Based Code Splitting
Split your application by routes for better loading performance:
Component-Based Code Splitting
Split large components into separate chunks:
Minification and Compression
Configure production builds for optimal performance:
Service Worker Integration
Implement caching strategies for better performance:
Proper Event Listener Cleanup
Avoid Circular References
Use WeakMap for storing metadata that should be garbage collected:
Batch Multiple State Changes
When making multiple property changes, use batch() to combine them into a single change notification:
Batch multiple array operations to prevent repeated re-renders:
Performance Benefits:
Reduces the number of change notifications
Prevents unnecessary intermediate UI updates
Particularly effective when updating multiple related properties
Essential for bulk data operations
Runtime Performance Profiling
Performance Metrics Collection
Real-World Performance Scenarios
Scenario 1: Optimized Data Grid
Build a high-performance data grid with 10,000+ rows:
Performance Features Used:
debounceprevents filtering on every keystrokevirtual-repeatrenders only visible rows@computedwith explicit deps caches filter resultsFixed
item-heightenables optimal scrolling
Scenario 2: Real-Time Dashboard Updates
Handle high-frequency updates efficiently:
Performance Features Used:
batch()combines multiple updates into one notificationPersistent task with delay for regular updates
Task cancellation on component detach prevents leaks
Scenario 3: Image Gallery with Lazy Loading
Optimize large image galleries:
Performance Features Used:
variable-heighthandles different aspect ratiosNative lazy loading with
loading="lazy"Task queue prevents UI blocking during data loading
Chunked loading for progressive rendering
Scenario 4: Complex Form with Validation
Optimize forms with many fields:
Performance Features Used:
debounceon inputs reduces validation frequencybatch()when loading initial form dataAsync flush for validation computation
Object.assign for efficient property updates
Use
batchfor array mutation operationsImplement memoization with
createStateMemoizerfor expensive state computationsChoose appropriate flush modes (
syncorasync) for computed propertiesOptimize watch expressions and prefer computed properties
Use
throttlefor continuous events,debouncefor discrete user inputEnable
deepobservation only when needed for nested objects
Always clean up event listeners and subscriptions in
detaching()Use WeakMap for metadata storage that should be garbage collected
Avoid circular references or use explicit cleanup
Cancel persistent tasks when components detach
Monitor memory usage during development
Implement virtual-repeat for lists with 100+ items
Use fixed
item-heightfor best virtual-repeat performanceEnable
variable-heightonly when necessaryUse pagination or infinite scroll for large datasets
Process data in batches with
batch()to avoid blocking the UIStream large datasets when possible using task queue
Use
debounceon form inputs (300-500ms for text, 200ms for other fields)Use
throttleon scroll/resize/mousemove handlers (100-200ms)Batch multiple observable changes with
batch()Prefer
@computedwith explicitdepsover complex expressions in templatesUse
flush: 'async'(default) unless immediate updates are critical
Configure tree shaking properly in your bundler
Use code splitting for routes and large components
Optimize bundle size with selective imports
Implement service worker caching for production apps
Minify and compress assets
6. Performance Monitoring
Monitor component lifecycle performance
Track memory usage patterns with browser DevTools
Use performance metrics to identify bottlenecks
Test with realistic data volumes
These optimization techniques will help you build high-performance Aurelia applications that scale well and provide excellent user experiences.
- Framework-Specific Optimizations
- Task Queue Performance
- State Management Performance
- Computed Observer Performance
- Watch Performance Optimization
- Binding Behaviors for Performance
- Virtual Repeat Performance
- Build Optimization
- Bundle Size Optimization
- Code Splitting
- Production Optimization
- Memory Management
- Component Cleanup
- Memory Leak Prevention
- Observable Batching
- Large Data Handling
- Pagination Strategies
- Data Streaming
- Performance Monitoring
- Runtime Performance Profiling
- Real-World Performance Scenarios
- Scenario 1: Optimized Data Grid
- Scenario 2: Real-Time Dashboard Updates
- Scenario 3: Image Gallery with Lazy Loading
- Scenario 4: Complex Form with Validation
- Best Practices Summary
- 1. Framework Usage
- 2. Memory Management
- 3. Data Handling
- 4. Binding Optimization
- 5. Build Optimization
- 6. Performance Monitoring
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;
}// 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;
}
}// 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();
}
// Deep observation - watch nested property changes
@computed({ deep: true })
get nestedTotal(): number {
return this.items.reduce((sum, item) => sum + item.value, 0);
}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;
}
}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);
}
}// 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);
}<!-- Throttle search updates - max once every 300ms -->
<input type="text"
value.bind="searchQuery & throttle:300">
<!-- Throttle scroll position updates -->
<div scroll.trigger="handleScroll($event) & throttle:100">
<!-- Content -->
</div>
<!-- Multiple values: delay and signal name -->
<input value.bind="filterText & throttle:200:'filter-changed'">import { customElement } from 'aurelia';
@customElement({ name: 'search-box' })
export class SearchBox {
searchQuery = '';
// This will only be called max once every 300ms
searchQueryChanged(newValue: string): void {
this.performExpensiveSearch(newValue);
}
private performExpensiveSearch(query: string): void {
// Expensive API call or computation
}
}<!-- Wait 500ms after user stops typing before updating -->
<input type="text"
value.bind="searchTerm & debounce:500">
<!-- Debounce with custom signal -->
<textarea value.bind="content & debounce:1000:'content-saved'">
</textarea>import { customElement } from 'aurelia';
@customElement({ name: 'live-search' })
export class LiveSearch {
searchTerm = '';
results: any[] = [];
// Only called 500ms after user stops typing
async searchTermChanged(newValue: string): Promise<void> {
if (newValue.length < 3) return;
// This expensive API call only fires after user stops typing
this.results = await this.searchAPI.query(newValue);
}
}<!-- Fixed item height - best performance -->
<div virtual-repeat.for="item of items"
item-height="60"
buffer-size="20">
<item-view item.bind="item"></item-view>
</div>
<!-- Horizontal layout -->
<div virtual-repeat.for="item of items"
layout="horizontal"
item-width="200"
buffer-size="10">
<item-card item.bind="item"></item-card>
</div>
<!-- Configure minimum views -->
<div virtual-repeat.for="item of items"
item-height="80"
min-views="15">
<item-row item.bind="item"></item-row>
</div><!-- Variable height support - Aurelia will measure each item -->
<div virtual-repeat.for="item of items"
variable-height="true"
buffer-size="20">
<div class="item">
<h3>${item.title}</h3>
<p>${item.description}</p>
<!-- Heights can vary based on content -->
</div>
</div>
<!-- Variable width for horizontal layouts -->
<div virtual-repeat.for="item of items"
layout="horizontal"
variable-width="true"
buffer-size="15">
<item-card item.bind="item"></item-card>
</div>// webpack.config.js
module.exports = {
optimization: {
usedExports: true,
sideEffects: false,
minimize: true,
},
resolve: {
mainFields: ['module', 'main']
}
};// 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';// Lazy load route components
@route({
routes: [
{ path: '', component: () => import('./home') },
{ path: 'dashboard', component: () => import('./dashboard') },
{ path: 'settings', component: () => import('./settings') }
]
})
export class App { }// Dynamic component loading
@customElement({ name: 'lazy-component' })
export class LazyComponent {
private heavyComponent: Promise<any>;
binding(): void {
this.heavyComponent = import('./heavy-component');
}
}// vite.config.js
export default defineConfig({
build: {
minify: 'terser',
rollupOptions: {
output: {
manualChunks: {
vendor: ['aurelia'],
utils: ['lodash', 'date-fns']
}
}
}
}
});// 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;
});
});
})
);
}
});@customElement({ name: 'event-component' })
export class EventComponent {
private resizeHandler = this.onResize.bind(this);
attached(): void {
window.addEventListener('resize', this.resizeHandler);
}
detaching(): void {
window.removeEventListener('resize', this.resizeHandler);
}
private onResize(): void {
// Handle resize
}
}import { resolve } from '@aurelia/kernel';
import { IEventAggregator } from '@aurelia/kernel';
import { customElement } from 'aurelia';
@customElement({ name: 'subscription-component' })
export class SubscriptionComponent {
private eventAggregator = resolve(IEventAggregator);
private subscriptions: Subscription[] = [];
attached(): void {
this.subscriptions.push(
this.eventAggregator.subscribe('event', this.handleEvent.bind(this))
);
}
detaching(): void {
this.subscriptions.forEach(sub => sub.dispose());
this.subscriptions = [];
}
private handleEvent(data: unknown): void {
// Handle event
}
}// 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 = [];
}
}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);
}
}import { batch, observable } from '@aurelia/runtime';
import { customElement } from 'aurelia';
@customElement({ name: 'user-profile' })
export class UserProfile {
@observable firstName = '';
@observable lastName = '';
@observable email = '';
@observable phoneNumber = '';
// Without batching: 4 separate change notifications
updateUserSlow(data: UserData): void {
this.firstName = data.firstName; // triggers update
this.lastName = data.lastName; // triggers update
this.email = data.email; // triggers update
this.phoneNumber = data.phoneNumber; // triggers update
}
// With batching: 1 combined change notification
updateUserFast(data: UserData): void {
batch(() => {
this.firstName = data.firstName;
this.lastName = data.lastName;
this.email = data.email;
this.phoneNumber = data.phoneNumber;
// All changes are batched into a single update cycle
});
}
}import { batch, observable } from '@aurelia/runtime';
import { customElement } from 'aurelia';
@customElement({ name: 'todo-list' })
export class TodoList {
@observable items: TodoItem[] = [];
// Batch multiple array operations
bulkUpdate(updates: TodoUpdate[]): void {
batch(() => {
for (const update of updates) {
if (update.action === 'add') {
this.items.push(update.item);
} else if (update.action === 'remove') {
const index = this.items.indexOf(update.item);
if (index > -1) this.items.splice(index, 1);
} else if (update.action === 'update') {
Object.assign(update.item, update.changes);
}
}
// Only one change notification for all operations
});
}
}@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;
}
}import { observable } from '@aurelia/runtime';
import { customElement } from 'aurelia';
interface IDataService {
getItems(offset: number, limit: number): Promise<Item[]>;
}
@customElement({ name: 'infinite-scroll' })
export class InfiniteScroll {
private loadingMore = false;
private hasMore = true;
private scrollContainer!: HTMLElement;
@observable items: Item[] = [];
attached(): void {
this.scrollContainer.addEventListener('scroll', this.onScroll.bind(this));
}
detaching(): void {
this.scrollContainer.removeEventListener('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) {
void this.loadMoreItems();
}
}
private async loadMoreItems(): Promise<void> {
this.loadingMore = true;
try {
// Fetch from your data service
const newItems = await this.fetchItems(this.items.length, 20);
this.items.push(...newItems);
this.hasMore = newItems.length === 20;
} finally {
this.loadingMore = false;
}
}
private async fetchItems(offset: number, limit: number): Promise<Item[]> {
// Your API call here
return [] as Item[];
}
}import { customElement, batch } from 'aurelia';
@customElement({ name: 'data-stream' })
export class DataStream {
private items: Item[] = [];
private processingQueue: Item[] = [];
async loadData(): Promise<void> {
const stream = this.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<void>(resolve => {
batch(() => {
this.items.push(...batch);
resolve();
});
});
}
private async *getStreamingData(): AsyncGenerator<Item[]> {
// Your streaming data source
// Example: fetch data in chunks from API
yield [] as Item[];
}
}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];
}
}import { customElement } from 'aurelia';
@customElement({
name: 'data-grid',
template: `
<div class="grid-container">
<div class="grid-header">
<input type="text"
value.bind="filterText & debounce:300"
placeholder="Search...">
</div>
<div class="grid-body"
virtual-repeat.for="row of filteredRows"
item-height="40"
buffer-size="20">
<div class="grid-row">
<span>\${row.id}</span>
<span>\${row.name}</span>
<span>\${row.email}</span>
</div>
</div>
</div>
`
})
export class DataGrid {
rows: DataRow[] = [];
filterText = '';
@computed({ deps: ['rows', 'filterText'] })
get filteredRows(): DataRow[] {
if (!this.filterText) return this.rows;
const search = this.filterText.toLowerCase();
return this.rows.filter(row =>
row.name.toLowerCase().includes(search) ||
row.email.toLowerCase().includes(search)
);
}
}import { batch, observable, queueRecurringTask } from '@aurelia/runtime';
import { customElement, resolve } from 'aurelia';
import { PLATFORM } from 'aurelia';
@customElement({ name: 'live-dashboard' })
export class LiveDashboard {
@observable metrics: DashboardMetrics = {
activeUsers: 0,
requestsPerSecond: 0,
errorRate: 0,
avgResponseTime: 0
};
private updateTask?: any;
attaching(): void {
// Batch multiple metric updates together
this.updateTask = queueRecurringTask(() => {
this.updateMetrics();
}, { interval: 1000 });
}
detaching(): void {
this.updateTask?.cancel();
}
private updateMetrics(): void {
// Fetch latest metrics from API
const newMetrics = this.fetchLatestMetrics();
// Use batch to update all metrics at once
batch(() => {
this.metrics.activeUsers = newMetrics.activeUsers;
this.metrics.requestsPerSecond = newMetrics.requestsPerSecond;
this.metrics.errorRate = newMetrics.errorRate;
this.metrics.avgResponseTime = newMetrics.avgResponseTime;
});
}
private fetchLatestMetrics(): DashboardMetrics {
// API call
return {} as DashboardMetrics;
}
}<!-- image-gallery.html -->
<div class="gallery">
<div virtual-repeat.for="image of images"
variable-height="true"
buffer-size="15"
layout="horizontal">
<img src.bind="image.thumbnail"
loading="lazy"
alt="\${image.title}">
</div>
</div>import { customElement, batch } from 'aurelia';
@customElement({ name: 'image-gallery' })
export class ImageGallery {
images: GalleryImage[] = [];
async attached(): Promise<void> {
// Load images in chunks
await this.loadImagesInChunks();
}
private async loadImagesInChunks(): Promise<void> {
const chunkSize = 50;
const allImages = await this.fetchAllImageMetadata();
for (let i = 0; i < allImages.length; i += chunkSize) {
const chunk = allImages.slice(i, i + chunkSize);
// Use task queue to prevent blocking
await new Promise<void>(resolve => {
batch(() => {
this.images.push(...chunk);
resolve();
});
});
}
}
private async fetchAllImageMetadata(): Promise<GalleryImage[]> {
// Fetch from API
return [] as GalleryImage[];
}
}import { batch, observable } from '@aurelia/runtime';
import { customElement } from 'aurelia';
@customElement({ name: 'complex-form' })
export class ComplexForm {
@observable formData: FormData = {
personalInfo: {},
address: {},
preferences: {},
settings: {}
};
// Debounce validation to avoid excessive checks
template = `
<form>
<input value.bind="formData.personalInfo.firstName & debounce:200">
<input value.bind="formData.personalInfo.lastName & debounce:200">
<input value.bind="formData.personalInfo.email & debounce:300">
<!-- More fields... -->
</form>
`;
loadFormData(data: FormData): void {
// Batch all field updates
batch(() => {
Object.assign(this.formData.personalInfo, data.personalInfo);
Object.assign(this.formData.address, data.address);
Object.assign(this.formData.preferences, data.preferences);
Object.assign(this.formData.settings, data.settings);
});
}
@computed({ flush: 'async' })
get isFormValid(): boolean {
// Expensive validation runs asynchronously
return this.validateAllFields();
}
private validateAllFields(): boolean {
// Validation logic
return true;
}
}