Skip to content

Performance Optimization

Overview

Offline functionality involves significant data caching and processing. This document covers strategies to optimize performance for caching, querying, syncing, and memory usage.

Caching Performance

Page Size Configuration

Configure the sync page size based on your needs:

{
    "clientConfiguration": {
        "offline": {
            "syncPageSize": 10000
        }
    }
}
Page Size Trade-off
Small Less memory, more requests
Medium Balanced
Large More memory, fewer requests

BSON Protocol

The Rust syncher uses BSON for efficient transfer:

  • 40-60% smaller than JSON for typical documents
  • Faster parsing on client
  • Lower memory during sync

Parallel File Caching

Cache files in parallel batches:

const BATCH_SIZE = 5;
const batches = chunk(fileReferences, BATCH_SIZE);

for (const batch of batches) {
    await Promise.all(
        batch.map(fileRef => cacheFile(fileRef))
    );
}

Query Performance

Index Optimization

Ensure frequently queried fields are indexed:

{
    "indexedDBIndexes": [
        "systemHeader.templateId",
        "systemHeader.systemType",
        "status",
        "*appTags"
    ]
}

Compound Indexes

For multi-field queries, use compound indexes:

{
    "indexedDBIndexes": [
        "systemHeader.templateId, systemHeader.systemType"
    ]
}

Query Analysis

Check if queries are using indexes. This analysis can be performed directly in the browser console:

// Enable Dexie debug mode
Dexie.debug = true;

// Check query plan
const query = db.documents
    .where('systemHeader.templateId')
    .equals('abc-123');

console.log('Using index:', query.query.keyPath);

Note: This level of analysis is feasible directly from the browser DevTools console - you don't need to run a separate Node script.

Table vs Collection Queries

Query Type Performance When to Use
Table.where() Fast Field is indexed
Collection.filter() Slower No index available
// Fast: Uses index
await db.documents.where('templateId').equals(id);

// Slower: Full scan with filter
await db.documents.filter(doc => doc.templateId === id);

Memory Optimization

Shared Worker Benefits

The Dexie Shared Worker:

  • Single worker for all tabs
  • Reduces memory per tab
  • Prevents concurrent access conflicts
  • Shares query cache

Worker Fallback

Android Chrome doesn't support Shared Workers:

if (isAndroidChrome()) {
    // Use Web Worker with Web Locks
    worker = new Worker('OfflinePoller.js');
} else {
    // Use Shared Worker
    worker = new SharedWorker('OfflinePoller.js').port;
}

Large Document Handling

For documents exceeding 32KB:

  1. Truncate error documents - Limit errorDocumentJSON to 32KB
  2. Split large arrays - Consider pagination
  3. Compress where possible - Use BSON encoding

Memory Monitoring

// Check memory usage
const memory = await navigator.storage.estimate();
console.log(`Using ${(memory.usage / 1024 / 1024).toFixed(2)} MB`);
console.log(`Available ${(memory.quota / 1024 / 1024).toFixed(2)} MB`);

Sync Performance

Sync Ordering

Client data is sent before receiving server data:

1. Push local changes → Server
2. Wait for server confirmation
3. Fetch server changes → Client

This order: - Reduces conflicts - Ensures server has latest data - Minimizes duplicate transfers

Incremental Sync

Only sync changes since last sync:

const lastSyncTime = await getLastSyncTime();
const changes = await fetchChanges(lastSyncTime);

Browser-Specific Optimizations

Firefox Caching

Firefox has known slower IndexedDB performance:

{
    "clientConfiguration": {
        "offline": {
            "syncPageSize": 5000
        }
    }
}

Reduce page size for Firefox users.

Chrome Optimizations

Chrome generally has the best IndexedDB performance:

  • Use larger page sizes
  • Take advantage of Shared Workers
  • Use persistent storage

Safari/WebKit

Safari has stricter storage limits:

// Request persistent storage
if (navigator.storage && navigator.storage.persist) {
    const persistent = await navigator.storage.persist();
    if (!persistent) {
        console.warn('Storage may be evicted');
    }
}

Service Worker Optimization

Precache Manifest Size

Limit files in the precache manifest:

injectManifest({
    maximumFileSizeToCacheInBytes: 10 * 1024 * 1024,
    globIgnores: [
        'vendor/custom-component-modules/**/*',
        '*.map'
    ]
});

Cache-First Strategy

Use cache-first for static assets:

workbox.routing.registerRoute(
    /\.(js|css|woff2?)$/,
    new workbox.strategies.CacheFirst()
);

Network-First for APIs

Use network-first for dynamic data:

workbox.routing.registerRoute(
    /\/api\//,
    new workbox.strategies.NetworkFirst()
);

Map Tile Optimization

Zoom Level Limits

Cache only necessary zoom levels:

{
    "tileServerGL": {
        "minZoom": 8,
        "maxZoom": 16
    }
}

Bounding Box Optimization

Cache only the necessary region:

{
    "tileServerGLCacheBounds": [144.5, -38.0, 145.5, -37.5]
}

Smaller bounds = faster caching.

Vector Tile Benefits

Vector tiles are more efficient than raster:

Type Size Scalability
Vector Small Any zoom
Raster Large Fixed zoom

Configuration for Performance

Optimized Settings

{
    "clientConfiguration": {
        "offline": {
            "syncPageSize": 10000,
            "cacheRetryInterval": 20000,
            "pollInterval": 5000
        }
    }
}

Monitoring and Profiling

Browser DevTools

  1. Application Tab - View IndexedDB contents
  2. Performance Tab - Profile caching operations
  3. Network Tab - Monitor sync requests
  4. Memory Tab - Track memory usage

Custom Logging

// Performance timing
const start = performance.now();
await cacheDocuments(documents);
const duration = performance.now() - start;
console.log(`Cached ${documents.length} docs in ${duration}ms`);

IndexedDB Stats

// Count documents
const count = await db.documents.count();
console.log(`Total documents: ${count}`);

// Estimate storage
const estimate = await navigator.storage.estimate();
console.log(`Storage used: ${estimate.usage} bytes`);

Troubleshooting Performance

Slow Caching

  1. Reduce syncPageSize
  2. Check network speed
  3. Verify no errors in sync
  4. Monitor memory usage

Slow Queries

Slow queries are by far the most common offline performance issue. When troubleshooting slow queries, ask these questions:

  1. What is the highest priority indexed term in your query? - Check which index will be used first
  2. Is this highest priority term efficient at cutting down the database size? - A term that returns only a few documents is better than one that returns hundreds
  3. Do you need to add a new index? - If an important filter field isn't indexed, add it to indexedDBIndexes
  4. Are there redundant terms in your query? - Unnecessary terms add overhead
  5. Is a high priority index returning large subsets? - For example, querying on systemHeader.templateId first will return many documents before the Collection.filter() step can narrow them down

Compound Indexes: Compound indexes have been explored and can be powerful. They can also be used as individual indexes - as noted in the documentation.

High Memory Usage

  1. Reduce page size
  2. Clear old queue entries
  3. Limit cached documents
  4. Use Shared Workers