🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →

Performance e Otimização

Aula 3 - Módulo 6: Técnicas avançadas para aplicações tRPC de alta performance

🎯 Por que Performance é Crítica em tRPC?

⚡ Experiência do Usuário

Cada milissegundo importa. Aplicações tRPC lentas resultam em menor engajamento, conversões reduzidas e usuários frustrados. Performance não é opcional.

💰 Impacto nos Negócios

Amazon descobriu que 100ms de latência custam 1% das vendas. Para aplicações enterprise, performance otimizada significa ROI direto.

🔧 Escalabilidade

Aplicações otimizadas suportam mais usuários com menos recursos. Cache inteligente e queries eficientes reduzem drasticamente a carga no servidor.

📊 Métricas que Importam

Time to First Byte (TTFB), Largest Contentful Paint (LCP) e Cumulative Layout Shift (CLS) definem a percepção de qualidade.

⚠️ Conceitos Importantes para Entender

Client-Side Caching:

Cache inteligente no browser para reduzir requests desnecessários

Query Batching:

Agrupamento de múltiplas queries em uma única requisição HTTP

Lazy Loading:

Carregamento sob demanda de componentes e dados

Database Optimization:

Índices, connection pooling e query optimization

🗂️ Estratégias de Cache

💾 Client-Side Cache com React Query

hooks/use-optimized-trpc.ts
// 📁 hooks/use-optimized-trpc.ts
import { useQueryClient } from '@tanstack/react-query';
import { trpc } from '@/utils/trpc';

export function useOptimizedUserProfile(userId: string) {
  const queryClient = useQueryClient();

  return trpc.user.getProfile.useQuery(
    { id: userId },
    {
      // 🕐 Cache por 10 minutos
      staleTime: 10 * 60 * 1000,
      
      // 🔄 Revalidar em background após 5 minutos
      cacheTime: 30 * 60 * 1000,
      
      // 📊 Prefetch relacionados
      onSuccess: (userData) => {
        // 🔄 Prefetch posts do usuário
        queryClient.prefetchQuery({
          queryKey: [['post', 'getByUserId'], { userId }],
          queryFn: () => trpc.post.getByUserId.fetch({ userId }),
          staleTime: 5 * 60 * 1000,
        });

        // 💾 Cache dados relacionados
        queryClient.setQueryData(
          [['user', 'getBasicInfo'], { id: userId }],
          {
            id: userData.id,
            name: userData.name,
            avatar: userData.avatar,
          }
        );
      },
      
      // 🎯 Estratégia de retry inteligente
      retry: (failureCount, error) => {
        if (error?.data?.code === 'NOT_FOUND') return false;
        return failureCount < 3;
      },
      
      // ⚡ Otimização de refetch
      refetchOnWindowFocus: false,
      refetchOnReconnect: true,
    }
  );
}

// 🔄 Hook para invalidação inteligente
export function useSmartCacheInvalidation() {
  const queryClient = useQueryClient();
  
  const invalidateUserData = (userId: string) => {
    // 🎯 Invalidar apenas dados específicos do usuário
    queryClient.invalidateQueries({
      queryKey: [['user'], { id: userId }],
    });
    
    // 🔄 Invalidar posts relacionados
    queryClient.invalidateQueries({
      queryKey: [['post', 'getByUserId'], { userId }],
    });
  };
  
  const optimisticUpdate = (userId: string, updates: Partial<User>) => {
    // 🚀 Atualização otimista
    queryClient.setQueryData(
      [['user', 'getProfile'], { id: userId }],
      (oldData: User | undefined) => ({
        ...oldData,
        ...updates,
        updatedAt: new Date().toISOString(),
      })
    );
  };
  
  return { invalidateUserData, optimisticUpdate };
}

🔄 Server-Side Cache com Redis

server/cache/redis-cache.ts
// 📁 server/cache/redis-cache.ts
import Redis from 'ioredis';
import { z } from 'zod';

interface CacheOptions {
  ttl?: number; // Time to live em segundos
  tags?: string[]; // Tags para invalidação em grupo
  compress?: boolean; // Compressão para dados grandes
}

class RedisCacheManager {
  private redis: Redis;
  private defaultTTL = 300; // 5 minutos

  constructor(redisUrl: string) {
    this.redis = new Redis(redisUrl, {
      maxRetriesPerRequest: 3,
      retryDelayOnFailover: 100,
      lazyConnect: true,
    });
  }

  // 🔑 Gerar chave de cache estruturada
  private generateKey(procedure: string, input: any): string {
    const inputHash = this.hashInput(input);
    return `trpc:${procedure}:${inputHash}`;
  }

  // 🔐 Hash de input para chave única
  private hashInput(input: any): string {
    const crypto = require('crypto');
    const serialized = JSON.stringify(input, Object.keys(input).sort());
    return crypto.createHash('md5').update(serialized).digest('hex');
  }

  // 💾 Cache wrapper para procedures
  async cacheWrapper<T>(
    procedure: string,
    input: any,
    fetcher: () => Promise<T>,
    options: CacheOptions = {}
  ): Promise<T> {
    const key = this.generateKey(procedure, input);
    const ttl = options.ttl || this.defaultTTL;

    try {
      // 🔍 Tentar buscar do cache
      const cached = await this.redis.get(key);
      if (cached) {
        console.log(`🎯 Cache HIT: ${procedure}`);
        return JSON.parse(cached);
      }

      console.log(`❌ Cache MISS: ${procedure}`);
      
      // 📊 Executar função e cachear resultado
      const result = await fetcher();
      
      // 💾 Salvar no cache
      await this.setCache(key, result, ttl, options.tags);
      
      return result;
    } catch (error) {
      console.error(`Cache error for ${procedure}:`, error);
      // 🚨 Fallback para função original em caso de erro
      return fetcher();
    }
  }

  // 💾 Salvar no cache
  private async setCache(
    key: string, 
    data: any, 
    ttl: number, 
    tags?: string[]
  ): Promise<void> {
    const pipeline = this.redis.pipeline();
    
    // 💾 Salvar dados
    pipeline.setex(key, ttl, JSON.stringify(data));
    
    // 🏷️ Adicionar tags para invalidação em grupo
    if (tags) {
      for (const tag of tags) {
        pipeline.sadd(`tag:${tag}`, key);
        pipeline.expire(`tag:${tag}`, ttl);
      }
    }
    
    await pipeline.exec();
  }

  // 🗑️ Invalidar por tags
  async invalidateByTags(tags: string[]): Promise<void> {
    for (const tag of tags) {
      const keys = await this.redis.smembers(`tag:${tag}`);
      if (keys.length > 0) {
        await this.redis.del(...keys);
        await this.redis.del(`tag:${tag}`);
      }
    }
  }

  // 🔄 Invalidar cache específico
  async invalidate(procedure: string, input: any): Promise<void> {
    const key = this.generateKey(procedure, input);
    await this.redis.del(key);
  }
}

// 🚀 Instância singleton
export const cacheManager = new RedisCacheManager(
  process.env.REDIS_URL || 'redis://localhost:6379'
);

// 🎯 Decorator para cache automático
export function cached(options: CacheOptions = {}) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const procedure = `${target.constructor.name}.${propertyKey}`;
      const input = args[0]; // Primeiro argumento como input

      return cacheManager.cacheWrapper(
        procedure,
        input,
        () => originalMethod.apply(this, args),
        options
      );
    };

    return descriptor;
  };
}

⚡ Otimização de Performance

📦 Query Batching Inteligente

utils/trpc-client-optimized.ts
// 📁 utils/trpc-client-optimized.ts
import { createTRPCReact } from '@trpc/react-query';
import { httpBatchLink } from '@trpc/client';
import type { AppRouter } from '@/server/api/root';

// 🚀 Cliente otimizado com batching
export const trpc = createTRPCReact<AppRouter>();

export const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: '/api/trpc',
      
      // 📦 Configuração de batching otimizada
      maxBatchSize: 10, // Máximo 10 queries por batch
      
      // ⏱️ Delay para agrupar queries
      batchRequestTimeoutMs: 10,
      
      // 🎯 Headers otimizados
      headers: () => ({
        'Content-Type': 'application/json',
        'Accept-Encoding': 'gzip, br',
      }),
      
      // 🔄 Retry configuration
      fetch: (url, options) => {
        return fetch(url, {
          ...options,
          // 🚀 Keep-alive para reutilizar conexões
          keepalive: true,
        });
      },
    }),
  ],
  
  // 🎛️ Query client configuration
  queryClientConfig: {
    defaultOptions: {
      queries: {
        // 🕐 Cache padrão de 5 minutos
        staleTime: 5 * 60 * 1000,
        cacheTime: 10 * 60 * 1000,
        
        // 🔄 Retry inteligente
        retry: (failureCount, error: any) => {
          // Não retry para erros 4xx
          if (error?.data?.httpStatus >= 400 && error?.data?.httpStatus < 500) {
            return false;
          }
          return failureCount < 2;
        },
        
        // ⚡ Configurações de refetch
        refetchOnWindowFocus: false,
        refetchOnReconnect: true,
      },
      mutations: {
        // 🔄 Retry para mutations críticas
        retry: 1,
      },
    },
  },
});

// 🎯 Hook para prefetch inteligente
export function usePrefetchOptimizations() {
  const utils = trpc.useContext();
  
  const prefetchUserDashboard = async (userId: string) => {
    // 📊 Prefetch dados críticos em paralelo
    await Promise.all([
      utils.user.getProfile.prefetch({ id: userId }),
      utils.dashboard.getStats.prefetch({ userId }),
      utils.notification.getUnread.prefetch(),
      utils.post.getRecent.prefetch({ limit: 10 }),
    ]);
  };
  
  const prefetchOnHover = (component: string, data: any) => {
    // 🎯 Prefetch baseado em hover
    setTimeout(() => {
      switch (component) {
        case 'user-card':
          utils.user.getProfile.prefetch({ id: data.userId });
          break;
        case 'post-preview':
          utils.post.getById.prefetch({ id: data.postId });
          break;
      }
    }, 100); // Pequeno delay para evitar prefetch desnecessário
  };
  
  return { prefetchUserDashboard, prefetchOnHover };
}

// 📊 Sistema de métricas de performance
class PerformanceTracker {
  private metrics: Map<string, number[]> = new Map();
  
  startTimer(key: string): () => void {
    const start = performance.now();
    
    return () => {
      const duration = performance.now() - start;
      this.recordMetric(key, duration);
    };
  }
  
  private recordMetric(key: string, duration: number): void {
    if (!this.metrics.has(key)) {
      this.metrics.set(key, []);
    }
    
    const values = this.metrics.get(key)!;
    values.push(duration);
    
    // 📊 Manter apenas últimas 100 medições
    if (values.length > 100) {
      values.shift();
    }
    
    // 📈 Log de performance crítica
    if (duration > 1000) {
      console.warn(`⚠️ Slow operation: ${key} took ${duration.toFixed(2)}ms`);
    }
  }
  
  getStats(key: string) {
    const values = this.metrics.get(key) || [];
    if (values.length === 0) return null;
    
    const avg = values.reduce((a, b) => a + b, 0) / values.length;
    const min = Math.min(...values);
    const max = Math.max(...values);
    
    return { avg, min, max, count: values.length };
  }
  
  getReport(): Record<string, any> {
    const report: Record<string, any> = {};
    
    for (const [key, values] of this.metrics.entries()) {
      report[key] = this.getStats(key);
    }
    
    return report;
  }
}

export const performanceTracker = new PerformanceTracker();

// 🎯 Hook para tracking de performance
export function usePerformanceTracking(operationName: string) {
  const endTimer = React.useRef<(() => void) | null>(null);
  
  const startTracking = () => {
    endTimer.current = performanceTracker.startTimer(operationName);
  };
  
  const stopTracking = () => {
    if (endTimer.current) {
      endTimer.current();
      endTimer.current = null;
    }
  };
  
  // 🧹 Cleanup on unmount
  React.useEffect(() => {
    return () => {
      if (endTimer.current) {
        endTimer.current();
      }
    };
  }, []);
  
  return { startTracking, stopTracking };
}

🔧 Connection Pooling e Database Optimization

server/db/optimized-client.ts
// 📁 server/db/optimized-client.ts
import { PrismaClient } from '@prisma/client';

// 🏊 Connection pooling configuration
export const createOptimizedPrismaClient = () => {
  return new PrismaClient({
    datasources: {
      db: {
        url: process.env.DATABASE_URL,
      },
    },
    
    // 🔧 Logging otimizado
    log: process.env.NODE_ENV === 'development' 
      ? ['query', 'info', 'warn', 'error']
      : ['warn', 'error'],
      
    // ⚡ Query engine configuration
    __internal: {
      engine: {
        // 🏊 Connection pool settings
        connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT || '10'),
        poolTimeout: 20,
        
        // 🔄 Retry configuration
        maxRetries: 3,
        retryDelay: 100,
      },
    },
  });
};

// 📊 Query optimization middleware
export function createQueryOptimizer() {
  return {
    // 🎯 Lazy loading para relacionamentos
    async findManyWithIncludes<T>(
      model: any,
      args: any,
      includes: string[]
    ): Promise<T[]> {
      // 📊 Primeira query: dados principais
      const items = await model.findMany({
        ...args,
        select: this.buildSelectObject(args.select, includes),
      });
      
      if (items.length === 0) return items;
      
      // 🔄 Batch loading de relacionamentos
      const relationshipData = await this.batchLoadRelationships(
        model,
        items,
        includes
      );
      
      // 🔗 Combinar dados
      return this.combineResults(items, relationshipData);
    },
    
    // 🏗️ Build select object otimizado
    buildSelectObject(select: any, includes: string[]) {
      const optimizedSelect = { ...select };
      
      // 📊 Sempre incluir ID para joins
      optimizedSelect.id = true;
      
      // 🚫 Remover campos de relacionamento para primeira query
      includes.forEach(include => {
        delete optimizedSelect[include];
      });
      
      return optimizedSelect;
    },
    
    // 📦 Batch load relacionamentos
    async batchLoadRelationships(
      model: any,
      items: any[],
      includes: string[]
    ): Promise<Record<string, any[]>> {
      const results: Record<string, any[]> = {};
      const itemIds = items.map(item => item.id);
      
      // 🔄 Carregar todos os relacionamentos em paralelo
      await Promise.all(
        includes.map(async (include) => {
          const relatedData = await this.loadRelatedData(
            model,
            include,
            itemIds
          );
          results[include] = relatedData;
        })
      );
      
      return results;
    },
    
    // 📊 Carregar dados relacionados
    async loadRelatedData(
      model: any,
      relationName: string,
      parentIds: string[]
    ): Promise<any[]> {
      // 🎯 Query otimizada baseada no tipo de relacionamento
      switch (relationName) {
        case 'posts':
          return model.post.findMany({
            where: { authorId: { in: parentIds } },
            select: {
              id: true,
              title: true,
              createdAt: true,
              authorId: true,
            },
          });
          
        case 'comments':
          return model.comment.findMany({
            where: { postId: { in: parentIds } },
            select: {
              id: true,
              content: true,
              createdAt: true,
              postId: true,
              authorId: true,
            },
          });
          
        default:
          return [];
      }
    },
    
    // 🔗 Combinar resultados
    combineResults(items: any[], relationshipData: Record<string, any[]>) {
      return items.map(item => {
        const combined = { ...item };
        
        Object.entries(relationshipData).forEach(([relation, data]) => {
          combined[relation] = data.filter(related => 
            this.matchesRelationship(item, related, relation)
          );
        });
        
        return combined;
      });
    },
    
    // 🔍 Match relacionamento
    matchesRelationship(parent: any, related: any, relationName: string): boolean {
      switch (relationName) {
        case 'posts':
          return related.authorId === parent.id;
        case 'comments':
          return related.postId === parent.id;
        default:
          return false;
      }
    },
  };
}

// 🚀 Instância global otimizada
export const db = createOptimizedPrismaClient();
export const queryOptimizer = createQueryOptimizer();

// 🔧 Middleware de performance para procedures
export function performanceMiddleware() {
  return async (opts: any) => {
    const start = Date.now();
    const { path, type, next } = opts;
    
    try {
      const result = await next();
      const duration = Date.now() - start;
      
      // 📊 Log de performance
      if (duration > 500) {
        console.warn(`⚠️ Slow ${type}: ${path} took ${duration}ms`);
      }
      
      // 📈 Métricas para monitoramento
      if (process.env.ENABLE_METRICS === 'true') {
        await sendMetrics({
          operation: `${type}:${path}`,
          duration,
          timestamp: new Date().toISOString(),
        });
      }
      
      return result;
    } catch (error) {
      const duration = Date.now() - start;
      console.error(`❌ Error in ${type}: ${path} after ${duration}ms`, error);
      throw error;
    }
  };
}

async function sendMetrics(data: any) {
  // 📊 Enviar métricas para sistema de monitoramento
  try {
    if (process.env.METRICS_ENDPOINT) {
      await fetch(process.env.METRICS_ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });
    }
  } catch (error) {
    console.error('Failed to send metrics:', error);
  }
}

🔄 Lazy Loading Strategies

📱 Component-Based Lazy Loading

components/lazy-components.tsx
// 📁 components/lazy-components.tsx
import { lazy, Suspense, useState, useEffect } from 'react';
import { trpc } from '@/utils/trpc';

// 🔄 Lazy load pesados componentes
const HeavyDataTable = lazy(() => import('./HeavyDataTable'));
const ComplexChart = lazy(() => import('./ComplexChart'));
const UserProfileModal = lazy(() => import('./UserProfileModal'));

// 🎯 Intersection Observer para lazy loading
function useLazyLoad(threshold = 0.1) {
  const [isVisible, setIsVisible] = useState(false);
  const [ref, setRef] = useState<HTMLElement | null>(null);

  useEffect(() => {
    if (!ref) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { threshold }
    );

    observer.observe(ref);
    return () => observer.disconnect();
  }, [ref, threshold]);

  return { ref: setRef, isVisible };
}

// 🚀 Componente com lazy loading inteligente
export function LazyDataSection({ userId }: { userId: string }) {
  const { ref, isVisible } = useLazyLoad(0.2);
  
  return (
    <div ref={ref} className="min-h-[400px]">
      {isVisible && (
        <Suspense fallback={<DataSectionSkeleton />}>
          <LazyUserData userId={userId} />
        </Suspense>
      )}
    </div>
  );
}

function LazyUserData({ userId }: { userId: string }) {
  // 📊 Dados só carregam quando componente está visível
  const { data: user } = trpc.user.getProfile.useQuery(
    { id: userId },
    { enabled: true } // Enabled porque só monta quando visível
  );

  const { data: posts } = trpc.post.getByUserId.useQuery(
    { userId },
    { enabled: !!user } // Cascata de loading
  );

  return (
    <div>
      <HeavyDataTable data={posts} />
      <ComplexChart userData={user} />
    </div>
  );
}

🎯 Query Optimization

📊 Database Query Optimization

server/api/routers/optimized-user.ts
// 📁 server/api/routers/optimized-user.ts
import { z } from 'zod';
import { createTRPCRouter, publicProcedure } from '@/server/api/trpc';
import { queryOptimizer, db } from '@/server/db/optimized-client';

export const optimizedUserRouter = createTRPCRouter({
  // 🚀 Query otimizada com N+1 prevention
  getUsersWithPosts: publicProcedure
    .input(
      z.object({
        limit: z.number().min(1).max(100).default(10),
        cursor: z.string().optional(),
        includePostCount: z.boolean().default(false),
      })
    )
    .query(async ({ input }) => {
      const { limit, cursor, includePostCount } = input;

      // 📊 Query principal otimizada
      const users = await db.user.findMany({
        take: limit + 1,
        cursor: cursor ? { id: cursor } : undefined,
        select: {
          id: true,
          name: true,
          email: true,
          avatar: true,
          createdAt: true,
          // 🎯 Conditional include baseado na necessidade
          ...(includePostCount && {
            _count: { select: { posts: true } },
          }),
        },
        orderBy: { createdAt: 'desc' },
      });

      // 🔄 Pagination cursor
      let nextCursor: string | undefined;
      if (users.length > limit) {
        const nextItem = users.pop();
        nextCursor = nextItem?.id;
      }

      // 📦 Batch load posts se necessário
      if (includePostCount) {
        const userIds = users.map(u => u.id);
        const recentPosts = await db.post.findMany({
          where: { authorId: { in: userIds } },
          select: {
            id: true,
            title: true,
            authorId: true,
            createdAt: true,
          },
          orderBy: { createdAt: 'desc' },
          take: 3, // Últimos 3 posts por usuário
        });

        // 🔗 Group posts by user
        const postsByUser = recentPosts.reduce((acc, post) => {
          if (!acc[post.authorId]) acc[post.authorId] = [];
          acc[post.authorId].push(post);
          return acc;
        }, {} as Record<string, typeof recentPosts>);

        // 🎯 Attach posts to users
        users.forEach(user => {
          (user as any).recentPosts = postsByUser[user.id] || [];
        });
      }

      return { users, nextCursor };
    }),

  // ⚡ Search otimizado com full-text
  searchUsers: publicProcedure
    .input(
      z.object({
        query: z.string().min(2),
        limit: z.number().min(1).max(50).default(10),
      })
    )
    .query(async ({ input }) => {
      const { query, limit } = input;

      // 🔍 Full-text search com ranking
      const users = await db.$queryRaw`
        SELECT 
          id, name, email, avatar,
          ts_rank(search_vector, plainto_tsquery(${searchQuery})) as rank
        FROM users 
        WHERE search_vector @@ plainto_tsquery(${searchQuery})
        ORDER BY rank DESC, created_at DESC
        LIMIT ${limitValue}
      `;

      return users;
    }),
});

✅ O que você conquistou nesta aula

Client-Side Cache inteligente com React Query
Server-Side Cache com Redis
Query Batching otimizado
Performance Tracking automático
Lazy Loading com Intersection Observer
Database Optimization avançada
Connection Pooling eficiente
N+1 Prevention sistemático

🎯 Próximos Passos

Na próxima aula, vamos explorar Monitoramento e Observability, implementando sistemas completos de logging, métricas e alertas para aplicações tRPC em produção.

Aula Anterior
Módulo 6
Aula 3 de 5
Performance e Otimização
Próxima Aula