Skip to the content.

Performance Optimization

This guide covers best practices and techniques for optimizing the performance of your Firebase ORM applications.

Database Design Optimization

Efficient Data Modeling

// ✅ Good: Denormalized data for read optimization
@Model({
  reference_path: "user_posts",
  path_id: "post_id"
})
export class UserPost extends BaseModel {
  @Field({ is_required: true })
  public title!: string;

  @Field({ is_required: true })
  public content!: string;

  // Denormalized user data for efficient reads
  @Field({ field_name: "author_name" })
  public authorName!: string;

  @Field({ field_name: "author_avatar" })
  public authorAvatar?: string;

  @Field({ field_name: "user_id" })
  public userId!: string;
}

// ❌ Avoid: Over-normalization requiring many joins
@Model({
  reference_path: "posts",
  path_id: "post_id"
})
export class Post extends BaseModel {
  @Field({ is_required: true })
  public title!: string;

  @Field({ field_name: "user_id" })
  public userId!: string;
  // Requires additional query to get user details
}

Optimize Collection Structure

// ✅ Good: Hierarchical structure for related data
@Model({
  reference_path: "websites/:website_id/pages",
  path_id: "page_id"
})
export class WebsitePage extends BaseModel {
  @Field({ is_required: true })
  public title!: string;

  @Field({ is_required: true })
  public content!: string;
}

// Usage: Automatically scoped to website
const website = await Website.findOne('domain', '==', 'example.com');
const pages = await website.getModel(WebsitePage).getAll(); // Efficient subcollection query

Field Optimization

@Model({
  reference_path: "products",
  path_id: "product_id"
})
export class Product extends BaseModel {
  // ✅ Index frequently queried fields
  @Field({ 
    is_required: true,
    is_text_indexing: true  // Enable for search
  })
  public name!: string;

  @Field({ is_required: true })
  public price!: number;

  @Field({ is_required: true })
  public category!: string;

  // ✅ Use appropriate field names
  @Field({ field_name: "created_at" })
  public createdAt!: string;

  // ❌ Avoid: Large objects that aren't frequently accessed
  // Store these in separate documents or use lazy loading
  @Field({ is_required: false })
  public detailedSpecs?: any; // Keep this small or move to separate model
}

Query Optimization

Efficient Query Patterns

// ✅ Good: Use compound queries with proper indexing
const getActiveProductsByCategory = async (category: string) => {
  return await Product.query()
    .where('category', '==', category)
    .where('isActive', '==', true)
    .orderBy('createdAt', 'desc')
    .limit(20)
    .get();
};

// ✅ Good: Use pagination for large datasets
const getProductsPaginated = async (lastDoc?: Product, limit: number = 20) => {
  let query = Product.query()
    .where('isActive', '==', true)
    .orderBy('createdAt', 'desc')
    .limit(limit);

  if (lastDoc) {
    query = query.startAfter(lastDoc.createdAt);
  }

  return await query.get();
};

// ❌ Avoid: Loading all data then filtering
const getAllProducts = async () => {
  const allProducts = await Product.getAll(); // Expensive!
  return allProducts.filter(p => p.isActive); // Client-side filtering
};

Query Caching

class QueryCache {
  private cache = new Map<string, { data: any; timestamp: number; ttl: number }>();

  set(key: string, data: any, ttlMs: number = 300000) { // 5 min default
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
      ttl: ttlMs
    });
  }

  get(key: string): any | null {
    const entry = this.cache.get(key);
    if (!entry) return null;

    if (Date.now() - entry.timestamp > entry.ttl) {
      this.cache.delete(key);
      return null;
    }

    return entry.data;
  }

  clear() {
    this.cache.clear();
  }
}

const queryCache = new QueryCache();

const getCachedProducts = async (category: string): Promise<Product[]> => {
  const cacheKey = `products_${category}`;
  
  let products = queryCache.get(cacheKey);
  if (products) {
    return products;
  }

  products = await Product.query()
    .where('category', '==', category)
    .where('isActive', '==', true)
    .get();

  queryCache.set(cacheKey, products, 300000); // Cache for 5 minutes
  return products;
};

Batch Operations

// ✅ Good: Batch multiple operations
const createMultipleProducts = async (productData: any[]) => {
  const promises = productData.map(async (data) => {
    const product = new Product();
    Object.assign(product, data);
    return product.save();
  });

  // Execute all saves in parallel
  return await Promise.all(promises);
};

// ✅ Good: Batch relationship loading
const loadUsersWithPosts = async (userIds: string[]) => {
  // Load all users in parallel
  const userPromises = userIds.map(id => {
    const user = new User();
    return user.load(id);
  });
  
  const users = await Promise.all(userPromises);

  // Load posts for all users in parallel
  const postsPromises = users.map(user => user.loadHasMany('posts'));
  await Promise.all(postsPromises);

  return users;
};

Memory Management

Efficient Model Loading

// ✅ Good: Load only required fields
const getProductSummaries = async () => {
  const products = await Product.query()
    .select(['name', 'price', 'category']) // Only load needed fields
    .where('isActive', '==', true)
    .get();

  return products.map(p => ({
    id: p.getId(),
    name: p.name,
    price: p.price,
    category: p.category
  }));
};

// ✅ Good: Lazy loading for relationships
class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  private _posts?: Post[];

  async getPosts(): Promise<Post[]> {
    if (!this._posts) {
      this._posts = await this.loadHasMany('posts');
    }
    return this._posts;
  }

  clearPostsCache() {
    this._posts = undefined;
  }
}

Memory Pool Management

class ModelPool<T extends BaseModel> {
  private pool: T[] = [];
  private maxSize: number;
  private createFn: () => T;

  constructor(createFn: () => T, maxSize: number = 50) {
    this.createFn = createFn;
    this.maxSize = maxSize;
  }

  acquire(): T {
    return this.pool.pop() || this.createFn();
  }

  release(model: T) {
    if (this.pool.length < this.maxSize) {
      // Reset model state
      model.clearCache?.();
      this.pool.push(model);
    }
  }
}

// Usage
const userPool = new ModelPool(() => new User(), 20);

const processUsers = async (userIds: string[]) => {
  const results = [];
  
  for (const id of userIds) {
    const user = userPool.acquire();
    try {
      await user.load(id);
      // Process user
      results.push(user.name);
    } finally {
      userPool.release(user);
    }
  }
  
  return results;
};

Real-time Optimization

Selective Subscriptions

// ✅ Good: Subscribe only to necessary data
const subscribeToActiveProducts = (callback: (products: Product[]) => void) => {
  return Product.query()
    .where('isActive', '==', true)
    .where('category', '==', 'electronics') // Narrow scope
    .orderBy('updatedAt', 'desc')
    .limit(10) // Limit subscription size
    .onSnapshot(callback);
};

// ✅ Good: Unsubscribe when component unmounts
class ProductComponent {
  private unsubscribe?: () => void;

  componentDidMount() {
    this.unsubscribe = subscribeToActiveProducts((products) => {
      this.setState({ products });
    });
  }

  componentWillUnmount() {
    if (this.unsubscribe) {
      this.unsubscribe();
    }
  }
}

Connection Pooling

class ConnectionManager {
  private maxConnections = 10;
  private activeConnections = 0;
  private queue: Array<() => void> = [];

  async executeQuery<T>(queryFn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      const execute = async () => {
        if (this.activeConnections >= this.maxConnections) {
          this.queue.push(execute);
          return;
        }

        this.activeConnections++;
        try {
          const result = await queryFn();
          resolve(result);
        } catch (error) {
          reject(error);
        } finally {
          this.activeConnections--;
          const next = this.queue.shift();
          if (next) next();
        }
      };

      execute();
    });
  }
}

const connectionManager = new ConnectionManager();

// Usage
const getProducts = async () => {
  return connectionManager.executeQuery(() => 
    Product.query().where('isActive', '==', true).get()
  );
};

Client-Side Optimization

Data Preloading

// Preload commonly accessed data
class DataPreloader {
  private preloadedData = new Map<string, any>();

  async preloadCategories() {
    const categories = await Category.getAll();
    this.preloadedData.set('categories', categories);
  }

  async preloadUserProfile(userId: string) {
    const user = new User();
    await user.load(userId);
    await user.loadWithRelationships(['profile', 'preferences']);
    this.preloadedData.set(`user_${userId}`, user);
  }

  getPreloadedData(key: string) {
    return this.preloadedData.get(key);
  }
}

const preloader = new DataPreloader();

// Initialize app
const initializeApp = async () => {
  await Promise.all([
    preloader.preloadCategories(),
    preloader.preloadUserProfile(getCurrentUserId())
  ]);
};

Virtual Scrolling for Large Lists

class VirtualizedProductList {
  private visibleStart = 0;
  private visibleEnd = 20;
  private itemHeight = 100;
  private containerHeight = 600;
  private products: Product[] = [];

  async loadMoreProducts() {
    const newProducts = await Product.query()
      .where('isActive', '==', true)
      .orderBy('createdAt', 'desc')
      .startAfter(this.products[this.products.length - 1]?.createdAt)
      .limit(20)
      .get();

    this.products.push(...newProducts);
  }

  getVisibleItems() {
    return this.products.slice(this.visibleStart, this.visibleEnd);
  }

  updateVisibleRange(scrollTop: number) {
    this.visibleStart = Math.floor(scrollTop / this.itemHeight);
    this.visibleEnd = this.visibleStart + Math.ceil(this.containerHeight / this.itemHeight);
    
    // Load more if near end
    if (this.visibleEnd > this.products.length - 5) {
      this.loadMoreProducts();
    }
  }
}

Network Optimization

Request Batching

class RequestBatcher {
  private batches = new Map<string, any[]>();
  private timers = new Map<string, NodeJS.Timeout>();

  batch<T>(key: string, request: () => Promise<T>, delay: number = 100): Promise<T> {
    return new Promise((resolve, reject) => {
      if (!this.batches.has(key)) {
        this.batches.set(key, []);
      }

      this.batches.get(key)!.push({ resolve, reject, request });

      // Clear existing timer
      if (this.timers.has(key)) {
        clearTimeout(this.timers.get(key)!);
      }

      // Set new timer
      this.timers.set(key, setTimeout(() => {
        this.executeBatch(key);
      }, delay));
    });
  }

  private async executeBatch(key: string) {
    const batch = this.batches.get(key);
    if (!batch) return;

    this.batches.delete(key);
    this.timers.delete(key);

    // Execute all requests in parallel
    const promises = batch.map(({ request, resolve, reject }) => 
      request().then(resolve).catch(reject)
    );

    await Promise.allSettled(promises);
  }
}

const batcher = new RequestBatcher();

// Usage
const loadUser = (id: string) => 
  batcher.batch(`user_${id}`, () => {
    const user = new User();
    return user.load(id);
  });

Compression and Serialization

// Optimize data transfer
class DataOptimizer {
  static compressModel(model: BaseModel): any {
    const data = model.toJSON();
    
    // Remove null/undefined values
    const compressed = Object.keys(data).reduce((acc, key) => {
      if (data[key] != null) {
        acc[key] = data[key];
      }
      return acc;
    }, {} as any);

    return compressed;
  }

  static decompressModel<T extends BaseModel>(
    data: any, 
    ModelClass: new () => T
  ): T {
    const model = new ModelClass();
    Object.assign(model, data);
    return model;
  }

  static batchCompress(models: BaseModel[]): any[] {
    return models.map(model => this.compressModel(model));
  }
}

Monitoring and Analytics

Performance Monitoring

class PerformanceMonitor {
  private metrics = new Map<string, number[]>();

  startTimer(operation: string): () => void {
    const start = performance.now();
    
    return () => {
      const duration = performance.now() - start;
      this.recordMetric(operation, duration);
    };
  }

  recordMetric(operation: string, duration: number) {
    if (!this.metrics.has(operation)) {
      this.metrics.set(operation, []);
    }
    
    const measurements = this.metrics.get(operation)!;
    measurements.push(duration);
    
    // Keep only last 100 measurements
    if (measurements.length > 100) {
      measurements.shift();
    }
  }

  getAverageTime(operation: string): number {
    const measurements = this.metrics.get(operation);
    if (!measurements || measurements.length === 0) return 0;
    
    return measurements.reduce((sum, time) => sum + time, 0) / measurements.length;
  }

  getReport(): Record<string, any> {
    const report: Record<string, any> = {};
    
    for (const [operation, measurements] of this.metrics) {
      report[operation] = {
        count: measurements.length,
        average: this.getAverageTime(operation),
        min: Math.min(...measurements),
        max: Math.max(...measurements)
      };
    }
    
    return report;
  }
}

const monitor = new PerformanceMonitor();

// Usage
const getProducts = async () => {
  const endTimer = monitor.startTimer('Product.query');
  
  try {
    const products = await Product.query()
      .where('isActive', '==', true)
      .get();
    return products;
  } finally {
    endTimer();
  }
};

Memory Usage Tracking

class MemoryTracker {
  private snapshots: Array<{ timestamp: number; usage: any }> = [];

  takeSnapshot(label?: string) {
    if (typeof window !== 'undefined' && 'performance' in window) {
      const memory = (performance as any).memory;
      if (memory) {
        this.snapshots.push({
          timestamp: Date.now(),
          usage: {
            label,
            usedJSHeapSize: memory.usedJSHeapSize,
            totalJSHeapSize: memory.totalJSHeapSize,
            jsHeapSizeLimit: memory.jsHeapSizeLimit
          }
        });
      }
    }
  }

  getMemoryTrend() {
    return this.snapshots.map(snapshot => ({
      timestamp: snapshot.timestamp,
      usedMB: Math.round(snapshot.usage.usedJSHeapSize / 1024 / 1024),
      label: snapshot.usage.label
    }));
  }

  detectMemoryLeaks() {
    if (this.snapshots.length < 3) return false;
    
    const recent = this.snapshots.slice(-3);
    const trend = recent.every((snapshot, index) => {
      if (index === 0) return true;
      return snapshot.usage.usedJSHeapSize > recent[index - 1].usage.usedJSHeapSize;
    });
    
    return trend; // Consistently increasing memory usage
  }
}

const memoryTracker = new MemoryTracker();

// Usage
setInterval(() => {
  memoryTracker.takeSnapshot();
  
  if (memoryTracker.detectMemoryLeaks()) {
    console.warn('Potential memory leak detected');
  }
}, 30000); // Check every 30 seconds

Best Practices Summary

1. Database Design

2. Query Optimization

3. Memory Management

4. Real-time Features

5. Client-Side Performance

6. Monitoring

7. Error Handling