> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Performance Optimization Guide

> Version: 1.0.0 Last Updated: 2025-12-31 Target Audience: Developers and AI agents (GitHub Copilot, Cursor, general AI assistants)

**Version:** 1.0.0\
**Last Updated:** 2025-12-31\
**Target Audience:** Developers and AI agents (GitHub Copilot, Cursor, general AI assistants)

Comprehensive guide for performance optimization in the Encore Health OS Platform, covering debugging workflows, bundle optimization, query optimization, image optimization, lazy loading, caching, and performance targets.

***

## AI Agent Context

**Key Patterns for AI:**

* Always use `React.lazy()` for route components (required for code splitting)
* Always configure QueryClient with `staleTime` and `gcTime` (prevents unnecessary refetches)
* Always select only needed columns in database queries (never `SELECT *`)
* Always use skeleton loaders for loading states (never `return null`)
* Always optimize images (WebP format, lazy loading, proper sizing)

**Common Mistakes to Avoid:**

* Direct imports for route components (must use `React.lazy()`)
* Missing QueryClient configuration (causes excessive API calls)
* Using `SELECT *` in queries (fetches unnecessary data)
* Returning `null` for loading states (causes layout shifts)
* Not optimizing images (slows page loads)

***

## Quick Reference

| Optimization Area    | Pattern                    | Impact                        |
| -------------------- | -------------------------- | ----------------------------- |
| Route Code Splitting | `React.lazy()`             | High - Reduces initial bundle |
| Query Optimization   | Select only needed columns | High - Faster queries         |
| Image Optimization   | WebP format, lazy loading  | Medium - Faster page loads    |
| Caching              | QueryClient staleTime      | High - Fewer API calls        |
| Bundle Size          | Tree-shakeable imports     | Medium - Smaller bundles      |

***

## Performance Debugging Workflow

### 1. Measure Current Performance

**Lighthouse Audit:**

```bash theme={null}
# Run Lighthouse in Chrome DevTools
# Or use CLI:
npx lighthouse http://localhost:5173 --view
```

**Bundle Analysis:**

```bash theme={null}
# Build and analyze bundle
npm run build
npx vite-bundle-visualizer
```

**Network Analysis:**

* Open Chrome DevTools → Network tab
* Check for large files, slow requests
* Look for duplicate requests

### 2. Identify Bottlenecks

**Common Issues:**

* Large initial bundle (>500KB)
* Slow queries (>1s)
* Unoptimized images
* Missing code splitting
* Excessive re-renders

### 3. Apply Optimizations

Follow optimization patterns below based on identified issues.

### 4. Verify Improvements

* Re-run Lighthouse audit
* Compare bundle sizes
* Test on slow 3G connection
* Verify performance targets met

***

## Bundle Size Optimization

### Route Code Splitting (REQUIRED)

**✅ CORRECT: Use React.lazy() for all routes**

```typescript theme={null}
// In App.tsx
import { lazy, Suspense } from 'react';
import { RouteLoadingSkeleton } from '@/platform/navigation/components';

const Dashboard = lazy(() => import('./platform/dashboard/Dashboard'));
const EmployeesPage = lazy(() => import('./cores/hr/pages/EmployeesPage'));

// Wrap routes in Suspense
<Suspense fallback={<RouteLoadingSkeleton />}>
  <Routes>
    <Route path="/" element={<Dashboard />} />
    <Route path="/hr/employees" element={<EmployeesPage />} />
  </Routes>
</Suspense>
```

**❌ WRONG: Direct imports for routes**

```typescript theme={null}
// This loads all routes in initial bundle!
import Dashboard from './platform/dashboard/Dashboard';
import EmployeesPage from './cores/hr/pages/EmployeesPage';
```

### Tree-Shakeable Imports

**✅ CORRECT: Import only what you need**

```typescript theme={null}
// Tree-shakeable - only imports Button
import { Button } from '@/shared/ui/button';

// Tree-shakeable - only imports specific icons
import { Plus, Trash2 } from 'lucide-react';
```

**❌ WRONG: Import entire libraries**

```typescript theme={null}
// Imports entire library - larger bundle
import * as Icons from 'lucide-react';
import * as Components from '@/shared/ui';
```

### Manual Chunk Splitting (Vite 8 / Rolldown)

**Location:** `vite.config.ts`

Vite 8 bundles with Rolldown. The old Rollup object form `output.manualChunks` is removed; use **`build.rolldownOptions.output.codeSplitting.groups`** with `test` regexes and `priority` (see [Vite migration guide](https://github.com/vitejs/vite/blob/main/docs/guide/migration.md)).

**Pattern (illustrative — match real `vite.config.ts` for full vendor split list):**

```typescript theme={null}
build: {
  rolldownOptions: {
    output: {
      codeSplitting: {
        groups: [
          {
            name: 'vendor-react',
            test: /node_modules\/(?:react-dom|react-router|scheduler|react(?:\/|$)|@tanstack\/)/,
            priority: 100,
          },
          {
            name: 'vendor-radix',
            test: /node_modules[\\/]@radix-ui[\\/]/,
            priority: 90,
          },
          {
            name: 'vendor-supabase',
            test: /node_modules[\\/]@supabase[\\/]/,
            priority: 80,
          },
          // ... additional groups (charts, pdf, editor, etc.)
        ],
      },
    },
  },
}
```

**Benefits:**

* Better caching (vendor chunks change less frequently)
* Parallel loading
* Smaller initial bundle

***

## Query Optimization

### Select Only Needed Columns

**✅ CORRECT: Select specific columns**

```typescript theme={null}
const { data } = await supabase
  .from('hr_employees')
  .select('id, full_name, email, employee_number')  // Only needed columns
  .eq('organization_id', orgId);
```

**❌ WRONG: Select all columns**

```typescript theme={null}
// Fetches all columns, including large JSONB fields
const { data } = await supabase
  .from('hr_employees')
  .select('*')  // Too much data!
  .eq('organization_id', orgId);
```

### Use Pagination

**✅ CORRECT: Limit results**

```typescript theme={null}
const { data } = await supabase
  .from('hr_employees')
  .select('id, full_name')
  .eq('organization_id', orgId)
  .range(0, 49)  // First 50 records
  .order('created_at', { ascending: false });
```

### QueryClient Configuration (REQUIRED)

**Location:** `src/App.tsx`

```typescript theme={null}
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,  // 5 minutes - data fresh for 5 min
      gcTime: 10 * 60 * 1000,    // 10 minutes - cache for 10 min
      retry: 1,                   // Retry once on failure
      refetchOnWindowFocus: false, // Don't refetch on window focus (PWA)
    },
  },
});
```

**Benefits:**

* Reduces API calls (cached for 5 minutes)
* Faster UI (instant data from cache)
* Better offline experience

### Optimize Query Keys

**✅ CORRECT: Specific query keys**

```typescript theme={null}
// Specific key - invalidates only this query
useQuery({
  queryKey: ['hr-employees', orgId, departmentId],
  queryFn: () => fetchEmployees(orgId, departmentId),
});
```

**❌ WRONG: Too broad query keys**

```typescript theme={null}
// Too broad - invalidates all employee queries
useQuery({
  queryKey: ['hr-employees'],  // Missing parameters!
  queryFn: () => fetchEmployees(orgId),
});
```

***

## Image Optimization

### Use Modern Formats

**✅ CORRECT: WebP format**

```tsx theme={null}
<img 
  src="/images/logo.webp" 
  alt="Logo"
  loading="lazy"
/>
```

**❌ WRONG: Large PNG/JPG**

```tsx theme={null}
<img src="/images/logo.png" />  // May be large file
```

### Lazy Loading

**✅ CORRECT: Lazy load images**

```tsx theme={null}
// Native lazy loading
<img 
  src="/images/hero.webp" 
  loading="lazy"  // Loads when near viewport
  alt="Hero image"
/>

// Or use Intersection Observer for more control
```

### Responsive Images

**✅ CORRECT: Responsive srcset**

```tsx theme={null}
<img
  sizes="(max-width: 640px) 320px,
         (max-width: 1280px) 640px,
         1280px"
  src="/images/hero-1280w.webp"
  alt="Hero image"
/>
```

### Image Sizing

* **Icons:** 16-32px (SVG preferred)
* **Thumbnails:** 64-128px
* **Hero images:** Max 1920px width
* **Use appropriate formats:** SVG for icons, WebP for photos

***

## Lazy Loading Patterns

### Route Lazy Loading (REQUIRED)

**Pattern:**

```typescript theme={null}
const RouteComponent = lazy(() => import('./RouteComponent'));

<Suspense fallback={<RouteLoadingSkeleton />}>
  <RouteComponent />
</Suspense>
```

### Component Lazy Loading

**For heavy components:**

```typescript theme={null}
const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);
  
  return (
    <div>
      <Button onClick={() => setShowChart(true)}>Show Chart</Button>
      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}
```

### Dynamic Imports

**For conditional features:**

```typescript theme={null}
async function loadFeature() {
  const { FeatureComponent } = await import('./FeatureComponent');
  return FeatureComponent;
}
```

***

## Caching Strategies

### QueryClient Caching

**Automatic caching with TanStack Query:**

```typescript theme={null}
// Data cached for staleTime (5 minutes)
const { data } = useQuery({
  queryKey: ['employees', orgId],
  queryFn: () => fetchEmployees(orgId),
  staleTime: 5 * 60 * 1000,  // 5 minutes
});
```

### Manual Cache Invalidation

```typescript theme={null}
// Invalidate after mutation
const { mutate } = useMutation({
  mutationFn: updateEmployee,
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['employees'] });
  },
});
```

### Browser Caching

**Static assets (handled by Vite):**

* Files with content hashes are cached long-term
* Changed files get new hashes (cache busting)

**API responses:**

* Use QueryClient caching (not browser cache)
* Configure staleTime appropriately

***

## Performance Targets

### Lighthouse Scores

**Targets:**

* **Performance:** 85+
* **Accessibility:** 90+
* **Best Practices:** 90+
* **SEO:** 90+
* **PWA:** 90+

### Core Web Vitals

**Targets:**

* **First Contentful Paint (FCP):** \< 2s
* **Largest Contentful Paint (LCP):** \< 2.5s
* **Cumulative Layout Shift (CLS):** \< 0.1
* **Time to Interactive (TTI):** \< 3.5s on 3G
* **Total Blocking Time (TBT):** \< 300ms

### Bundle Size Targets

* **Initial bundle:** \< 200KB (gzipped)
* **Route chunks:** \< 100KB each (gzipped)
* **Vendor chunks:** \< 300KB total (gzipped)

***

## Performance Monitoring

### Chrome DevTools

**Performance Tab:**

1. Open DevTools → Performance
2. Click Record
3. Interact with app
4. Stop recording
5. Analyze flame chart

**Network Tab:**

* Check file sizes
* Look for slow requests
* Identify duplicate requests

### Bundle Analysis

```bash theme={null}
# Build and analyze
npm run build
npx vite-bundle-visualizer

# Check output for:
# - Large chunks
# - Duplicate dependencies
# - Unused code
```

### Real User Monitoring

**Consider adding:**

* Web Vitals tracking
* Error tracking (Sentry)
* Performance API monitoring

***

## Common Performance Issues

### Issue: Large Initial Bundle

**Symptoms:** Slow initial page load, large bundle size

**Solutions:**

1. Verify all routes use `React.lazy()`
2. Check for direct route imports
3. Analyze bundle with `vite-bundle-visualizer`
4. Split vendor chunks in `vite.config.ts`

### Issue: Slow Queries

**Symptoms:** Queries take > 1 second

**Solutions:**

1. Select only needed columns (not `*`)
2. Add pagination (`.range()`)
3. Add indexes for frequently queried columns
4. Use QueryClient caching

### Issue: Excessive Re-renders

**Symptoms:** UI feels sluggish, many re-renders

**Solutions:**

1. Use `React.memo()` for expensive components
2. Use `useMemo()` for expensive calculations
3. Use `useCallback()` for stable function references
4. Check for unnecessary state updates

### Issue: Images Loading Slowly

**Symptoms:** Images take time to appear

**Solutions:**

1. Convert to WebP format
2. Add `loading="lazy"` attribute
3. Use responsive images with srcset
4. Optimize image sizes

***

## Related Documentation

### Standards

* [Constitution](../../constitution.md) §6.5 - Frontend Performance Patterns
* [UI/UX Standards](./UI_UX_STANDARDS.md) - Performance UX patterns

### Development Guides

* [Troubleshooting Guide](./TROUBLESHOOTING_GUIDE.md) - Performance debugging
* [Database Development Guide](./DATABASE_DEVELOPMENT_GUIDE.md) - Query optimization

### Performance Resources

* [Performance Audit](../../docs/migration/PERFORMANCE_AUDIT.md) - Recent audit results
* [Vite Performance](https://vitejs.dev/guide/performance.html) - Vite optimization guide

***

**Maintained By:** Platform Foundation Team
