Skip to main content

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.

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 AreaPatternImpact
Route Code SplittingReact.lazy()High - Reduces initial bundle
Query OptimizationSelect only needed columnsHigh - Faster queries
Image OptimizationWebP format, lazy loadingMedium - Faster page loads
CachingQueryClient staleTimeHigh - Fewer API calls
Bundle SizeTree-shakeable importsMedium - Smaller bundles

Performance Debugging Workflow

1. Measure Current Performance

Lighthouse Audit:
# Run Lighthouse in Chrome DevTools
# Or use CLI:
npx lighthouse http://localhost:5173 --view
Bundle Analysis:
# 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
// 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
// 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
// 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
// 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). Pattern (illustrative — match real vite.config.ts for full vendor split list):
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
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
// 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
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
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
// Specific key - invalidates only this query
useQuery({
  queryKey: ['hr-employees', orgId, departmentId],
  queryFn: () => fetchEmployees(orgId, departmentId),
});
❌ WRONG: Too broad query keys
// Too broad - invalidates all employee queries
useQuery({
  queryKey: ['hr-employees'],  // Missing parameters!
  queryFn: () => fetchEmployees(orgId),
});

Image Optimization

Use Modern Formats

✅ CORRECT: WebP format
<img 
  src="/images/logo.webp" 
  alt="Logo"
  loading="lazy"
/>
❌ WRONG: Large PNG/JPG
<img src="/images/logo.png" />  // May be large file

Lazy Loading

✅ CORRECT: Lazy load images
// 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
<img 
  srcSet="/images/hero-320w.webp 320w,
          /images/hero-640w.webp 640w,
          /images/hero-1280w.webp 1280w"
  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:
const RouteComponent = lazy(() => import('./RouteComponent'));

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

Component Lazy Loading

For heavy components:
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:
async function loadFeature() {
  const { FeatureComponent } = await import('./FeatureComponent');
  return FeatureComponent;
}

Caching Strategies

QueryClient Caching

Automatic caching with TanStack Query:
// Data cached for staleTime (5 minutes)
const { data } = useQuery({
  queryKey: ['employees', orgId],
  queryFn: () => fetchEmployees(orgId),
  staleTime: 5 * 60 * 1000,  // 5 minutes
});

Manual Cache Invalidation

// 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

# 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

Standards

Development Guides

Performance Resources


Maintained By: Platform Foundation Team