Aula 3 - Módulo 6: Técnicas avançadas para aplicações tRPC de alta performance
Cada milissegundo importa. Aplicações tRPC lentas resultam em menor engajamento, conversões reduzidas e usuários frustrados. Performance não é opcional.
Amazon descobriu que 100ms de latência custam 1% das vendas. Para aplicações enterprise, performance otimizada significa ROI direto.
Aplicações otimizadas suportam mais usuários com menos recursos. Cache inteligente e queries eficientes reduzem drasticamente a carga no servidor.
Time to First Byte (TTFB), Largest Contentful Paint (LCP) e Cumulative Layout Shift (CLS) definem a percepção de qualidade.
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
// 📁 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/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;
};
}
// 📁 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 };
}
// 📁 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);
}
}
// 📁 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>
);
}
// 📁 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;
}),
});
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.