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:
- Truncate error documents - Limit
errorDocumentJSONto 32KB - Split large arrays - Consider pagination
- 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
- Application Tab - View IndexedDB contents
- Performance Tab - Profile caching operations
- Network Tab - Monitor sync requests
- 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
- Reduce
syncPageSize - Check network speed
- Verify no errors in sync
- Monitor memory usage
Slow Queries
Slow queries are by far the most common offline performance issue. When troubleshooting slow queries, ask these questions:
- What is the highest priority indexed term in your query? - Check which index will be used first
- 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
- Do you need to add a new index? - If an important filter field isn't indexed, add it to
indexedDBIndexes - Are there redundant terms in your query? - Unnecessary terms add overhead
- Is a high priority index returning large subsets? - For example, querying on
systemHeader.templateIdfirst will return many documents before theCollection.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
- Reduce page size
- Clear old queue entries
- Limit cached documents
- Use Shared Workers
Related Documentation
- 101-Offline-Architecture.md - System overview
- 102-IndexedDB-and-Dexie.md - Database layer
- 104-Offline-Syncing.md - Sync process
- 109-Configuration-Reference.md - Configuration options