sass-imobiliaria/specs/032-performance-homepage/performance-audit.md
MatheusAlves96 cf5603243c
Some checks failed
CI/CD → Deploy via SSH / Build & Push Docker Images (push) Successful in 1m0s
CI/CD → Deploy via SSH / Deploy via SSH (push) Successful in 4m35s
CI/CD → Deploy via SSH / Validate HTTPS & Endpoints (push) Failing after 46s
feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
- 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)
2026-04-22 22:35:17 -03:00

11 KiB
Raw Blame History

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 +6001200 ms
Imagem hero sem preload / sem dimensões 🔴 Alta +8001500 ms
Scroll scene: useTheme + re-renders desnecessários 🟡 Média +60150 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.


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 12 s adicionais.

Solução:

  1. Adicionar fetchpriority="high" e loading="eager" na tag <img>
  2. Injetar <link rel="preload"> dinamicamente via useEffect assim que backgroundImage estiver 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
  • setInterval para 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: transform no 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]) sem key estável baseada em agent.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.tsxSlideImage

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.55 s < 2.5 s ~1.5 s
CLS (Cumulative Layout Shift) ~0.120.18 < 0.1 ~0.05
INP (Interaction to Next Paint) ~150250 ms < 200 ms borderline
FCP (First Contentful Paint) ~1.21.8 s < 1.8 s borderline
TTFB (Time to First Byte) ~200400 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