Performance e Core Web Vitals: de 3.8s para 0.9s de LCP (reduzindo bounce em 42%)

Guia completo de otimização: lazy loading, code splitting, image optimization e caching. De poor para good em todas as métricas.

Performance não é detalhe. É diferença entre converter ou perder o usuário.

Dados do Google (2023):

  • Site carrega em 1s → Conversão 100% (baseline)
  • Site carrega em 3s → Conversão -32%
  • Site carrega em 5s → Conversão -53%
  • Site carrega em 10s → Conversão -90%

Core Web Vitals = métricas oficiais do Google que afetam SEO e conversão.

Este artigo mostra como otimizar LCP, FID e CLS de “poor” para “good”.

As 3 métricas Core Web Vitals

1. LCP (Largest Contentful Paint)

O que é: Tempo até elemento principal estar visível.

Thresholds:

  • Good: menor que 2,5s
  • ⚠️ Needs improvement: 2,5-4s
  • Poor: maior que 4s

Elementos que contam como LCP:

  • <img> tags
  • <image> dentro de <svg>
  • <video> poster
  • Elemento com background-image
  • Bloco de texto

Como medir:

// lib/web-vitals.ts
import { onLCP } from 'web-vitals'

onLCP((metric) => {
  console.log('LCP:', metric.value, 'ms')

  // Envia para analytics
  gtag('event', 'web_vitals', {
    event_category: 'Web Vitals',
    event_label: 'LCP',
    value: Math.round(metric.value),
    non_interaction: true
  })
})

Otimizações:

A) Priorizar imagem hero (above the fold):

// components/HeroImage.tsx
import Image from 'next/image'

export function HeroImage() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1920}
      height={1080}
      priority // ← Carrega antes de tudo
      quality={90}
      placeholder="blur"
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." // Low quality placeholder
    />
  )
}

B) Preload critical resources:

// app/layout.tsx
export default function RootLayout({ children }: Props) {
  return (
    <html>
      <head>
        {/* Preload fontes */}
        <link
          rel="preload"
          href="/fonts/inter-var.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />

        {/* Preconnect a APIs externas */}
        <link rel="preconnect" href="https://api.suaempresa.com" />
        <link rel="dns-prefetch" href="https://cdn.suaempresa.com" />
      </head>
      <body>{children}</body>
    </html>
  )
}

C) Otimizar imagens (WebP + tamanhos corretos):

# Converte PNG/JPG para WebP (70-80% menor)
npx @squoosh/cli --webp auto images/*.{png,jpg}
// next.config.js
module.exports = {
  images: {
    formats: ['image/webp', 'image/avif'], // Formatos modernos
    deviceSizes: [640, 750, 828, 1080, 1200, 1920], // Responsive
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 31536000, // 1 ano
  }
}

D) Self-host critical fonts:

/* app/globals.css */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-weight: 100 900;
  font-display: swap; /* Mostra fallback font até carregar */
  font-style: normal;
}

Resultado:

  • Antes: LCP 3,8s (Poor) - Google Fonts externo + PNG 2MB
  • Depois: LCP 0,9s (Good) - Self-hosted font + WebP 180KB
  • Melhoria: -76% (3x mais rápido)

2. FID (First Input Delay)

O que é: Tempo até site responder à primeira interação (clique, tap).

Thresholds:

  • Good: menor que 100ms
  • ⚠️ Needs improvement: 100-300ms
  • Poor: maior que 300ms

Causa comum: JavaScript bloqueando thread principal.

Como medir:

import { onFID } from 'web-vitals'

onFID((metric) => {
  console.log('FID:', metric.value, 'ms')
})

Otimizações:

A) Code splitting (carregar apenas JS necessário):

// app/dashboard/page.tsx
import dynamic from 'next/dynamic'

// Lazy load componente pesado
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false // Não renderiza no servidor
})

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <HeavyChart /> {/* Carrega apenas quando necessário */}
    </div>
  )
}

B) Defer de scripts não-críticos:

// app/layout.tsx
<Script
  src="https://chat.suporte.com/widget.js"
  strategy="lazyOnload" // Carrega após página estar pronta
/>

<Script
  src="https://analytics.com/script.js"
  strategy="afterInteractive" // Carrega após interatividade
/>

C) Web Workers para processamento pesado:

// workers/data-processor.ts
self.addEventListener('message', (e) => {
  const { data } = e.data

  // Processamento pesado (não bloqueia UI)
  const result = complexCalculation(data)

  self.postMessage({ result })
})

// Uso
const worker = new Worker('/workers/data-processor.js')

worker.postMessage({ data: largeDataset })

worker.addEventListener('message', (e) => {
  const { result } = e.data
  setProcessedData(result)
})

Resultado:

  • Antes: FID 280ms (Poor) - Bundle 800KB, tudo eager-loaded
  • Depois: FID 45ms (Good) - Bundle 180KB inicial, lazy-loading
  • Melhoria: -84%

3. CLS (Cumulative Layout Shift)

O que é: Instabilidade visual (elementos pulando durante carregamento).

Thresholds:

  • Good: menor que 0,1
  • ⚠️ Needs improvement: 0,1-0,25
  • Poor: maior que 0,25

Causas comuns:

  • Imagens sem width/height
  • Ads/embeds dinâmicos
  • Fontes causando FOIT (Flash of Invisible Text)
  • Conteúdo injetado acima do fold

Como medir:

import { onCLS } from 'web-vitals'

onCLS((metric) => {
  console.log('CLS:', metric.value)

  // Identifica elementos causando shift
  metric.entries.forEach((entry) => {
    console.log('Layout shift:', entry)
  })
})

Otimizações:

A) Definir dimensões em imagens:

// ❌ ERRADO (causa CLS)
<img src="/produto.jpg" alt="Produto" />

// ✅ CORRETO
<Image
  src="/produto.jpg"
  alt="Produto"
  width={400}
  height={300}
  placeholder="blur"
/>

B) Reservar espaço para ads/embeds:

// components/AdSlot.tsx
export function AdSlot() {
  return (
    <div
      className="ad-container"
      style={{
        minHeight: '250px', // Reserva espaço
        width: '300px',
        backgroundColor: '#f0f0f0' // Placeholder
      }}
    >
      {/* Ad carrega aqui */}
      <script async src="https://ads.com/script.js" />
    </div>
  )
}

C) Font-display: swap (evita FOIT):

@font-face {
  font-family: 'Custom';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap; /* Mostra fallback, depois troca */
}

D) Skeleton screens (placeholders):

// components/Skeleton.tsx
export function ProductCardSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="bg-gray-300 h-48 w-full mb-4"></div>
      <div className="bg-gray-300 h-4 w-3/4 mb-2"></div>
      <div className="bg-gray-300 h-4 w-1/2"></div>
    </div>
  )
}

// Uso
{isLoading ? <ProductCardSkeleton /> : <ProductCard product={product} />}

Resultado:

  • Antes: CLS 0,38 (Poor) - Imagens sem dimensões, FOIT
  • Depois: CLS 0,04 (Good) - Aspect ratios, font-display:swap
  • Melhoria: -89%

Bundle size optimization

Análise de bundle:

# Next.js bundle analyzer
npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
})

module.exports = withBundleAnalyzer({
  // config
})

# Roda análise
ANALYZE=true npm run build

Otimizações comuns:

A) Tree-shaking (remover código não usado):

// ❌ ERRADO (importa TUDO do lodash - 70KB)
import _ from 'lodash'
const result = _.debounce(fn, 300)

// ✅ CORRETO (importa apenas debounce - 3KB)
import debounce from 'lodash/debounce'
const result = debounce(fn, 300)

B) Remover dependências pesadas:

# Antes: moment.js (288KB)
npm uninstall moment

# Depois: date-fns (modular, 12KB)
npm install date-fns
// Antes
import moment from 'moment'
const formatted = moment(date).format('DD/MM/YYYY')

// Depois
import { format } from 'date-fns'
const formatted = format(date, 'dd/MM/yyyy')

C) Dynamic imports (código assíncrono):

// Antes: Importa Stripe no bundle inicial (+87KB)
import { loadStripe } from '@stripe/stripe-js'

// Depois: Carrega apenas quando necessário
const handlePayment = async () => {
  const { loadStripe } = await import('@stripe/stripe-js')
  const stripe = await loadStripe(STRIPE_KEY)
  // ...
}

Caching strategies

1. Static Generation (SSG)

Quando usar: Conteúdo que muda raramente (landing pages, blog).

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts()

  return posts.map((post) => ({
    slug: post.slug
  }))
}

export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug)

  return <Article post={post} />
}

// Revalidate cache a cada 1h
export const revalidate = 3600

2. Server-Side Rendering (SSR) com cache

// app/dashboard/page.tsx
import { unstable_cache } from 'next/cache'

const getCachedData = unstable_cache(
  async (userId) => {
    return await fetchUserData(userId)
  },
  ['user-dashboard'],
  { revalidate: 300 } // Cache por 5min
)

export default async function Dashboard() {
  const data = await getCachedData(userId)
  return <DashboardView data={data} />
}

3. Client-side caching (SWR)

// hooks/use-bookings.ts
import useSWR from 'swr'

export function useBookings(userId: string) {
  const { data, error, isLoading } = useSWR(
    `/api/bookings?userId=${userId}`,
    fetcher,
    {
      revalidateOnFocus: false,
      dedupingInterval: 60000, // Cache 1min
      refreshInterval: 300000 // Auto-refresh 5min
    }
  )

  return { bookings: data, error, isLoading }
}

4. HTTP caching headers

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable' // 1 ano
          }
        ]
      },
      {
        source: '/api/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, s-maxage=60, stale-while-revalidate=300'
          }
        ]
      }
    ]
  }
}

CDN e edge caching

Vercel (Next.js):

// Configuração automática de edge caching
export const runtime = 'edge' // Roda em 300+ datacenters

// Cache agressivo em assets estáticos
export const revalidate = false // Never revalidate

Cloudflare CDN (assets estáticos):

# cloudflare-cache-rules.txt
/*.css cache 1 year
/*.js cache 1 year
/*.woff2 cache 1 year
/images/* cache 1 month
/api/* cache 5 minutes

Checklist de performance

Antes de lançar:

  • LCP menor que 2,5s (teste no PageSpeed Insights)
  • FID menor que 100ms
  • CLS menor que 0,1
  • First bundle menor que 200KB (gzip)
  • Todas as imagens em WebP/AVIF
  • Lazy loading de imagens below-the-fold
  • Fontes self-hosted (ou com font-display:swap)
  • Code splitting implementado
  • HTTP/2 ou HTTP/3 ativo
  • Gzip/Brotli compression ativo

Monitoramento contínuo:

  • Web Vitals tracking (Mixpanel/GA4)
  • Lighthouse CI em cada deploy
  • Real User Monitoring (RUM) ativo
  • Alertas para regressões de performance

Case real: E-commerce que aumentou conversão em 42%

Perfil: Loja online de roupas (50K visitantes/mês).

Problema (métricas iniciais):

  • LCP: 4,2s (Poor) ← Hero image 3,5MB PNG
  • FID: 320ms (Poor) ← Bundle 950KB
  • CLS: 0,28 (Poor) ← Imagens sem dimensões
  • Bounce rate: 68%
  • Conversão: 1,8%

Otimizações implementadas (3 semanas):

Semana 1: Imagens

  • Converteu para WebP (3,5MB → 280KB, -92%)
  • Adicionou priority em hero
  • Lazy loading abaixo do fold

Semana 2: JavaScript

  • Code splitting (950KB → 180KB inicial)
  • Dynamic imports (Stripe, analytics)
  • Removeu libraries não usadas

Semana 3: Layout

  • Definiu aspect ratios em imagens
  • Skeleton screens
  • Font-display:swap

Resultado (3 meses após):

MétricaAntesDepoisVariação
LCP4,2s1,1s-74%
FID320ms52ms-84%
CLS0,280,05-82%
Bounce rate68%39%-43%
Conversão1,8%2,9%+61%
Receita/mêsR$ 86KR$ 142K+65%

ROI:

  • Investimento: R$ 18K (3 semanas de dev)
  • Ganho mensal: R$ 56K (receita extra)
  • Payback: menor que 2 semanas

Ferramentas de análise

1. PageSpeed Insights: https://pagespeed.web.dev

  • Mostra Core Web Vitals
  • Dicas de otimização
  • Teste mobile e desktop

2. Lighthouse CI (integração com CI/CD):

# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v9
        with:
          urls: |
            https://preview-${{ github.event.number }}.vercel.app
          uploadArtifacts: true
          temporaryPublicStorage: true

3. WebPageTest: https://webpagetest.org

  • Teste em devices reais
  • Filmstrip (loading visual)
  • Waterfall de requests

4. Chrome DevTools:

  • Performance tab (profiling)
  • Coverage tab (código não usado)
  • Network tab (payload analysis)

Próximos passos

  1. Medir baseline (PageSpeed Insights)
  2. Otimizar LCP (imagens, fonts, preload)
  3. Otimizar FID (code splitting, defer)
  4. Otimizar CLS (aspect ratios, skeletons)
  5. Setup caching (SSG, SWR, HTTP headers)
  6. Configurar CDN (Vercel, Cloudflare)
  7. Monitorar Web Vitals (analytics)
  8. Lighthouse CI em cada PR

Lembre-se: Cada 100ms economizados = +1% conversão. Performance = receita.

Pronto para sair do manual?

Agende o diagnóstico gratuito. Vamos mapear o gargalo, estimar o impacto e definir o primeiro resultado mensurável.

Você sai com clareza — não com um pitch de vendas.