Skip to content

Performance Optimization

Learn how to optimize performance when using @pubflow/flowfull-client.

TIP

@pubflow/flowfull-client is designed to be lightweight and performant. This guide shows you best practices for optimal performance.

Request Optimization

Debounce search queries to reduce API calls:

typescript
// src/hooks/useDebounce.ts
import { useState, useEffect } from 'react';

export function useDebounce<T>(value: T, delay: number = 300): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Usage
import { useState, useEffect } from 'react';
import { api } from '../api/client';
import { useDebounce } from '../hooks/useDebounce';

function SearchUsers() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      searchUsers(debouncedQuery);
    }
  }, [debouncedQuery]);

  async function searchUsers(q: string) {
    const response = await api.query('/users')
      .where('name', 'like', `%${q}%`)
      .limit(10)
      .get();

    if (response.success) {
      setResults(response.data || []);
    }
  }

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search users..."
      />
      <ul>
        {results.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Request Cancellation

Cancel pending requests when component unmounts:

typescript
// src/hooks/useApiRequest.ts
import { useState, useEffect, useRef } from 'react';
import { api } from '../api/client';

export function useApiRequest<T>(endpoint: string) {
  const [data, setData] = useState<T | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const isMountedRef = useRef(true);

  useEffect(() => {
    return () => {
      isMountedRef.current = false;
    };
  }, []);

  async function fetchData() {
    setIsLoading(true);
    setError(null);

    try {
      const response = await api.get(endpoint);

      if (isMountedRef.current && response.success) {
        setData(response.data);
      }
    } catch (err) {
      if (isMountedRef.current) {
        setError(err as Error);
      }
    } finally {
      if (isMountedRef.current) {
        setIsLoading(false);
      }
    }
  }

  useEffect(() => {
    fetchData();
  }, [endpoint]);

  return { data, isLoading, error, refetch: fetchData };
}

Caching Strategies

Manual Caching

Implement simple caching for frequently accessed data:

typescript
// src/utils/cache.ts
class SimpleCache<T> {
  private cache = new Map<string, { data: T; timestamp: number }>();
  private ttl: number;

  constructor(ttlMinutes: number = 5) {
    this.ttl = ttlMinutes * 60 * 1000;
  }

  set(key: string, data: T): void {
    this.cache.set(key, { data, timestamp: Date.now() });
  }

  get(key: string): T | null {
    const cached = this.cache.get(key);

    if (!cached) return null;

    const isExpired = Date.now() - cached.timestamp > this.ttl;

    if (isExpired) {
      this.cache.delete(key);
      return null;
    }

    return cached.data;
  }

  clear(): void {
    this.cache.clear();
  }
}

export const apiCache = new SimpleCache(5); // 5 minutes TTL

// Usage
import { api } from '../api/client';
import { apiCache } from '../utils/cache';

async function getUsers() {
  const cacheKey = 'users_list';

  // Check cache first
  const cached = apiCache.get(cacheKey);
  if (cached) {
    return cached;
  }

  // Fetch from API
  const response = await api.query('/users').limit(20).get();

  if (response.success) {
    apiCache.set(cacheKey, response.data);
    return response.data;
  }

  return [];
}

Using TanStack Query

For advanced caching, use TanStack Query:

bash
npm install @tanstack/react-query
typescript
// src/App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      refetchOnWindowFocus: false,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

// src/hooks/useUsers.ts
import { useQuery } from '@tanstack/react-query';
import { api } from '../api/client';

export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await api.query('/users').limit(20).get();
      return response.data || [];
    },
  });
}

// Usage in component
function UserList() {
  const { data: users, isLoading } = useUsers();

  if (isLoading) return <div>Loading...</div>;

  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Pagination Optimization

Infinite Scroll

Implement efficient infinite scroll:

typescript
// src/components/InfiniteUserList.tsx
import { useState, useEffect } from 'react';
import { api } from '../api/client';

interface User {
  id: string;
  name: string;
  email: string;
}

function InfiniteUserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    loadMore();
  }, [page]);

  async function loadMore() {
    if (isLoading || !hasMore) return;

    setIsLoading(true);

    const response = await api.query('/users')
      .page(page)
      .limit(20)
      .get<User[]>();

    if (response.success) {
      setUsers(prev => [...prev, ...(response.data || [])]);
      setHasMore(response.meta?.has_more ?? false);
    }

    setIsLoading(false);
  }

  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
      {hasMore && (
        <button onClick={() => setPage(p => p + 1)} disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

Virtual Scrolling

Use virtual scrolling for large lists:

bash
npm install react-window
typescript
// src/components/VirtualUserList.tsx
import { useState, useEffect } from 'react';
import { FixedSizeList } from 'react-window';
import { api } from '../api/client';

function VirtualUserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetchUsers();
  }, []);

  async function fetchUsers() {
    const response = await api.query('/users').limit(1000).get();
    if (response.success) {
      setUsers(response.data || []);
    }
  }

  const Row = ({ index, style }: any) => (
    <div style={style}>
      {users[index]?.name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={users.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

Bundle Size Optimization

Tree Shaking

Import only what you need:

typescript
// ❌ Bad - imports everything
import * as FlowfullClient from '@pubflow/flowfull-client';

// ✅ Good - imports only what's needed
import { createFlowfull } from '@pubflow/flowfull-client';

Code Splitting

Split code by route:

typescript
// src/App.tsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const Users = lazy(() => import('./pages/Users'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Products = lazy(() => import('./pages/Products'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/users" element={<Users />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/products" element={<Products />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Query Builder Optimization

Reuse Query Builders

Create reusable query builders:

typescript
// src/queries/userQueries.ts
import { api } from '../api/client';

export const userQueries = {
  list: (page: number = 1, limit: number = 20) =>
    api.query('/users').page(page).limit(limit),

  search: (query: string) =>
    api.query('/users').where('name', 'like', `%${query}%`).limit(10),

  active: () =>
    api.query('/users').where('status', 'eq', 'active').sort('created_at', 'desc'),

  byRole: (role: string) =>
    api.query('/users').where('role', 'eq', role),
};

// Usage
import { userQueries } from '../queries/userQueries';

async function getActiveUsers() {
  const response = await userQueries.active().get();
  return response.data || [];
}

Batch Requests

Batch multiple requests when possible:

typescript
// src/utils/batchRequests.ts
import { api } from '../api/client';

async function fetchDashboardData() {
  // Execute requests in parallel
  const [usersResponse, productsResponse, ordersResponse] = await Promise.all([
    api.query('/users').limit(10).get(),
    api.query('/products').limit(10).get(),
    api.query('/orders').limit(10).get(),
  ]);

  return {
    users: usersResponse.data || [],
    products: productsResponse.data || [],
    orders: ordersResponse.data || [],
  };
}

Monitoring

Performance Metrics

Monitor API performance:

typescript
// src/utils/apiMonitor.ts
import { createFlowfull } from '@pubflow/flowfull-client';

class ApiMonitor {
  private metrics: Map<string, number[]> = new Map();

  logRequest(endpoint: string, duration: number) {
    if (!this.metrics.has(endpoint)) {
      this.metrics.set(endpoint, []);
    }
    this.metrics.get(endpoint)!.push(duration);
  }

  getAverageDuration(endpoint: string): number {
    const durations = this.metrics.get(endpoint) || [];
    if (durations.length === 0) return 0;
    return durations.reduce((a, b) => a + b, 0) / durations.length;
  }

  getMetrics() {
    const result: Record<string, { avg: number; count: number }> = {};

    this.metrics.forEach((durations, endpoint) => {
      result[endpoint] = {
        avg: this.getAverageDuration(endpoint),
        count: durations.length,
      };
    });

    return result;
  }
}

export const apiMonitor = new ApiMonitor();

// Wrapper function to monitor requests
export async function monitoredRequest<T>(
  endpoint: string,
  requestFn: () => Promise<T>
): Promise<T> {
  const start = performance.now();

  try {
    const result = await requestFn();
    const duration = performance.now() - start;
    apiMonitor.logRequest(endpoint, duration);
    return result;
  } catch (error) {
    const duration = performance.now() - start;
    apiMonitor.logRequest(endpoint, duration);
    throw error;
  }
}

// Usage
import { api } from '../api/client';
import { monitoredRequest } from '../utils/apiMonitor';

async function getUsers() {
  return monitoredRequest('/users', async () => {
    const response = await api.query('/users').limit(20).get();
    return response.data || [];
  });
}

Best Practices

  1. Debounce Search: Always debounce search inputs (300ms recommended)
  2. Use Pagination: Load data in pages instead of all at once
  3. Cache Responses: Cache frequently accessed data
  4. Batch Requests: Execute independent requests in parallel
  5. Virtual Scrolling: Use virtual scrolling for large lists
  6. Code Splitting: Split code by route to reduce initial bundle size
  7. Monitor Performance: Track API performance metrics
  8. Cleanup: Clean up resources when components unmount

Next Steps