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.
<!-- 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.
// 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).
// 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.
// 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