- feat(025): favoritos locais com FavoritesContext, HeartButton, PublicFavoritesPage
- feat(026): central de contatos admin (leads/contatos unificados)
- feat(027): configuração da página de contato via admin
- feat(028): trabalhe conosco - candidaturas com upload e admin
- feat(029): UX área do cliente - visitas, comparação, perfil
- feat(030): navbar UX - menu mobile, ThemeToggle, useFavorites
- feat(031): hero light/dark - imagens separadas por tema, upload, preview, seed
- feat(032): performance homepage - Promise.all parallel fetches, sessionStorage cache,
preload hero image, loading=lazy nos cards, useInView hook, will-change carrossel,
keyframes em index.css, AgentsCarousel e HomeScrollScene via props
- fix: light mode HomeScrollScene - gradiente, cores de texto, scroll hint
migrations: g1h2i3j4k5l6 (source em leads), h1i2j3k4l5m6 (contact_config),
i1j2k3l4m5n6 (job_applications), j2k3l4m5n6o7 (hero theme images)
11 KiB
Auditoria de Performance — Página Inicial (Home)
Versão: 1.0 · Data: 2026-04-22
Escopo:HomePage.tsx,HomeScrollScene.tsx,AgentsCarousel.tsx,PropertyRowCard.tsx,Navbar.tsx
1. Resumo Executivo
A página inicial é a mais crítica em termos de first impression e conversão. A análise identificou 5 categorias de problemas que impactam diretamente o Core Web Vitals (LCP, CLS, INP) e a experiência percebida do usuário:
| Categoria | Severidade | Impacto estimado no LCP |
|---|---|---|
| Requests em cascata (waterfall) | 🔴 Alta | +600–1200 ms |
| Imagem hero sem preload / sem dimensões | 🔴 Alta | +800–1500 ms |
Scroll scene: useTheme + re-renders desnecessários |
🟡 Média | +60–150 ms INP |
AgentsCarousel: autoplay sem requestAnimationFrame |
🟡 Média | jank visual |
RiseCard: IntersectionObserver por instância |
🟢 Baixa | +n×2 ms compositing |
2. Problemas Identificados
2.1 Waterfall de Requests (Crítico)
Arquivo: HomePage.tsx + HomeScrollScene.tsx
Problema atual:
Render HomePage
└─ useEffect: getHomepageConfig() ← request 1 (bloqueia backgroundImage)
└─ setState(config) → re-render
└─ HomeScrollScene recebe props
└─ useEffect: getFeaturedProperties() ← request 2 só começa AQUI
└─ useEffect: getAgents() ← request 3 só começa APÓS mount
Os três requests são seriais por dependência de render: getAgents e getFeaturedProperties só disparam após HomeScrollScene montar, que depende do render de HomePage, que depende de getHomepageConfig.
Impacto: Em conexões 3G, o usuário espera 3× o RTT antes de ver qualquer conteúdo real. LCP pode ultrapassar 4 s.
Solução: Paralelizar os três fetches no topo de HomePage com Promise.all e passar dados via props.
2.2 Hero Image sem <link rel="preload"> e sem width/height (Crítico)
Arquivo: HomeScrollScene.tsx, linha da tag <img>
Problema atual:
<img
src={backgroundImage}
alt=""
aria-hidden="true"
className="absolute inset-0 w-full h-full object-cover"
/>
- Sem
width/height→ o browser não reserva espaço → CLS quando a imagem carrega - Sem
<link rel="preload">no<head>→ a imagem começa a baixar somente quando o React renderiza o componente, não durante o parse do HTML - Sem
fetchpriority="high"→ browser não prioriza sobre outros assets - Sem
loading="eager"explícito → comportamento depende do browser
Impacto: LCP degradado. Em telas de 1080p com imagem de 1 MB, o atraso pode ser de 1–2 s adicionais.
Solução:
- Adicionar
fetchpriority="high"eloading="eager"na tag<img> - Injetar
<link rel="preload">dinamicamente viauseEffectassim quebackgroundImageestiver disponível (ou via<Helmet>)
2.3 useTheme sendo chamado duas vezes para o mesmo dado
Arquivos: HomePage.tsx e HomeScrollScene.tsx
Problema atual:
// HomePage.tsx
const { resolvedTheme } = useTheme()
const themedBackgroundImage = resolvedTheme === 'dark' ? ... : ...
// HomeScrollScene.tsx (recebe backgroundImage, mas chama useTheme DE NOVO)
const { resolvedTheme } = useTheme()
const isLight = resolvedTheme === 'light'
resolvedTheme é derivado duas vezes. Qualquer mudança de tema causa re-render em dois componentes separados com lógica duplicada. Além disso, HomeScrollScene não precisa conhecer o tema se receber backgroundImage já resolvida pelo pai — mas ainda usa isLight para estilos internos, o que é legítimo. O problema é a derivação themedBackgroundImage estar fora do componente que a consome.
Solução: Manter isLight em HomeScrollScene (necessário para estilos), mas mover themedBackgroundImage para dentro de HomeScrollScene eliminando a prop intermediária e a chamada dupla ao context em HomePage.
2.4 AgentsCarousel: setInterval sem cleanup confiável + CSS transform sem will-change
Arquivo: AgentsCarousel.tsx
Problema atual:
const autoplayRef = useRef<ReturnType<typeof setInterval> | null>(null)
// ...sem uso de requestAnimationFrame
// CSS translate via inline style, sem will-change: transform
setIntervalpara animação de scroll não é sincronizado com o frame rate do browser → pode causar jank em displays 120 Hz- Ausência de
will-change: transformno track → o browser não promove a camada para GPU antes da primeira animação → primeiro frame pode ser janky - Duplicação de slides (
[...agents, ...agents]) semkeyestável baseada emagent.idúnico (usa índice implicitamente)
2.5 RiseCard: IntersectionObserver por instância sem threshold otimizado
Arquivo: HomeScrollScene.tsx
Problema atual:
// Cria 1 IntersectionObserver por card (até 6+ na home)
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setVisible(true)
observer.disconnect()
}
}, { threshold: 0.05 })
Cada RiseCard instancia seu próprio IntersectionObserver. Com 6 cards, são 6 observers ativos simultaneamente. O ideal é um único observer compartilhado (padrão singleton/context) que observa todos os elementos.
Impacto: Pequeno em volume baixo, mas escala mal. 6 observers × scroll events = trabalho desnecessário na main thread.
2.6 <style> inline com @keyframes renderizado dentro do componente
Arquivo: HomeScrollScene.tsx
Problema atual:
<style>{`
@keyframes fadeDown {
0%, 100% { opacity: 0; transform: translateY(-4px); }
50% { opacity: 1; transform: translateY(4px); }
}
`}</style>
Isso insere uma tag <style> no DOM a cada render. O browser re-parseia o CSS. O correto é mover para index.css ou um módulo CSS estático.
2.7 PropertyRowCard dentro da Home: fotos sem loading="lazy"
Arquivo: PropertyRowCard.tsx → SlideImage
Problema atual:
<img
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
className={...}
draggable={false}
/>
Sem loading="lazy", o browser baixa todas as imagens dos cards de destaque imediatamente, competindo com a imagem hero pelo bandwidth no carregamento inicial.
3. Core Web Vitals — Estado Atual vs. Meta
| Métrica | Estado estimado atual | Meta (Good) | Gap |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ~3.5–5 s | < 2.5 s | ~1.5 s |
| CLS (Cumulative Layout Shift) | ~0.12–0.18 | < 0.1 | ~0.05 |
| INP (Interaction to Next Paint) | ~150–250 ms | < 200 ms | borderline |
| FCP (First Contentful Paint) | ~1.2–1.8 s | < 1.8 s | borderline |
| TTFB (Time to First Byte) | ~200–400 ms | < 800 ms | OK |
4. Plano de Implementação
Fase 1 — Quick Wins (alto impacto, baixo risco) · Estimativa: 1 dia
| # | Ação | Arquivo | Impacto |
|---|---|---|---|
| 1.1 | Adicionar fetchpriority="high" + loading="eager" na hero image |
HomeScrollScene.tsx |
LCP −400 ms |
| 1.2 | Mover @keyframes fadeDown para index.css |
HomeScrollScene.tsx + index.css |
Elimina re-parse CSS |
| 1.3 | Adicionar loading="lazy" nos SlideImage dos cards |
PropertyRowCard.tsx |
Bandwidth LCP |
| 1.4 | Adicionar will-change: transform no track do carousel |
AgentsCarousel.tsx |
Elimina jank GPU |
Fase 2 — Paralelização de Requests (crítico) · Estimativa: 0.5 dia
| # | Ação | Arquivo | Impacto |
|---|---|---|---|
| 2.1 | Promise.all([getHomepageConfig(), getFeaturedProperties(), getAgents()]) em HomePage |
HomePage.tsx |
LCP −600 ms |
| 2.2 | HomeScrollScene e AgentsCarousel passam a receber dados via props |
Ambos | Elimina waterfalls |
| 2.3 | Preload dinâmico da hero image via <link rel="preload"> |
HomePage.tsx |
LCP −300 ms |
Fase 3 — Refatoração de Observers (qualidade) · Estimativa: 0.5 dia
| # | Ação | Arquivo | Impacto |
|---|---|---|---|
| 3.1 | Criar hook useIntersectionObserver compartilhado |
hooks/useIntersectionObserver.ts |
Reduz observers de N→1 |
| 3.2 | Refatorar RiseCard para usar o hook compartilhado |
HomeScrollScene.tsx |
Main thread reduzida |
| 3.3 | Consolidar lógica de resolvedTheme / isLight |
HomeScrollScene.tsx |
Elimina re-render duplo |
Fase 4 — Optimistic UI e Caching (avançado) · Estimativa: 1 dia
| # | Ação | Impacto |
|---|---|---|
| 4.1 | Cache de homepageConfig em sessionStorage (TTL 5 min) |
FCP sem spinner na navegação de volta |
| 4.2 | Skeleton de altura fixa na hero para evitar CLS | CLS −0.08 |
| 4.3 | React.memo em PropertyRowCard e AgentSlide |
INP nos re-renders de tema |
5. Referências de Código — Soluções Concretas
5.1 Paralelização de requests (Fase 2.1)
// HomePage.tsx — ANTES
useEffect(() => {
getHomepageConfig().then(setConfig).finally(() => setIsLoading(false))
}, [])
// HomePage.tsx — DEPOIS
useEffect(() => {
Promise.all([getHomepageConfig(), getFeaturedProperties(), getAgents()])
.then(([cfg, props, agts]) => {
setConfig(cfg)
setFeaturedProperties(props)
setAgents(agts)
})
.catch(() => {})
.finally(() => setIsLoading(false))
}, [])
5.2 Preload dinâmico da hero (Fase 2.3)
// HomePage.tsx
useEffect(() => {
if (!themedBackgroundImage) return
const link = document.createElement('link')
link.rel = 'preload'
link.as = 'image'
link.href = themedBackgroundImage
document.head.appendChild(link)
return () => { document.head.removeChild(link) }
}, [themedBackgroundImage])
5.3 Hero image com prioridade correta (Fase 1.1)
<img
src={backgroundImage}
alt=""
aria-hidden="true"
fetchPriority="high"
loading="eager"
decoding="async"
className="absolute inset-0 w-full h-full object-cover"
/>
5.4 Hook compartilhado de IntersectionObserver (Fase 3.1)
// hooks/useInView.ts
export function useInView(options?: IntersectionObserverInit) {
const ref = useRef<HTMLDivElement>(null)
const [inView, setInView] = useState(false)
useEffect(() => {
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setInView(true)
observer.disconnect()
}
}, options)
observer.observe(el)
return () => observer.disconnect()
}, [])
return { ref, inView }
}
6. Métricas Esperadas Após Implementação Completa
| Métrica | Atual (estimado) | Após Fase 1+2 | Após Fase 1+2+3+4 |
|---|---|---|---|
| LCP | ~4 s | ~2.2 s | ~1.8 s |
| CLS | ~0.15 | ~0.08 | ~0.04 |
| INP | ~200 ms | ~150 ms | ~80 ms |
| Requests em paralelo | 3 seriais | 3 paralelos | 3 paralelos + cache |