Next.js: Análise de Performance & Monitoramento

Última atualização: 2026-08-26

Performance é um atributo-chave — em uma era da internet onde um atraso de 100 milissegundos pode resultar em uma perda de 7% dos usuários, a otimização de performance é uma métrica central da competitividade de um produto.

1. O Que Você Vai Aprender



2. Uma História Real de uma Engenheira de Performance

(1) Ponto Problemático: Pontuação Lighthouse de 45; os usuários estão indo embora

Diana é engenheira de performance na plataforma TaskFlow. Ela recebeu recentemente um relatório de análise de comportamento do usuário:

Métrica Valor Atual Referência do Setor Impacto
LCP 4.8s < 2.5s Taxa de rejeição de usuários 45%
FID/INP 320 ms < 200 ms A interação parece travada
CLS 0.35 < 0.1 Cliques errados causados por mudanças de layout
Tamanho do Bundle 1.2 MB < 500 KB Carregamento lento da primeira tela

Para piorar, ela não conseguia responder às seguintes perguntas:

(2) Soluções para Sistemas de Monitoramento de Performance

Diana estabeleceu um sistema abrangente de monitoramento de performance:

TSX
// Monitoramento de Usuário Real (RUM)
import { useReportWebVitals } from 'next/web-vitals'

export function WebVitals() {
  useReportWebVitals(metric => {
    fetch('/api/analytics', {
      method: 'POST',
      body: JSON.stringify(metric)
    })
  })
}

(3) Resultados

Dimensão Antes da Otimização Depois da Otimização
LCP 4.8s 1.2s
Tamanho do Bundle 1.2 MB 380 KB
Pontuação Lighthouse 45/100 92/100
Detecção de Degradação de Performance Descoberta após implantação Interceptada durante a fase de PR


3. Lighthouse CI

O Lighthouse CI executa automaticamente auditorias Lighthouse no pipeline CI/CD e define limites de orçamento de performance.

100%
graph TB
    A[Pipeline CI] --> B[Lighthouse CI]
    B --> C[Executar Auditoria Lighthouse]
    C --> D{Comparando Performance e Orçamento}
    D -->|Aprovado| E[Pipeline Continua]
    D -->|Falha| F[Pipeline Interrompido]
    F --> G[PR Reporta Falha no Comentário]
    G --> H[Desenvolvedores Otimizam Localmente]
    
    style A fill:#cce5ff
    style C fill:#fff3cd
    style D fill:#f8d7da
    style E fill:#d4edda

(1) Configuração lighthouserc.js

JS
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      // Número de vezes coletado (Encontrar a mediana)
      numberOfRuns: 3,
      
      // Páginas a Serem Auditadas
      url: [
        'http://localhost:3000',
        'http://localhost:3000/login',
        'http://localhost:3000/dashboard',
        'http://localhost:3000/projects',
        'http://localhost:3000/projects/p1'
      ],
      
      // Iniciar Servidor de Desenvolvimento Next.js
      startServerCommand: 'npm run start -p 3000',
      startServerReadyPattern: 'ready started server',
      
      // Simulação de Equipamento
      settings: {
        preset: 'desktop',
        throttling: {
          cpuSlowdownMultiplier: 4,
          downloadThroughputKbps: 10000,
          uploadThroughputKbps: 5000,
          rttMs: 40
        }
      }
    },
    
    assert: {
      // Controle de Acesso Baseado em Performance
      budgets: [
        {
          path: '/',
          resourceSizes: [
            { resourceType: 'total', budget: 500 * 1024 },     // 500KB
            { resourceType: 'script', budget: 200 * 1024 },    // 200KB
            { resourceType: 'image', budget: 150 * 1024 }      // 150KB
          ],
          resourceCounts: [
            { resourceType: 'script', budget: 15 },
            { resourceType: 'stylesheet', budget: 5 },
            { resourceType: 'image', budget: 20 }
          ]
        }
      ],
      
      // Limiar de Pontuação Lighthouse
      assertions: {
        // Pontuação por Categoria
        'categories:performance': ['warn', { minScore: 0.9 }],
        'categories:accessibility': ['warn', { minScore: 0.9 }],
        'categories:best-practices': ['warn', { minScore: 0.9 }],
        'categories:seo': ['warn', { minScore: 0.9 }],
        
        // Core Web Vitals
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['error', { maxNumericValue: 200 }],
        
        // Outras Métricas-Chave
        'first-contentful-paint': ['warn', { maxNumericValue: 1800 }],
        'interactive': ['warn', { maxNumericValue: 3500 }],
        'max-potential-fid': ['warn', { maxNumericValue: 100 }],
        
        // Melhores Práticas
        'uses-http2': ['error'],
        'uses-responsive-images': ['error'],
        'offscreen-images': ['error'],
        'unused-javascript': ['warn', { maxNumericValue: 50 * 1024 }],
        'unused-css-rules': ['warn', { maxNumericValue: 10 * 1024 }],
        'uses-optimized-images': ['error'],
        'uses-text-compression': ['error'],
        'uses-rel-preconnect': ['warn'],
        'uses-rel-preload': ['warn'],
        'efficient-animated-content': ['warn'],
        'total-byte-weight': ['error', { maxNumericValue: 500 * 1024 }]
      }
    },
    
    upload: {
      target: 'temporary-public-storage'
    },
    
    server: {
      // Permitir links externos (Ambiente CI)
      allowStaticServer: true
    }
  }
}

(2) Integração CI

YAML
# .github/workflows/lighthouse.yml
name: Lighthouse CI

on:
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Instalar dependências
        run: npm ci
      
      - name: Construir aplicação
        run: npm run build
      
      - name: Executar Lighthouse CI
        run: |
          npm install -g @lhci/cli
          lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_TOKEN }}

▶ Exemplo: Interpretando Relatórios do Lighthouse CI

TEXT 📖 Somente leitura
Resultados do Lighthouse CI (3 execuções, mediana)

URL: http://localhost:3000/dashboard

┌──────────────┬─────────┬─────────┬──────┐
│ Categoria    │ Pontuação │ Alvo   │ Passou │
├──────────────┼─────────┼─────────┼──────┤
│ Performance  │ 92      │ ≥ 90    │ ✅   │
│ Acessibilidade│ 95      │ ≥ 90    │ ✅   │
│ Melhores Práticas│ 100     │ ≥ 90    │ ✅   │
│ SEO          │ 100     │ ≥ 90    │ ✅   │
└──────────────┴─────────┴─────────┴──────┘

Core Web Vitals
  LCP: 1.423 ms  (≤ 2.500 ms)  ✅
  TBT: 87 ms     (≤ 200 ms)   ✅
  CLS: 0.05      (≤ 0.1)      ✅

Orçamentos
  KB Total: 382 de 500 KB  ✅
  Scripts:  15 de 15       ✅
  Imagens:   4 de 20        ✅


4. Bundle Analyzer

@next/bundle-analyzer visualiza o tamanho dos módulos JavaScript minificados para ajudar a identificar dependências muito grandes.

100%
graph LR
    A[Processo de Build] --> B[Plugin Bundle Analyzer]
    B --> C[Gerar HTML treemap]
    C --> D[Análise Aberta no Navegador]
    D --> E{Identificando Módulos Grandes}
    E --> F[tree-shaking Não está funcionando]
    E --> G[Dependências Duplicadas]
    E --> H[Uma biblioteca muito grande foi carregada]
    
    style A fill:#cce5ff
    style C fill:#d4edda

(1) Configurando Bundle Analyzer

JS
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
  openAnalyzer: true,
  analyzerMode: 'static',
  reportFilename: 'bundle-report.html',
  defaultSizes: 'gzip'
})

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  productionBrowserSourceMaps: false,
  swcMinify: true
}

module.exports = withBundleAnalyzer(nextConfig)

(2) Análise Operacional

JSON
{
  "scripts": {
    "analyze": "ANALYZE=true npm run build",
    "analyze:server": "ANALYZE=true npm run build && npx serve .next/analyze"
  }
}

▶ Exemplo: Interpretando o Relatório de Análise de Bundle

BASH
ANALYZE=true npm run build
TEXT 📖 Somente leitura
Relatório de Bundle — client/gzip

Módulo                          Tamanho      % do Total
├── node_modules               280 KB    73.3%
│   ├── @radix-ui              85 KB     22.3%
│   ├── react-dom              52 KB     13.6%
│   ├── react                  18 KB      4.7%
│   ├── date-fns               45 KB     11.8%
│   ├── recharts               32 KB      8.4%
│   └── lodash                 28 KB      7.3%
├── components/                72 KB     18.8%
│   ├── Dashboard.tsx          18 KB      4.7%
│   ├── ProjectList.tsx        12 KB      3.1%
│   ├── TaskCard.tsx           8 KB       2.1%
│   └── ...
├── pages/                     22 KB      5.8%
└── lib/                       8 KB       2.1%

Total: 382 KB (gzip)

(3) Estratégias de Otimização de Bundle

Estratégia Método Economia Esperada
Import Dinâmico const Chart = dynamic(() => import('./Chart')) -280 KB Carregamento Inicial
Tree Shaking Use imports ESM em vez de CJS -20% código morto
Code Splitting Divisão automática por rota (padrão Next.js) -30% JavaScript na primeira tela
Substituição de Dependência date-fnsdayjs (45KB → 6KB) -39 KB
Lazy Loading React.lazy + Suspense -85 KB Biblioteca de Interação


5. Monitoramento de Usuário Real Core Web Vitals

O Lighthouse fornece dados de laboratório; apenas o Monitoramento de Usuário Real (RUM) pode refletir a experiência real do usuário.

100%
graph TB
    A[Navegador do usuário] --> B[Coleta de Dados Web Vitals]
    B --> C{Tipo de Indicador}
    C --> D[LCP Renderização de Conteúdo Máximo]
    C --> E[INP Interação para a Próxima Pintura]
    C --> F[CLS Mudança Cumulativa de Layout]
    C --> G[FCP Renderização de Conteúdo Inicial]
    C --> H[TTFB Tempo até o Primeiro Byte]
    D --> I[Reportar para Serviços de Analytics]
    E --> I
    F --> I
    G --> I
    H --> I
    I --> J[PostHog / GA4 / Autoconstruído]
    J --> K[Dashboard Visualização]
    
    style B fill:#cce5ff
    style I fill:#fff3cd
    style K fill:#d4edda

(1) Componente de Relatório Web Vitals

TSX
// src/components/WebVitals.tsx
'use client'

import { useReportWebVitals } from 'next/web-vitals'

type MetricType = {
  id: string
  name: string
  value: number
  rating: 'good' | 'needs-improvement' | 'poor'
  delta: number
}

export function WebVitals() {
  useReportWebVitals((metric: MetricType) => {
    // Reportar para Serviços de Analytics
    const body = {
      metric_name: metric.name,
      metric_value: metric.value,
      metric_rating: metric.rating,
      metric_delta: metric.delta,
      url: window.location.pathname,
      user_agent: navigator.userAgent,
      device_type: getDeviceType(),
      connection: (navigator as any).connection?.effectiveType || 'unknown',
      timestamp: new Date().toISOString()
    }

    // Usar sendBeacon para garantir que os dados sejam reportados mesmo quando a página for descarregada
    if (navigator.sendBeacon) {
      navigator.sendBeacon('/api/vitals', JSON.stringify(body))
    } else {
      fetch('/api/vitals', {
        method: 'POST',
        body: JSON.stringify(body),
        keepalive: true
      })
    }
  })

  return null
}

function getDeviceType(): string {
  const width = window.innerWidth
  if (width < 768) return 'mobile'
  if (width < 1024) return 'tablet'
  return 'desktop'
}

(2) Rota API de Vitals (Armazenamento de Dados)

TS
// src/app/api/vitals/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'

export async function POST(request: NextRequest) {
  try {
    const data = await request.json()
    
    // Salvar no banco de dados
    await prisma.webVital.create({
      data: {
        metricName: data.metric_name,
        metricValue: data.metric_value,
        metricRating: data.metric_rating,
        url: data.url,
        deviceType: data.device_type,
        connection: data.connection,
        userAgent: data.user_agent,
        timestamp: new Date(data.timestamp)
      }
    })
    
    // Se o indicador for ruim, disparar um alerta
    if (data.metric_rating === 'poor') {
      await checkAlertThresholds(data)
    }
    
    return NextResponse.json({ ok: true }, { status: 200 })
  } catch (error) {
    console.error('Falha ao armazenar web vital:', error)
    return NextResponse.json({ ok: false }, { status: 500 })
  }
}

async function checkAlertThresholds(metric: any) {
  // Olhando para trás 5 minutos, o número de Indicadores ruins do caminho
  const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000)
  
  const poorCount = await prisma.webVital.count({
    where: {
      metricName: metric.metric_name,
      metricRating: 'poor',
      url: metric.url,
      timestamp: { gte: fiveMinAgo }
    }
  })
  
  // Se exceder o limite, Enviar um Alerta
  if (poorCount > 10) {
    // await sendAlert(`Degradação de performance detectada em ${metric.url}: ${metric.metric_name} = ${metric.metric_value}`)
    console.warn(`ALERTA: ${poorCount} ${metric.metric_name} ruins em ${metric.url}`)
  }
}

(3) Consulta do Dashboard de Performance

TS
// src/app/dashboard/performance/page.tsx
import { prisma } from '@/lib/prisma'

async function getPerformanceSummary() {
  const today = new Date()
  today.setHours(0, 0, 0, 0)
  
  const vitals = await prisma.webVital.groupBy({
    by: ['metricName'],
    where: {
      timestamp: { gte: today }
    },
    _avg: {
      metricValue: true
    },
    _count: true
  })
  
  // Cálculo da proporção good / needs-improvement / poor
  const ratings = await prisma.webVital.groupBy({
    by: ['metricRating'],
    where: {
      timestamp: { gte: today }
    },
    _count: true
  })
  
  return { vitals, ratings }
}

▶ Exemplo: Interpretando Dados Web Vitals

TEXT 📖 Somente leitura
Core Web Vitals de Hoje (2026-07-06)

Métrica       │ P75     │ P95     │ Bom %  │ Ruim %
─────────────┼─────────┼─────────┼─────────┼───────
LCP          │ 1.234ms │ 3.567ms │ 85.2%   │ 5.1%
INP          │ 98ms    │ 245ms   │ 91.3%   │ 2.8%
CLS          │ 0.05    │ 0.18    │ 88.7%   │ 4.2%
FCP          │ 823ms   │ 1.945ms │ 90.1%   │ 3.5%
TTFB         │ 345ms   │ 890ms   │ 92.4%   │ 2.1%

Top 5 Rotas Lentas
/dashboard          LCP: 3.2s  (200 visitas)
/projects/p1/tasks  LCP: 2.8s  (150 visitas)
/reports            LCP: 2.6s  (80 visitas)
/settings           LCP: 2.1s  (60 visitas)
/analytics          LCP: 1.9s  (120 visitas)


6. Integração PostHog Speed Insights

PostHog é uma plataforma de análise de produtos de código aberto que inclui gravação de sessão integrada, feature flags e Speed Insights.

(1) Integração no Cliente

TSX
// src/components/PostHogProvider.tsx
'use client'

import { posthog } from 'posthog-js'
import { PostHogProvider as PHProvider, usePostHog } from 'posthog-js/react'
import { useEffect } from 'react'
import { useReportWebVitals } from 'next/web-vitals'

if (typeof window !== 'undefined') {
  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
    api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
    capture_pageview: false,
    capture_performance: true,  // Captura Automática Web Vitals
    loaded: (ph) => {
      if (process.env.NODE_ENV === 'development') ph.opt_out_capturing()
    }
  })
}

export function PostHogWebVitals() {
  const posthog = usePostHog()

  useReportWebVitals((metric) => {
    posthog.capture('$web_vitals', {
      $metric_name: metric.name,
      $metric_value: metric.value,
      $metric_rating: metric.rating,
      $pathname: window.location.pathname,
      $device: navigator.userAgent
    })
  })

  return null
}

export function PHProvider({ children }: { children: React.ReactNode }) {
  return <PHProvider client={posthog}>{children}</PHProvider>
}

(2) Integração no Layout Raiz

TSX
// src/app/layout.tsx
import { PHProvider, PostHogWebVitals } from '@/components/PostHogProvider'
import { WebVitals } from '@/components/WebVitals'

export default function RootLayout({
  children
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pt-BR">
      <body>
        <PHProvider>
          {children}
          <WebVitals />
          <PostHogWebVitals />
        </PHProvider>
      </body>
    </html>
  )
}


7. /instrumentation.ts Rastreamento Personalizado

O Next.js 16 suporta o registro de rastreamentos OpenTelemetry via instrumentation.ts para monitorar performance do lado do servidor e rotas lentas.

TS
// src/instrumentation.ts
// ============================================
// Telemetria Personalizada Next.js 16
// ============================================
import { registerOTel } from '@vercel/otel'

export async function register() {
  registerOTel({
    serviceName: 'taskflow',
    attributes: {
      'deployment.environment': process.env.NODE_ENV,
      'service.version': process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA || 'unknown'
    }
  })
}

(1) Rastreamento de Span Personalizado

TS
// src/lib/tracing.ts
// Rastreamento de Span Personalizado
const SPAN_NAMES = {
  DATABASE_QUERY: 'db.query',
  EXTERNAL_API: 'http.request',
  CACHE_READ: 'cache.read',
  CACHE_WRITE: 'cache.write',
  RENDER_COMPONENT: 'component.render'
} as const

export async function trace<T>(
  spanName: string,
  fn: () => Promise<T>,
  attributes?: Record<string, string>
): Promise<T> {
  // Se OpenTelemetry Indisponível, Executar Diretamente
  if (typeof (globalThis as any).performance === 'undefined') {
    return fn()
  }

  const start = performance.now()
  
  try {
    const result = await fn()
    const duration = performance.now() - start
    
    // Registrado no sistema de log
    if (duration > 100) {
      console.warn(
        `[TRACE] ${spanName} concluído em ${duration.toFixed(2)}ms`,
        { attributes, duration }
      )
    }
    
    return result
  } catch (error) {
    const duration = performance.now() - start
    console.error(`[TRACE] ${spanName} falhou após ${duration.toFixed(2)}ms`, error)
    throw error
  }
}

(2) Uso em Server Action

TS
// src/actions/task.ts
'use server'

import { trace } from '@/lib/tracing'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'

export async function createTask(formData: FormData) {
  return trace('server-action.createTask', async () => {
    const title = formData.get('title') as string
    
    await trace('db.query.create-task', () =>
      prisma.task.create({
        data: { title, projectId: formData.get('projectId') as string, status: 'TODO' }
      })
    )
    
    revalidatePath('/projects')
    return { success: true }
  })
}

▶ Exemplo: Rastreamento de Componente de Servidor

Saída:

TEXT 📖 Somente leitura
Server action executa e chama revalidatePath() para atualizar o cache da página.
TSX
// src/app/projects/page.tsx
import { trace } from '@/lib/tracing'
import { prisma } from '@/lib/prisma'

async function getProjects() {
  return trace('db.query.list-projects', async () => {
    const projects = await prisma.project.findMany({
      include: { _count: { select: { tasks: true } } },
      orderBy: { updatedAt: 'desc' },
      take: 50
    })
    return projects
  })
}

export default async function ProjectsPage() {
  const start = performance.now()
  
  const projects = await getProjects()
  const fetchTime = performance.now() - start
  
  return (
    <div>
      <p data-testid="fetch-time">
        Dados obtidos em {fetchTime.toFixed(0)}ms ({projects.length} projetos)
      </p>
      {/* renderizar projetos... */}
    </div>
  )
}

Saída:

TEXT 📖 Somente leitura
Renderiza a interface do componente ▶ Exemplo: Rastreamento de Componente de Servidor conforme descrito na seção.


8. Identificando e Otimizando Rotas Lentas

(1) Middleware de Log de Rotas Lentas

TS
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const start = Date.now()
  
  const response = NextResponse.next()
  
  // Registrar o tempo gasto após a conclusão da resposta
  response.headers.set('X-Response-Time', '0')
  
  // Usar AsyncLocalStorage ou eventos personalizados
  process.nextTick(() => {
    const duration = Date.now() - start
    
    // Registrar Requisições Lentas
    if (duration > 1000) {
      console.warn(
        `[ROTA LENTA] ${request.method} ${request.nextUrl.pathname} ` +
        `levou ${duration}ms`
      )
    }
  })
  
  return response
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}

(2) Tabela de Análise de Rotas Lentas

Rota Tempo Médio de Resposta P95 Volume de Requisições Estratégia de Otimização
/dashboard 1.234 ms 3.567 ms 10.000/dia Shell estático PPR + divisão Suspense
/reports 2.891 ms 5.200 ms 500/dia Adicionar ISR revalidate=300, cache de relatórios
/api/search 1.567 ms 4.100 ms 8.000/dia Adicionar cache Redis e paginação
/projects/[id]/tasks 892 ms 2.100 ms 6.000/dia Adicionar índices de banco de dados para limitar consultas N+1

(3) Checklist de Otimização de Performance

TSX
// 1. Import Dinâmico de Componentes Pesados
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <div className="animate-pulse h-64" />,
  ssr: false  // Para componentes que não precisam de SEO, desabilitar SSR
})

// 2. Otimização de Imagem
import Image from 'next/image'
<Image
  src="/hero.webp"
  alt="Hero"
  width={1200}
  height={630}
  priority  // Adicionar prioridade para Imagem em Destaque
  placeholder="blur"
  blurDataURL="data:image/webp;base64,..."
/>

// 3. Recuperação Paralela de Dados
const [projects, tasks, members] = await Promise.all([
  getProjects(),
  getTasks(),
  getMembers()
])

// 4. Adicionar Cabeçalhos de Cache
export const revalidate = 300  // ISR 5 minutos
export const dynamic = 'force-static'

// 5. Resposta de Compressão
// next.config.js
compress: true

// 6. Pré-conectar a Origens de Terceiros
<link rel="preconnect" href="https://api.posthog.com" />


9. Exemplo Completo: Dashboard de Monitoramento de Performance

TSX
// src/app/performance/page.tsx
// ============================================
// Dashboard de Monitoramento de Performance
// ============================================
import { prisma } from '@/lib/prisma'

// --- 1. Coleta de Dados ---
async function getPerformanceData() {
  const today = new Date()
  today.setHours(0, 0, 0, 0)
  
  const weekAgo = new Date(today)
  weekAgo.setDate(weekAgo.getDate() - 7)

  // Indicadores de Hoje
  const todayVitals = await prisma.webVital.groupBy({
    by: ['metricName'],
    where: {
      timestamp: { gte: today },
      metricName: { in: ['LCP', 'INP', 'CLS', 'FCP', 'TTFB'] }
    },
    _avg: { metricValue: true },
    _count: true
  })

  // TOP 10 Rotas Lentas
  const slowRoutes = await prisma.webVital.groupBy({
    by: ['url'],
    where: {
      timestamp: { gte: weekAgo },
      metricRating: 'poor'
    },
    _count: true,
    _avg: { metricValue: true },
    orderBy: { _avg: { metricValue: 'desc' } },
    take: 10
  })

  // Distribuição de Equipamentos
  const deviceStats = await prisma.webVital.groupBy({
    by: ['deviceType'],
    where: { timestamp: { gte: today } },
    _count: true
  })

  // Tendências Diárias
  const dailyTrend = await prisma.$queryRaw`
    SELECT
      DATE(timestamp) as day,
      AVG(CASE WHEN metric_name = 'LCP' THEN metric_value END) as avg_lcp,
      AVG(CASE WHEN metric_name = 'CLS' THEN metric_value END) as avg_cls
    FROM web_vitals
    WHERE timestamp >= ${weekAgo}
    GROUP BY DATE(timestamp)
    ORDER BY day ASC
  `

  return { todayVitals, slowRoutes, deviceStats, dailyTrend }
}

// --- 2. Componente de Cartão de Métrica ---
function MetricCard({ name, value, rating, count }: {
  name: string; value: number; rating: string; count: number
}) {
  const colorMap = {
    good: 'text-green-600',
    'needs-improvement': 'text-yellow-600',
    poor: 'text-red-600'
  }

  const formatValue = (metric: string, val: number) => {
    if (metric === 'CLS') return val.toFixed(2)
    return `${val.toFixed(0)}ms`
  }

  return (
    <div className="bg-white rounded-lg shadow p-4">
      <h3 className="text-sm font-medium text-gray-500">{name}</h3>
      <p className={`text-2xl font-bold ${colorMap[rating as keyof typeof colorMap] || ''}`}>
        {formatValue(name, value)}
      </p>
      <p className="text-xs text-gray-400">{count} amostras hoje</p>
    </div>
  )
}

// --- 3. Componentes da Página ---
export default async function PerformancePage() {
  const data = await getPerformanceData()

  return (
    <div className="p-6 space-y-6">
      <h1 className="text-2xl font-bold">Dashboard de Performance</h1>

      {/* CWV de Hoje */}
      <div className="grid grid-cols-5 gap-4">
        {data.todayVitals.map(v => (
          <MetricCard
            key={v.metricName}
            name={v.metricName}
            value={v._avg.metricValue || 0}
            rating={
              (v._avg.metricValue || 0) < 2500 ? 'good' :
              (v._avg.metricValue || 0) < 4000 ? 'needs-improvement' : 'poor'
            }
            count={v._count}
          />
        ))}
      </div>

      {/* Rotas Lentas */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">Rotas Lentas (Últimos 7 Dias)</h2>
        <table className="w-full text-sm">
          <thead>
            <tr className="text-left text-gray-500">
              <th className="pb-2">Rota</th>
              <th className="pb-2">Resposta Média</th>
              <th className="pb-2">Amostras Ruins</th>
            </tr>
          </thead>
          <tbody>
            {data.slowRoutes.map(r => (
              <tr key={r.url} className="border-t">
                <td className="py-2 font-mono">{r.url}</td>
                <td className="py-2">{r._avg.metricValue?.toFixed(0)}ms</td>
                <td className="py-2 text-red-600">{r._count}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Distribuição de Equipamentos */}
      <div className="bg-white rounded-lg shadow p-4">
        <h2 className="text-lg font-semibold mb-4">Distribuição de Dispositivos</h2>
        <div className="flex gap-8">
          {data.deviceStats.map(d => (
            <div key={d.deviceType}>
              <span className="text-2xl font-bold">{d._count}</span>
              <span className="text-gray-500 ml-2">{d.deviceType}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

❓ Perguntas Frequentes

P: Qual é a diferença entre Lighthouse CI e PageSpeed Insights? R: O Lighthouse CI é executado automaticamente dentro de um pipeline CI/CD e permite definir limites para evitar degradação de performance. O PageSpeed Insights é uma ferramenta online fornecida pelo Google baseada em dados reais de usuários CrUX. Os dois se complementam: Lighthouse CI é usado para verificações pré-implantação, enquanto PageSpeed Insights é usado para verificação pós-implantação.

P: O que LCP, INP e CLS no Core Web Vitals representam? R: LCP (Largest Contentful Paint) mede o tempo de renderização do maior elemento de conteúdo e deve ser < 2.5 segundos. INP (Interaction to Next Paint) mede o tempo de resposta da página às interações do usuário e deve ser < 200 ms. CLS (Cumulative Layout Shift) mede a estabilidade do layout da página e deve ser < 0.1. Essas três métricas impactam diretamente os rankings de busca do Google.

P: Qual é a diferença entre "gzip" e "parsed" no relatório do Bundle Analyzer? R: "gzip" refere-se ao tamanho do arquivo após ser comprimido pelo servidor e transmitido (o tamanho real de transferência de rede), enquanto "parsed" refere-se ao tamanho após o navegador descomprimir e analisar o arquivo (que afeta o tempo de processamento do motor JS). Normalmente, você deve focar no tamanho gzip (gargalo de transferência de rede) e no tamanho parsed (gargalo de processamento de CPU). O Next.js habilita compressão gzip por padrão.

P: useReportWebVitals e @vercel/speed-insights entram em conflito? R: Não, eles não entram em conflito. useReportWebVitals é um callback Web Vitals integrado do Next.js que você pode usar para reportar dados para seu próprio serviço de análise. @vercel/speed-insights é um serviço pago fornecido pela Vercel que coleta e exibe automaticamente dados RUM. Ambos podem ser usados simultaneamente sem interferir um no outro.

P: Qual é a diferença entre /instrumentation.ts e middleware.ts? R: instrumentation.ts é executado uma vez quando a aplicação inicia (quando o servidor inicia) e é usado para registrar telemetria global como OpenTelemetry e Sentry. middleware.ts é executado antes de cada requisição e é usado para interceptação de requisições, redirecionamento e injeção de cabeçalhos. Para rastreamento de performance, recomendamos usar instrumentation.ts (global), envolvido em uma Server Action (lógica de negócios) e middleware (nível de requisição).

P: Por onde devo começar a otimizar rotas lentas? R: Etapas de otimização recomendadas: (1) Use Bundle Analyzer para identificar e dividir bundles grandes; (2) Defina guardiões de performance usando Lighthouse CI; (3) Colete dados RUM para identificar os 5% de usuários mais lentos; (4) Comece otimizando as rotas mais lentas com o maior volume de requisições (usando PPR, ISR e import dinâmico); (5) Otimize consultas de banco de dados (consultas N+1, índices ausentes).


📖 Resumo


📝 Exercícios

  1. Questão Básica (⭐): Configure lighthouserc.js para auditar a página inicial e a página de login, defina orçamentos de performance de LCP < 2.5s e CLS < 0.1, e execute lhci autorun localmente para verificar se a configuração passa.

  2. Exercício Avançado (⭐⭐): Integre useReportWebVitals em sua aplicação: (1) Crie uma rota API para reportar dados e armazene-os no banco de dados; (2) Adicione o componente WebVitals ao layout raiz; (3) Crie uma página /performance para exibir as métricas CWV de hoje e as 10 rotas mais lentas.

  3. Desafio (⭐⭐⭐): Implemente um dashboard abrangente de monitoramento de performance: (1) Use Bundle Analyzer para analisar o tamanho atual do bundle e reduza o JavaScript da primeira tela em mais de 40% através de carregamento dinâmico; (2) Configure um gate de GitHub Actions do Lighthouse CI para bloquear o merge de pull requests (PRs) se a pontuação de performance cair mais de 5 pontos; (3) Integre o rastreamento automatizado capture_performance do PostHog e crie um gráfico de tendência CWV de 7 dias.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%