- All SKILL.md files now <500 lines (avg reduction 69%) - Detailed content extracted to references/ subdirectories - Frontmatter standardised: only name + description (Anthropic standard) - New skills: brand-guidelines, spec-coauthor, report-templates, skill-creator - Design skills: anti-slop guidelines, premium-proposals reference - Removed non-standard frontmatter fields (triggers, version, author, category) Plugins affected: infraestrutura, marketing, dev-tools, crm-ops, gestao, core-tools, negocio, perfex-dev, wordpress, design-media Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
246 lines
6.5 KiB
Markdown
246 lines
6.5 KiB
Markdown
# /nextjs - Performance Rules (Vercel Engineering)
|
|
|
|
57 regras priorizadas por impacto. Fonte: Vercel Engineering react-best-practices.
|
|
|
|
## 1. Eliminating Waterfalls (CRITICAL)
|
|
|
|
```tsx
|
|
// ❌ ERRADO: Awaits sequenciais (waterfall)
|
|
const session = await getSession()
|
|
const config = await getConfig()
|
|
const user = await getUser(session.userId)
|
|
|
|
// ✅ CORRECTO: Paralelo com Promise.all()
|
|
const [session, config] = await Promise.all([
|
|
getSession(),
|
|
getConfig()
|
|
])
|
|
const user = await getUser(session.userId) // depende de session
|
|
|
|
// ✅ CORRECTO: Suspense boundaries estratégicos
|
|
async function Page() {
|
|
return (
|
|
<Layout>
|
|
<Suspense fallback={<HeaderSkeleton />}>
|
|
<Header />
|
|
</Suspense>
|
|
<Suspense fallback={<ContentSkeleton />}>
|
|
<SlowContent />
|
|
</Suspense>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
// ✅ CORRECTO: Defer await até necessário
|
|
async function handler(req) {
|
|
const dataPromise = fetchData() // inicia imediatamente
|
|
if (req.method === 'HEAD') return new Response() // early return
|
|
const data = await dataPromise // só espera quando precisa
|
|
return Response.json(data)
|
|
}
|
|
```
|
|
|
|
**Regras:**
|
|
- `async-defer-await`: Mover await para onde é necessário
|
|
- `async-dependency-parallel`: Usar `better-all` para dependências parciais
|
|
- `async-prevent-waterfall`: Iniciar promises antes de awaitar
|
|
- `async-promise-all`: `Promise.all()` para operações independentes
|
|
- `async-suspense-boundaries`: Suspense para UI progressiva
|
|
|
|
---
|
|
|
|
## 2. Bundle Size (CRITICAL)
|
|
|
|
```tsx
|
|
// ❌ ERRADO: Barrel file imports
|
|
import { Button, Icon } from '@mui/material' // carrega tudo
|
|
|
|
// ✅ CORRECTO: Import directo
|
|
import Button from '@mui/material/Button'
|
|
|
|
// ✅ CORRECTO: optimizePackageImports (Next.js 13.5+)
|
|
module.exports = {
|
|
experimental: {
|
|
optimizePackageImports: ['lucide-react', '@mui/material', 'lodash']
|
|
}
|
|
}
|
|
|
|
// ✅ CORRECTO: Dynamic imports para componentes pesados
|
|
const MonacoEditor = dynamic(() => import('./MonacoEditor'), {
|
|
ssr: false,
|
|
loading: () => <EditorSkeleton />
|
|
})
|
|
|
|
// ✅ CORRECTO: Preload em hover/focus
|
|
function FeatureButton() {
|
|
return (
|
|
<button
|
|
onMouseEnter={() => import('./HeavyFeature')}
|
|
onClick={openFeature}
|
|
>
|
|
Open Feature
|
|
</button>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Server-Side Performance (HIGH)
|
|
|
|
```tsx
|
|
// ✅ Server Actions: autenticar SEMPRE
|
|
'use server'
|
|
async function updateProfile(formData: FormData) {
|
|
const session = await auth() // 1. Autenticar
|
|
if (!session) throw new Error('Unauthorized')
|
|
const data = schema.parse(formData) // 2. Validar
|
|
await db.user.update({ where: { id: session.userId }, data })
|
|
revalidatePath('/profile')
|
|
}
|
|
|
|
// ✅ React.cache() para deduplicação per-request
|
|
const getUser = cache(async (id: string) => {
|
|
return db.user.findUnique({ where: { id } })
|
|
})
|
|
|
|
// ✅ after() para operações não-bloqueantes
|
|
import { after } from 'next/server'
|
|
async function submitForm(data: FormData) {
|
|
const result = await processForm(data)
|
|
after(async () => {
|
|
await logAnalytics(result) // executa APÓS resposta enviada
|
|
})
|
|
return result
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Client-Side Data Fetching (MEDIUM-HIGH)
|
|
|
|
```tsx
|
|
// ✅ SWR para deduplicação automática
|
|
const { data, isLoading } = useSWR('/api/user', fetcher)
|
|
|
|
// ✅ Passive event listeners para scroll performance
|
|
element.addEventListener('scroll', handler, { passive: true })
|
|
|
|
// ✅ localStorage versionado com try-catch
|
|
const key = 'userConfig:v2'
|
|
try {
|
|
const data = JSON.parse(localStorage.getItem(key) ?? '{}')
|
|
} catch { /* incognito mode, quota exceeded */ }
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Re-render Optimization (MEDIUM)
|
|
|
|
```tsx
|
|
// ✅ Derived state: calcular durante render, NÃO em useEffect
|
|
function FilteredList({ items, query }: Props) {
|
|
const filtered = items.filter(i => i.name.includes(query)) // derive!
|
|
return <List items={filtered} />
|
|
}
|
|
|
|
// ✅ Functional setState para evitar stale closures
|
|
setItems(curr => [...curr, ...newItems]) // ✅
|
|
// setItems([...items, ...newItems]) // ❌ stale closure
|
|
|
|
// ✅ Lazy state initialization
|
|
const [index] = useState(() => buildSearchIndex(items)) // computa 1x
|
|
|
|
// ✅ useTransition para updates não-urgentes
|
|
const [isPending, startTransition] = useTransition()
|
|
startTransition(() => setSearchResults(results))
|
|
|
|
// ✅ Extrair defaults não-primitivos para module scope
|
|
const EMPTY_ARRAY: string[] = []
|
|
const NOOP = () => {}
|
|
function Component({ items = EMPTY_ARRAY, onClick = NOOP }) { ... }
|
|
|
|
// ✅ Dependências de effect: primitivos, não objectos
|
|
useEffect(() => { fetchUser(userId) }, [userId]) // ✅ primitivo
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Rendering Performance (MEDIUM)
|
|
|
|
```tsx
|
|
// ✅ Hoist static JSX fora do componente
|
|
const skeleton = <div className="skeleton h-4 w-full" /> // module scope
|
|
|
|
// ✅ Conditional rendering explícito (evitar && com falsy)
|
|
{count > 0 ? <Badge>{count}</Badge> : null} // ✅
|
|
// {count && <Badge>{count}</Badge>} // ❌ renderiza "0"
|
|
|
|
// ✅ useTransition em vez de useState manual para loading
|
|
const [isPending, startTransition] = useTransition()
|
|
```
|
|
|
|
---
|
|
|
|
## 7. JavaScript Performance (LOW-MEDIUM)
|
|
|
|
```tsx
|
|
// ✅ Index Maps para lookups repetidos O(1) vs O(n)
|
|
const userMap = new Map(users.map(u => [u.id, u]))
|
|
orders.map(o => ({ ...o, user: userMap.get(o.userId) }))
|
|
|
|
// ✅ Set para membership checks
|
|
const allowedIds = new Set(['a', 'b', 'c'])
|
|
if (allowedIds.has(id)) { ... } // O(1)
|
|
|
|
// ✅ toSorted() para imutabilidade (React-safe)
|
|
const sorted = items.toSorted((a, b) => a.name.localeCompare(b.name))
|
|
// items.sort() // ❌ muta o array original
|
|
|
|
// ✅ Combinar iterações de array
|
|
const { active, expired } = items.reduce((acc, item) => {
|
|
if (item.active) acc.active.push(item)
|
|
else acc.expired.push(item)
|
|
return acc
|
|
}, { active: [], expired: [] })
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Advanced Patterns (LOW)
|
|
|
|
```tsx
|
|
// ✅ Init uma vez, não por mount (Strict Mode remonta)
|
|
let didInit = false
|
|
function App() {
|
|
useEffect(() => {
|
|
if (didInit) return
|
|
didInit = true
|
|
initializeAnalytics()
|
|
}, [])
|
|
}
|
|
|
|
// ✅ useEffectEvent para callbacks estáveis em effects
|
|
const onTick = useEffectEvent((value) => {
|
|
console.log(value) // sempre valor mais recente
|
|
})
|
|
useEffect(() => {
|
|
const id = setInterval(() => onTick(count), 1000)
|
|
return () => clearInterval(id)
|
|
}, []) // sem dependência de count!
|
|
```
|
|
|
|
---
|
|
|
|
## Resumo de Impacto
|
|
|
|
| Prioridade | Categoria | Ganho |
|
|
|---|---|---|
|
|
| CRITICAL | Waterfalls | 2-10x latência |
|
|
| CRITICAL | Bundle Size | 200-800ms load |
|
|
| HIGH | Server-Side | RSC payload, TTI |
|
|
| MEDIUM-HIGH | Client Fetch | Request dedup |
|
|
| MEDIUM | Re-renders | UI responsiveness |
|
|
| MEDIUM | Rendering | Paint performance |
|
|
| LOW-MEDIUM | JS Perf | CPU cycles |
|