Skip to content

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:

  1. Snapshot-first dashboards
    • Use get-dashboard-snapshot-v2 and other aggregated endpoints where possible.
    • Avoid stitching together many small REST calls on the client.
  2. 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.
  3. 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.
  4. No REST on hover / animation
    • Hover handlers, onMouseMove, and animation/simulation loops must never call Supabase or fetch.
    • These paths may only read from already-fetched, cached data.
  5. 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.
  • retry: 1 with 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_00010_000 for near-realtime freshness.
    • refetchOnWindowFocus: true only where it is clearly desired.
  • Background or rarely changing data (e.g. settings, static reference data) can:
    • Increase staleTime beyond 30s.
    • Increase gcTime if memory pressure is not a concern.

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 + invalidateQueries over constant polling.

Changes Implemented

1. Lightweight Dashboard Mode

  • New Parameter: lightweight=true query 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: page and pageSize query 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_id if 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-Encoding header
  • Client: Sends Accept-Encoding: gzip, deflate header

5. Removed Unused Fields

  • Drivers: Removed raw_user_meta_data (large JSONB field)
  • Stock Movements: Removed zone_id when 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 lists
  • page=1 - Page number (default: 1)
  • pageSize=50 - Items per page (default: 50, max: 200)

Example Requests:

bash
# 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=50

Client-Side Changes

Updated Functions

  1. fetchDashboardSnapshotV2() - Now accepts options:

    typescript
    fetchDashboardSnapshotV2({
      useCache: true,
      lightweight: false,
      page: 1,
      pageSize: 50
    })
  2. useInventoryItems() - Now selects specific fields instead of *

  3. useStockMovements() - Reduced limit from 1000 to 100, specific fields

  4. useShipments() - Added limit 100, specific fields

  5. usePickLists() - Added limit 100, specific fields

  6. React Query Behavior – All of the above hooks now:

    • Respect the global staleTime / gcTime defaults unless overridden.
    • Do not use refetchOnWindowFocus by default.
    • Can be configured with enabled flags so they only run when inputs (IDs, toggles) are present.

Expected Size Reductions

EndpointBeforeAfter (Full)After (Lightweight)Reduction
Dashboard Snapshot~500KB~200KB~50KB60-90%
Inventory Items (1000)~800KB~300KBN/A62%
Stock Movements (500)~400KB~150KBN/A62%
Shipments (unlimited)~600KB~200KBN/A67%

Migration Guide

For KPI Dashboards

Use lightweight mode for dashboards that only show metrics:

typescript
const { data } = useDashboardSnapshotV2({ lightweight: true });
// Returns: { zones: { total: 10 }, inventory: { metrics: {...} }, ... }
// No items arrays

For Detailed Views

Use full mode with pagination:

typescript
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

  1. Lightweight Mode: Verify KPI dashboards still show correct metrics
  2. Pagination: Verify list views load correctly with pagination
  3. Field Selection: Verify all displayed fields are still present
  4. Compression: Check network tab for Content-Encoding: gzip header

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 data

Best Practices

For KPI Dashboards

Use lightweight mode for dashboards that only show metrics:

typescript
const { data } = useDashboardSnapshotV2({ lightweight: true });
// Returns: { zones: { total: 10 }, inventory: { metrics: {...} }, ... }
// No items arrays - 60-70% smaller response

For Detailed Views

Use full mode with pagination:

typescript
const { data } = useDashboardSnapshotV2({ 
  lightweight: false,
  page: 1,
  pageSize: 50 
});
// Returns: { zones: { total: 10, items: [...] }, ... }

Cache Invalidation

Invalidate cache when data changes:

typescript
import { invalidateDashboardCache } from '@/lib/queries';

// After mutation
await updateInventoryItem(itemId, updates);
invalidateDashboardCache(); // Clear client-side cache

Error Handling

Always provide fallback:

typescript
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, 60s gcTime, no refetchOnWindowFocus)
  • 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

Released under Commercial License