Performance Optimization - Response Size Reduction
Overview
This document outlines optimizations implemented to reduce API response sizes by 30-70% and to make data loading structurally stable (no unnecessary refetches, no REST on hover, predictable caching behavior).
Stability Layer Principles
The stability layer sits on top of existing performance work and defines how the application should interact with Supabase:
- Snapshot-first dashboards
- Use
get-dashboard-snapshot-v2and other aggregated endpoints where possible. - Avoid stitching together many small REST calls on the client.
- Use
- Cache-first client behavior
- Rely on React Query caches and IndexedDB before hitting Supabase again.
- Treat caches as the primary source of truth between user actions.
- Realtime-over-polling
- Use Supabase Realtime for high-frequency updates (e.g. shipments, driver locations).
- Polling is allowed only as a slow fallback (30–60s) and must be clearly documented.
- No REST on hover / animation
- Hover handlers,
onMouseMove, and animation/simulation loops must never call Supabase orfetch. - These paths may only read from already-fetched, cached data.
- Hover handlers,
- Simulation is local
- All simulation engines operate entirely on in-memory state.
- They consume snapshots but do not perform network I/O from their tick loops.
React Query Caching Rules
Global Defaults (QueryClient)
The global QueryClient is configured with stability-oriented defaults:
staleTime: 30_000(30 seconds)- Data is considered fresh for 30 seconds (good balance for dashboards and maps).
gcTime: 60_000(60 seconds)- Cache entries are retained for 60 seconds for quick back/forward navigation.
refetchOnWindowFocus: false- No hidden refetch bursts when the user switches browser tabs.
refetchOnReconnect: false- Avoids reconnect storms on flaky networks.
refetchOnMount: false(while data is fresh)- Components do not automatically re-fetch when remounted if data is still within
staleTime.
- Components do not automatically re-fetch when remounted if data is still within
retry: 1with exponential backoff- Handles transient Supabase/network issues without hammering the backend.
Per-Hook Overrides
Feature hooks can override defaults when needed:
- Critical live views (e.g. active driver lists, live safety KPIs) may set:
staleTime: 5_000–10_000for near-realtime freshness.refetchOnWindowFocus: trueonly where it is clearly desired.
- Background or rarely changing data (e.g. settings, static reference data) can:
- Increase
staleTimebeyond 30s. - Increase
gcTimeif memory pressure is not a concern.
- Increase
Polling and Refetch Intervals
- High-frequency polling (
< 30s) is not allowed for production views. - If realtime is unavailable or unreliable:
- Use a slow
refetchInterval(30–60s) with comments explaining why. - Prefer explicit refresh buttons +
invalidateQueriesover constant polling.
- Use a slow
Changes Implemented
1. Lightweight Dashboard Mode
- New Parameter:
lightweight=truequery parameter - Behavior: Returns only metrics and counts, no full item lists
- Size Reduction: ~60-70% for KPI dashboards
- Usage:typescript
fetchDashboardSnapshotV2({ lightweight: true })
2. Pagination Support
- New Parameters:
pageandpageSizequery parameters - Default: page=1, pageSize=50
- Max: pageSize=200
- Applied To: Zones, inventory items, pick lists, shipments, drivers, safety incidents
3. Field Selection Optimization
Edge Functions
- Zones: Reduced from 7 fields to 3 in lightweight mode
- Inventory: Reduced from 7 fields to 3 in lightweight mode, limit reduced from 1000 to 200
- Pick Lists: Reduced from 4 fields to 2 in lightweight mode
- Shipments: Reduced from 4 fields to 2 in lightweight mode
- Drivers: Removed
raw_user_meta_data(can be large JSONB), reduced to 2-3 fields - Safety: Reduced from 5 fields to 3 in lightweight mode, limit reduced from 100 to 50
- Stock Movements: Reduced from 500 to 200 items, removed
zone_idif not needed
Client-Side Queries
- Inventory Items: Replaced
select('*')with specific field list (11 fields) - Stock Movements: Replaced
select('*')with specific field list, limit reduced from 1000 to 100 - Shipments: Replaced
select('*')with specific field list (11 fields), added limit 100 - Pick Lists: Replaced
select('*')with specific field list (7 fields), added limit 100
4. Compression
- Automatic: Deno.serve automatically handles gzip/deflate compression
- Headers: Added
Vary: Accept-Encodingheader - Client: Sends
Accept-Encoding: gzip, deflateheader
5. Removed Unused Fields
- Drivers: Removed
raw_user_meta_data(large JSONB field) - Stock Movements: Removed
zone_idwhen not needed for aggregation - Import Jobs: Reduced fields in lightweight mode (status, type only)
API Changes
get-dashboard-snapshot-v2 Edge Function
New Query Parameters:
lightweight=true- Return metrics only, no item listspage=1- Page number (default: 1)pageSize=50- Items per page (default: 50, max: 200)
Example Requests:
# Full data (default)
GET /functions/v1/get-dashboard-snapshot-v2
# Lightweight mode (metrics only)
GET /functions/v1/get-dashboard-snapshot-v2?lightweight=true
# Paginated
GET /functions/v1/get-dashboard-snapshot-v2?page=2&pageSize=100
# Lightweight + Paginated
GET /functions/v1/get-dashboard-snapshot-v2?lightweight=true&page=1&pageSize=50Client-Side Changes
Updated Functions
fetchDashboardSnapshotV2()- Now accepts options:typescriptfetchDashboardSnapshotV2({ useCache: true, lightweight: false, page: 1, pageSize: 50 })useInventoryItems()- Now selects specific fields instead of*useStockMovements()- Reduced limit from 1000 to 100, specific fieldsuseShipments()- Added limit 100, specific fieldsusePickLists()- Added limit 100, specific fieldsReact Query Behavior – All of the above hooks now:
- Respect the global
staleTime/gcTimedefaults unless overridden. - Do not use
refetchOnWindowFocusby default. - Can be configured with
enabledflags so they only run when inputs (IDs, toggles) are present.
- Respect the global
Expected Size Reductions
| Endpoint | Before | After (Full) | After (Lightweight) | Reduction |
|---|---|---|---|---|
| Dashboard Snapshot | ~500KB | ~200KB | ~50KB | 60-90% |
| Inventory Items (1000) | ~800KB | ~300KB | N/A | 62% |
| Stock Movements (500) | ~400KB | ~150KB | N/A | 62% |
| Shipments (unlimited) | ~600KB | ~200KB | N/A | 67% |
Migration Guide
For KPI Dashboards
Use lightweight mode for dashboards that only show metrics:
const { data } = useDashboardSnapshotV2({ lightweight: true });
// Returns: { zones: { total: 10 }, inventory: { metrics: {...} }, ... }
// No items arraysFor Detailed Views
Use full mode with pagination:
const { data } = useDashboardSnapshotV2({
lightweight: false,
page: 1,
pageSize: 50
});
// Returns: { zones: { total: 10, items: [...] }, ... }Backward Compatibility
- All changes are additive – existing code continues to work.
- Default behavior for existing endpoints remains unchanged (full data, no pagination).
- New snapshot parameters and caching rules are opt-in where stricter behavior is required.
Testing
- Lightweight Mode: Verify KPI dashboards still show correct metrics
- Pagination: Verify list views load correctly with pagination
- Field Selection: Verify all displayed fields are still present
- Compression: Check network tab for
Content-Encoding: gzipheader
Performance Monitoring
Monitor these metrics:
- Response size (should decrease by 30-70%)
- Time to First Byte (TTFB) - should improve with compression
- Total load time - should improve with smaller payloads
- Cache hit rate - should remain stable
Implementation Status
✅ Completed:
- Lightweight mode for dashboard snapshot
- Pagination support (page, pageSize)
- Field selection optimization (removed
select('*')) - Compression headers (Vary: Accept-Encoding)
- Reduced limits (inventory: 1000→200, stock movements: 500→200, safety: 100→50)
- Removed large JSONB fields (raw_user_meta_data)
Architecture Diagram
Dashboard Snapshot Flow
┌─────────────────┐
│ React Client │
│ (Dashboard) │
└────────┬────────┘
│
│ 1. Request (with lightweight/pagination params)
▼
┌─────────────────────────────────────┐
│ Edge Function: │
│ get-dashboard-snapshot-v2 │
│ │
│ ┌──────────────────────────────┐ │
│ │ Check Cache (60s TTL) │ │
│ │ Key: snapshot:lw:1:50 │ │
│ └───────────┬──────────────────┘ │
│ │ │
│ │ Cache Miss │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Parallel Queries: │ │
│ │ • Zones (paginated) │ │
│ │ • Inventory (paginated) │ │
│ │ • Pick Lists (paginated) │ │
│ │ • Shipments (paginated) │ │
│ │ • Drivers (paginated) │ │
│ │ • Safety (paginated) │ │
│ │ • Import Jobs │ │
│ └───────────┬──────────────────┘ │
│ │ │
│ │ Aggregate │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Build Snapshot Response │ │
│ │ (lightweight or full) │ │
│ └───────────┬──────────────────┘ │
│ │ │
│ │ Cache Result │
│ ▼ │
└──────────────┼─────────────────────┘
│
│ 2. Compressed Response (gzip)
▼
┌─────────────────────────────────────┐
│ React Client │
│ │
│ ┌──────────────────────────────┐ │
│ │ Update React Query Cache │ │
│ │ (30s staleTime, 60s gcTime) │ │
│ └───────────┬──────────────────┘ │
│ │ │
│ │ Update Static Cache │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ DashboardCache.set() │ │
│ │ (60s TTL) │ │
│ └──────────────────────────────┘ │
└─────────────────────────────────────┘Stale-While-Revalidate Pattern
Time →
│
│ Request 1: Cache Miss
├─► Fetch from Edge Function
│ └─► Store in cache (fresh)
│
│ Request 2: Cache Hit (within staleTime)
├─► Return cached data immediately
│ └─► Background: Refresh cache
│
│ Request 3: Cache Stale (after staleTime)
├─► Return cached data immediately (stale)
│ └─► Background: Fetch fresh data
│ └─► Update cache when ready
│
│ Request 4: Cache Expired (after gcTime)
├─► Cache Miss
│ └─► Fetch fresh dataBest Practices
For KPI Dashboards
Use lightweight mode for dashboards that only show metrics:
const { data } = useDashboardSnapshotV2({ lightweight: true });
// Returns: { zones: { total: 10 }, inventory: { metrics: {...} }, ... }
// No items arrays - 60-70% smaller responseFor Detailed Views
Use full mode with pagination:
const { data } = useDashboardSnapshotV2({
lightweight: false,
page: 1,
pageSize: 50
});
// Returns: { zones: { total: 10, items: [...] }, ... }Cache Invalidation
Invalidate cache when data changes:
import { invalidateDashboardCache } from '@/lib/queries';
// After mutation
await updateInventoryItem(itemId, updates);
invalidateDashboardCache(); // Clear client-side cacheError Handling
Always provide fallback:
try {
const data = await fetchDashboardSnapshotV2({ lightweight: true });
return data;
} catch (error) {
// Fallback to old endpoint or direct queries
console.warn('Snapshot unavailable, using fallback');
return await fetchAdminDashboardSummary();
}Implementation Status
✅ Completed:
- Lightweight mode for dashboard snapshot
- Pagination support (
page,pageSize) - Field selection optimization (removed
select('*')) - Compression headers (
Vary: Accept-Encoding) - Reduced limits (inventory: 1000→200, stock movements: 500→200, safety: 100→50)
- Removed large JSONB fields (
raw_user_meta_data) - React Query stability defaults (30s
staleTime, 60sgcTime, norefetchOnWindowFocus) - Explicit "no REST on hover / animation" rule documented
🚫 Out of scope in this document:
- UI/layout changes
- New features or modules
- Database schema changes beyond performance indexes and views