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

302 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
---
### 2.2 Hero Image sem `<link rel="preload">` e sem width/height (Crítico)
**Arquivo:** `HomeScrollScene.tsx`, linha da tag `<img>`
**Problema atual:**
```tsx
<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:**
```tsx
// 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:**
```tsx
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:**
```tsx
// 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:**
```tsx
<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:**
```tsx
<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 N1 |
| 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)
```tsx
// 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)
```tsx
// 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)
```tsx
<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)
```ts
// 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 |