Saltar al contenido principal

🚀 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
  • revalidatePath en acciones críticas
  • ✅ Índices básicos en Prisma (userId, email, role, slug, status, hackathonId, etc.)
  • force-dynamic en 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 100
  • getUserGrowthData() - trae TODOS los usuarios
  • getParticipationTrends() - trae todos los registros de N días
  • calculateLeaderboard() - trae todas las submissions

Impacto: Escalabilidad limitada, tiempos de respuesta aumentan linealmente

3. Sin Caching - ALTO 🟡

Ubicación: Todo el proyecto

  • No usa unstable_cache de Next.js
  • Solo revalidatePath reactivo
  • 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.ts
  • src/app/api/organizer/analytics/route.ts
  • src/app/api/participant/analytics/route.ts
  • src/app/api/judge/analytics/route.ts
  • src/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.ts
  • src/modules/metrics/queries.ts
  • src/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:

  1. 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
}
  1. 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
}
  1. 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áginaTiempo 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áginaTiempo EsperadoMejora
GET /api/admin/analytics (cached)~50-100ms90% ⬇️
GET /api/leaderboards (optimized)~100-200ms75% ⬇️
GET /hackathons (indexed)~50-100ms70% ⬇️
Dashboard Admin~500-800ms TTI65% ⬇️
Leaderboard Page~300-500ms TTI70% ⬇️

Después de Fase 2

Endpoint/PáginaTiempo EsperadoMejora Total
GET /api/admin/analytics~50ms (cache)95% ⬇️
GET /api/leaderboards~80ms (raw SQL)90% ⬇️
GET /hackathons~30-50ms85% ⬇️
Dashboard Admin~400-600ms TTI70% ⬇️
Leaderboard Page~200-400ms TTI (+ realtime)80% ⬇️

🎬 Recomendación de Implementación

Semana 1: Quick Wins

  1. Día 1: Agregar cache a analytics APIs (1.1)
  2. Día 2: Agregar índices compuestos + migración (1.2)
  3. Día 3: Optimizar calculateLeaderboard (1.3)
  4. Día 4: Testing y ajustes
  5. Día 5: Deploy y monitoreo

Resultado: 70-80% de mejora en tiempos de respuesta

Semana 2: Mejoras Estructurales

  1. Días 1-2: Implementar paginación (2.1)
  2. Días 3-4: Expandir Supabase Realtime (2.2)
  3. 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=20 para 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

  1. Response Time p50/p95/p99
  2. Database Query Time
  3. Cache Hit Rate
  4. Concurrent Realtime Connections
  5. 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_cache a analytics APIs
  • Crear migration con índices compuestos
  • Optimizar calculateLeaderboard con raw SQL
  • Testing de rendimiento
  • Deploy a production

Fase 2: Mejoras Estructurales

  • Implementar paginación en listHackathons
  • Implementar paginación en analytics
  • Crear useSubmissionsRealtime hook
  • Crear useJudgeEvaluationsRealtime hook
  • Crear useAdminStatsRealtime hook
  • 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ónBeneficio
5 días desarrollo70-85% reducción en tiempos de respuesta
+ $25/mes Supabase ProUX moderna con realtime
+ $0 (Next.js cache built-in)90%+ cache hit rate
Total: 5 días + $25/mesEscalabilidad para 10,000+ usuarios

Próximos pasos: ¿Quieres que implemente la Fase 1 (Quick Wins)?