- 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)
302 lines
11 KiB
Markdown
302 lines
11 KiB
Markdown
# 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:**
|
||
```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 1–2 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.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)
|
||
|
||
```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 |
|