🚀 Análisis de Optimización del MVP - PuntoHack
Fecha: 7 de enero de 2026
Objetivo: Identificar y priorizar oportunidades de mejora en rendimiento
📊 Análisis del Estado Actual
✅ Implementado
- ✅ Supabase Realtime en 4 casos de uso:
- Lista de hackathons
- Estado de hackathon individual
- Dashboard de organizador
- Leaderboard
- ✅
revalidatePathen acciones críticas - ✅ Índices básicos en Prisma (userId, email, role, slug, status, hackathonId, etc.)
- ✅
force-dynamicen rutas API - ✅ Server Components por defecto
⚠️ Problemas Identificados
1. N+1 Query Problems - CRÍTICO 🔴
Ubicación: src/modules/metrics/queries.ts (línea 188-200)
// ❌ PROBLEMA: Query pesada con múltiples includes anidados
const allHackathons = await db.hackathon.findMany({
include: {
_count: {
select: {
participations: true,
teams: true,
submissions: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: 10,
});
Impacto: Cada hackathon hace 3 sub-queries adicionales Solución: Usar agregación nativa de Prisma
2. Falta de Paginación - ALTO 🟡
Ubicación: Múltiples queries
listHackathons()- límite hardcoded a 100getUserGrowthData()- trae TODOS los usuariosgetParticipationTrends()- trae todos los registros de N díascalculateLeaderboard()- trae todas las submissions
Impacto: Escalabilidad limitada, tiempos de respuesta aumentan linealmente
3. Sin Caching - ALTO 🟡
Ubicación: Todo el proyecto
- No usa
unstable_cachede Next.js - Solo
revalidatePathreactivo - APIs de analytics sin cache (se recalculan en cada request)
Impacto:
- Dashboard de admin: ~500ms-1s por request
- Leaderboard: ~300-800ms por cálculo
4. Queries Secuenciales - MEDIO 🟠
Ubicación: getAdminAnalytics() línea 172
// ✅ BIEN: Ya usa Promise.all
const [userGrowth, hackathonsByStatus, participationTrends, allHackathons] = await Promise.all([...]);
Pero otras funciones hacen queries secuenciales innecesariamente
5. Falta de Índices Compuestos - MEDIO 🟠
Ubicación: prisma/schema.prisma
// ❌ FALTA: Índices compuestos para queries comunes
model Score {
// No tiene @@index([submissionId, judgeId])
// No tiene @@index([submissionId, criterionId])
}
model Submission {
// No tiene @@index([hackathonId, createdAt])
}
6. Supabase Realtime Limitado - BAJO 🟢
Actualmente solo 4 casos de uso. Faltan:
- Notificaciones (ya tiene hook pero no se usa en todas partes)
- Submissions en tiempo real
- Evaluaciones de jueces en tiempo real
- Dashboard de stats en tiempo real
🎯 Plan de Optimización Priorizado
Fase 1: Quick Wins (1-2 días) ⚡
1.1 Agregar Cache a Analytics APIs
Archivos:
src/app/api/admin/analytics/route.tssrc/app/api/organizer/analytics/route.tssrc/app/api/participant/analytics/route.tssrc/app/api/judge/analytics/route.tssrc/app/api/sponsor/analytics/route.ts
Implementación:
import { unstable_cache } from 'next/cache';
// Cache por 5 minutos
const getCachedAdminAnalytics = unstable_cache(
async (days: number) => getAdminAnalytics(days),
['admin-analytics'],
{ revalidate: 300, tags: ['admin-analytics'] }
);
Beneficio: Reducir de ~500-1000ms a ~50ms en requests repetidos Complejidad: Baja Prioridad: ⭐⭐⭐⭐⭐
1.2 Agregar Índices Compuestos
Archivo: prisma/schema.prisma
Cambios:
model Score {
// ...existing fields
@@index([submissionId, judgeId])
@@index([submissionId, criterionId])
@@index([judgeId, createdAt])
}
model Submission {
@@index([hackathonId, createdAt])
@@index([teamId, hackathonId])
}
model TeamMember {
@@index([profileId, teamId])
}
model HackathonJudge {
@@index([hackathonId, profileId])
}
model Notification {
@@index([profileId, createdAt])
@@index([profileId, isRead])
}
Beneficio: Queries 2-10x más rápidas en joins complejos Complejidad: Baja (solo migración) Prioridad: ⭐⭐⭐⭐⭐
1.3 Optimizar calculateLeaderboard con Raw Query
Archivo: src/modules/evaluation/queries.ts
Antes:
// Trae todas las submissions con includes pesados
const submissions = await db.submission.findMany({
where: { hackathonId },
include: {
team: { include: { members: { include: { profile: true } } } },
scores: { include: { judge, criterion } },
},
});
Después:
// Usar query SQL optimizada con agregación
const leaderboard = await db.$queryRaw`
SELECT
s.id as "submissionId",
s."teamId",
t.name as "teamName",
COUNT(DISTINCT sc."judgeId") as "judgeCount",
SUM(sc.value * c.weight) / NULLIF(SUM(c."maxScore" * c.weight), 0) * 100 as score
FROM "Submission" s
JOIN "Team" t ON s."teamId" = t.id
LEFT JOIN "Score" sc ON s.id = sc."submissionId"
LEFT JOIN "Criterion" c ON sc."criterionId" = c.id
WHERE s."hackathonId" = ${hackathonId}
GROUP BY s.id, s."teamId", t.name
ORDER BY score DESC
`;
Beneficio: Reducir de ~800ms a ~100ms en leaderboards grandes Complejidad: Media Prioridad: ⭐⭐⭐⭐
Fase 2: Mejoras Estructurales (3-4 días) 🔨
2.1 Implementar Paginación en Queries Principales
Archivos:
src/modules/hackathons/queries.tssrc/modules/metrics/queries.tssrc/modules/evaluation/queries.ts
Implementación:
interface PaginationOptions {
page?: number;
pageSize?: number;
}
export async function listHackathons(
filters?: ListHackathonsFilters & PaginationOptions
) {
const page = filters?.page || 1;
const pageSize = filters?.pageSize || 20;
const skip = (page - 1) * pageSize;
const [items, total] = await Promise.all([
db.hackathon.findMany({
where: { ...filters },
skip,
take: pageSize,
orderBy: { createdAt: 'desc' },
}),
db.hackathon.count({ where: { ...filters } }),
]);
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
Beneficio: Escalabilidad, UX mejorada Complejidad: Media Prioridad: ⭐⭐⭐⭐
2.2 Expandir Supabase Realtime
Nuevos hooks a crear:
- useSubmissionsRealtime - Para dashboard de hackathon
// src/lib/realtime/hooks/use-submissions-realtime.ts
export function useSubmissionsRealtime(hackathonId: string) {
// Escuchar INSERT en tabla Submission
// Actualizar lista automáticamente
}
- useJudgeEvaluationsRealtime - Para progreso de jueces
// src/lib/realtime/hooks/use-judge-evaluations-realtime.ts
export function useJudgeEvaluationsRealtime(hackathonId: string) {
// Escuchar INSERT/UPDATE en Score
// Actualizar progreso de evaluación
}
- useAdminStatsRealtime - Para dashboard admin
// src/lib/realtime/hooks/use-admin-stats-realtime.ts
export function useAdminStatsRealtime() {
// Escuchar cambios en Profile, Hackathon, Submission
// Actualizar contadores en tiempo real
}
Páginas a actualizar:
/admin/hackathons/[slug]/dashboard- submissions en tiempo real/judge/hackathons/[slug]- submissions para evaluar/admin- stats en tiempo real/organizer/hackathons/[slug]- stats del hackathon
Beneficio: UX moderna, sin necesidad de refresh Complejidad: Media-Alta Prioridad: ⭐⭐⭐
2.3 Optimizar getUserGrowthData
Archivo: src/modules/metrics/queries.ts
Problema: Trae TODOS los usuarios solo para contar Solución: Usar agregación y SQL raw
export async function getUserGrowthData(days: number = 30): Promise<UserGrowthData> {
// Total de usuarios (simple count)
const totalUsers = await db.profile.count();
// Últimos 7 días vs anteriores 7 días (con SQL eficiente)
const growthStats = await db.$queryRaw<Array<{ period: string; count: bigint }>>`
SELECT
CASE
WHEN "createdAt" >= NOW() - INTERVAL '7 days' THEN 'last7'
WHEN "createdAt" >= NOW() - INTERVAL '14 days' THEN 'previous7'
ELSE 'older'
END as period,
COUNT(*) as count
FROM "Profile"
WHERE "createdAt" >= NOW() - INTERVAL '14 days'
GROUP BY period
`;
const last7Days = Number(growthStats.find(s => s.period === 'last7')?.count || 0);
const previous7Days = Number(growthStats.find(s => s.period === 'previous7')?.count || 0);
// Serie temporal solo de últimos N días (con agrupación en DB)
const timeSeries = await db.$queryRaw<TimeSeriesDataPoint[]>`
SELECT
DATE_TRUNC('day', "createdAt") as date,
COUNT(*) OVER (ORDER BY DATE_TRUNC('day', "createdAt")) as value
FROM "Profile"
WHERE "createdAt" >= NOW() - INTERVAL '${days} days'
GROUP BY DATE_TRUNC('day', "createdAt")
ORDER BY date
`;
return {
totalUsers,
growthRate: calculatePercentageChange(last7Days, previous7Days),
timeSeries,
};
}
Beneficio: 10x más rápido en bases de datos grandes Complejidad: Media Prioridad: ⭐⭐⭐⭐
Fase 3: Optimizaciones Avanzadas (4-5 días) 🚀
3.1 Implementar Cache Incremental
Estrategia: Tag-based revalidation
// src/lib/cache/tags.ts
export const CacheTags = {
hackathon: (slug: string) => `hackathon:${slug}`,
leaderboard: (hackathonId: string) => `leaderboard:${hackathonId}`,
adminAnalytics: () => 'admin-analytics',
userProfile: (userId: string) => `profile:${userId}`,
} as const;
// En acciones, invalidar tags específicos
import { revalidateTag } from 'next/cache';
// Cuando se crea una submission
revalidateTag(CacheTags.leaderboard(hackathonId));
revalidateTag(CacheTags.hackathon(hackathon.slug));
Beneficio: Cache granular, no invalida todo Complejidad: Media-Alta Prioridad: ⭐⭐⭐
3.2 Streaming de Datos Grandes
Para: Exportaciones CSV/Excel/PDF
// src/app/api/admin/export/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const stream = new ReadableStream({
async start(controller) {
// Enviar headers CSV
controller.enqueue('Name,Email,Role\n');
// Stream de datos en batches
let skip = 0;
const batchSize = 100;
while (true) {
const batch = await db.profile.findMany({
skip,
take: batchSize,
});
if (batch.length === 0) break;
for (const user of batch) {
controller.enqueue(`${user.name},${user.email},${user.role}\n`);
}
skip += batchSize;
}
controller.close();
},
});
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': 'attachment; filename="export.csv"',
},
});
}
Beneficio: No cargar todos los datos en memoria Complejidad: Alta Prioridad: ⭐⭐
3.3 Implementar Connection Pooling Avanzado
Archivo: src/core/db.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
// Prisma Client Extensions para logging automático
export const extendedDb = db.$extends({
query: {
$allModels: {
async $allOperations({ operation, model, args, query }) {
const start = performance.now();
const result = await query(args);
const end = performance.now();
if (end - start > 1000) {
console.warn(`[Slow Query] ${model}.${operation} took ${end - start}ms`);
}
return result;
},
},
},
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
Beneficio: Detectar queries lentas automáticamente Complejidad: Baja Prioridad: ⭐⭐⭐
📈 Métricas Esperadas
Antes de Optimizaciones
| Endpoint/Página | Tiempo Actual |
|---|---|
| GET /api/admin/analytics | ~800-1200ms |
| GET /api/leaderboards | ~500-800ms |
| GET /hackathons | ~200-400ms |
| Dashboard Admin | ~1.5-2s TTI |
| Leaderboard Page | ~1-1.5s TTI |
Después de Fase 1
| Endpoint/Página | Tiempo Esperado | Mejora |
|---|---|---|
| GET /api/admin/analytics (cached) | ~50-100ms | 90% ⬇️ |
| GET /api/leaderboards (optimized) | ~100-200ms | 75% ⬇️ |
| GET /hackathons (indexed) | ~50-100ms | 70% ⬇️ |
| Dashboard Admin | ~500-800ms TTI | 65% ⬇️ |
| Leaderboard Page | ~300-500ms TTI | 70% ⬇️ |
Después de Fase 2
| Endpoint/Página | Tiempo Esperado | Mejora Total |
|---|---|---|
| GET /api/admin/analytics | ~50ms (cache) | 95% ⬇️ |
| GET /api/leaderboards | ~80ms (raw SQL) | 90% ⬇️ |
| GET /hackathons | ~30-50ms | 85% ⬇️ |
| Dashboard Admin | ~400-600ms TTI | 70% ⬇️ |
| Leaderboard Page | ~200-400ms TTI (+ realtime) | 80% ⬇️ |
🎬 Recomendación de Implementación
Semana 1: Quick Wins
- Día 1: Agregar cache a analytics APIs (1.1)
- Día 2: Agregar índices compuestos + migración (1.2)
- Día 3: Optimizar calculateLeaderboard (1.3)
- Día 4: Testing y ajustes
- Día 5: Deploy y monitoreo
Resultado: 70-80% de mejora en tiempos de respuesta
Semana 2: Mejoras Estructurales
- Días 1-2: Implementar paginación (2.1)
- Días 3-4: Expandir Supabase Realtime (2.2)
- Día 5: Optimizar getUserGrowthData (2.3)
Resultado: Escalabilidad + UX moderna
Semana 3: Avanzadas (Opcional)
- Tag-based caching
- Streaming de exports
- Connection pooling avanzado
🚨 Consideraciones Importantes
Supabase Realtime
- Límite: 200 conexiones simultáneas en plan gratuito
- Costo: Plan Pro ($25/mes) = 500 conexiones
- Monitoreo: Implementar contador de conexiones activas
Prisma Client
- Connection Pool: Default 10 conexiones
- Recomendación: Ajustar a
connection_limit=20para production
Next.js Cache
- Revalidación: No usar tiempos muy largos (max 5-10 min)
- Tags: Usar naming consistente
- ISR: Considerar para páginas estáticas
📊 Tracking de Progreso
Métricas a Monitorear
- Response Time p50/p95/p99
- Database Query Time
- Cache Hit Rate
- Concurrent Realtime Connections
- Error Rate
Herramientas Recomendadas
- Vercel Analytics (ya incluido)
- Prisma Pulse (monitoreo de DB)
- Sentry (error tracking)
- LogTail/Axiom (logging)
✅ Checklist de Implementación
Fase 1: Quick Wins
- Agregar
unstable_cachea analytics APIs - Crear migration con índices compuestos
- Optimizar
calculateLeaderboardcon raw SQL - Testing de rendimiento
- Deploy a production
Fase 2: Mejoras Estructurales
- Implementar paginación en
listHackathons - Implementar paginación en analytics
- Crear
useSubmissionsRealtimehook - Crear
useJudgeEvaluationsRealtimehook - Crear
useAdminStatsRealtimehook - Actualizar páginas con nuevos hooks
- Optimizar
getUserGrowthData
Fase 3: Avanzadas
- Implementar tag-based caching
- Streaming de exports grandes
- Connection pooling con logging
- Monitoring y alertas
🎯 ROI Esperado
| Inversión | Beneficio |
|---|---|
| 5 días desarrollo | 70-85% reducción en tiempos de respuesta |
| + $25/mes Supabase Pro | UX moderna con realtime |
| + $0 (Next.js cache built-in) | 90%+ cache hit rate |
| Total: 5 días + $25/mes | Escalabilidad para 10,000+ usuarios |
Próximos pasos: ¿Quieres que implemente la Fase 1 (Quick Wins)?