Voltar ao Blog
    Performance
    Frontend

    Performance Web: De 3s Para 0.8s Na Prática

    LCP, CLS, INP — métricas reais e o que fiz para melhorar cada uma delas neste portfólio.

    Publicado em ·11 min de leitura·por Bernardo Gomes

    Por que performance importa além do UX

    Google usa Core Web Vitals como sinal de ranking desde 2021. LCP ruim não é apenas UX ruim — é SEO penalizado. Para um portfólio que precisa ser encontrado, isso importa.

    Baseline: o que eu tinha

    • LCP: 3.2s (imagem hero sem preload, fonte bloqueante)
    • CLS: 0.18 (layout shift de imagens sem dimensões)
    • INP: 280ms (animações caras em JS no main thread)
    • FCP: 2.1s (JS bundle grande bloqueando parsing)
    • Bundle: 847KB total, sem code splitting

    Fix #1: LCP — preload da imagem hero

    O LCP é determinado pelo elemento visível mais pesado no viewport. Neste site, era a imagem de perfil no hero. Sem preload, ela esperava o JS carregar e renderizar para começar o download.

    html
    <!-- index.html: preload explícito da imagem LCP -->
    <link
      rel="preload"
      as="image"
      href="/images/profile.webp"
      fetchpriority="high"
    />
    💡

    fetchpriority='high' diz ao browser para priorizar esse recurso sobre outros. Combinado com preload, garante que a imagem LCP começa a baixar o mais cedo possível.

    Fix #2: CLS — dimensões explícitas em imagens

    Layout shift acontece quando o browser não sabe o tamanho de um elemento antes de carregar. A correção é simples: sempre definir width e height em imagens.

    tsx
    // Antes — CLS score 0.18
    <img src="/profile.webp" alt="Bernardo" className="rounded-full" />
    
    // Depois — CLS score 0.02
    <img
      src="/profile.webp"
      alt="Bernardo"
      width={200}
      height={200}
      className="rounded-full"
      loading="eager"
    />

    Fix #3: INP — animações no compositor

    INP mede responsividade a interações. Animações caras no main thread bloqueiam inputs. A solução: só animar properties que o browser pode delegar ao GPU (transform e opacity).

    tsx
    // Ruim: anima propriedades que causam reflow
    animate={{ left: 0, top: 0, width: "100%" }}
    
    // Bom: só transform e opacity vão para o compositor
    animate={{ x: 0, y: 0, opacity: 1 }}

    Fix #4: Code splitting por rota

    O bundle de 847KB carregava tudo na primeira visita, incluindo páginas que o usuário talvez nunca abrisse. React.lazy() com routes resolve isso.

    tsx
    // App.tsx — lazy loading por rota
    const Index = lazy(() => import("./pages/Index"));
    const Services = lazy(() => import("./pages/Services"));
    const Blog = lazy(() => import("./pages/Blog/BlogPage"));
    
    // Com Suspense wrapping routes
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/" element={<Index />} />
        <Route path="/services" element={<Services />} />
        <Route path="/blog" element={<Blog />} />
      </Routes>
    </Suspense>

    Resultado final

    • LCP: 3.2s → 0.8s (75% melhora)
    • CLS: 0.18 → 0.02 (89% melhora)
    • INP: 280ms → 95ms (66% melhora)
    • FCP: 2.1s → 0.6s (71% melhora)
    • Lighthouse Performance: 61 → 97