Performance Web

Core Web Vitals — As Métricas que Definem Ranking

O Google usa três métricas coletadas de usuários reais (CrUX — Chrome User Experience Report) para determinar a pontuação de experiência de página. Essas métricas vêm do percentil 75 (p75) dos seus usuários reais, não de testes sintéticos.

LCP (Largest Contentful Paint)
  Bom: < 2.5s | Precisa melhorar: 2.5s–4.0s | Ruim: > 4.0s

  O que mede: tempo até o maior elemento visível no viewport ser renderizado.
  Elementos candidatos: <img>, <video poster>, background-image via url(),
                        blocos de texto (<h1>, <p>) se forem o maior elemento.

  Como é calculado:
    1. O browser identifica o maior elemento visível a cada frame.
    2. Se o maior elemento muda (ex: imagem carrega depois do texto), o LCP é atualizado.
    3. O LCP para de ser reportado após a primeira interação do usuário (click, tap, keydown).
    4. O valor final é o timestamp do último candidato a LCP antes da interação.

INP (Interaction to Next Paint) — substituiu o FID em março de 2024
  Bom: < 200ms | Precisa melhorar: 200ms–500ms | Ruim: > 500ms

  O que mede: latência de TODAS as interações durante a sessão (click, tap, keydown).
  Diferença do FID: o FID media apenas o delay da PRIMEIRA interação.
  O INP pega o p98 de todas as interações (ignora outliers extremos).

  Decomposição de uma interação:
    Input Delay:      tempo entre o evento do SO e o início do event handler
    Processing Time:  tempo executando os event handlers (seu JS)
    Presentation Delay: tempo entre o handler terminar e o próximo frame ser pintado

  INP = Input Delay + Processing Time + Presentation Delay

CLS (Cumulative Layout Shift)
  Bom: < 0.1 | Precisa melhorar: 0.1–0.25 | Ruim: > 0.25

  O que mede: instabilidade visual — quanto os elementos "pulam" na tela.
  Fórmula: Impact Fraction × Distance Fraction
    - Impact Fraction: % do viewport afetada pelo shift
    - Distance Fraction: distância que o elemento se moveu / altura do viewport

  Session Windows: o CLS agrupa shifts em janelas de até 5s com gap máximo de 1s.
  O valor reportado é a maior session window, não a soma total.

Critical Rendering Path — O Caminho até o Primeiro Pixel

O browser segue uma sequência determinística para transformar HTML/CSS/JS em pixels:

1. HTML parsing → DOM tree
2. CSS parsing → CSSOM tree
3. DOM + CSSOM → Render tree
4. Layout (reflow) → geometria dos elementos
5. Paint → pixels nas layers
6. Composite → GPU compõe as layers no frame final

Recursos críticos são aqueles que bloqueiam a renderização inicial:

<!-- CSS é render-blocking por padrão -->
<link rel="stylesheet" href="/styles.css" />

<!-- JS é parser-blocking por padrão (bloqueia construção do DOM) -->
<script src="/app.js"></script>

<!-- async: baixa em paralelo, executa assim que baixou (não garante ordem) -->
<script async src="/analytics.js"></script>

<!-- defer: baixa em paralelo, executa após o DOM estar pronto (garante ordem) -->
<script defer src="/app.js"></script>

<!-- Estratégia: mover CSS crítico inline e carregar o resto async -->
<style>
  /* CSS crítico inline — apenas o above-the-fold */
  .hero { display: flex; min-height: 100vh; }
  .nav { height: 64px; }
</style>

<!-- CSS não-crítico carregado de forma assíncrona -->
<link rel="preload" href="/styles.css" as="style"
      onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/styles.css" /></noscript>

Otimizando o Critical Path Length

Critical Path Length = número de roundtrips sequenciais necessários para renderizar.

Exemplo ruim:
  HTML (1 RT) → CSS (1 RT) → @import em CSS (1 RT) → font (1 RT) = 4 roundtrips

Exemplo otimizado:
  HTML com CSS inline (1 RT) → tudo em paralelo com preload = 1-2 roundtrips

Técnicas para reduzir:
  1. Inline CSS crítico (above the fold)
  2. Eliminar @import em CSS (usar <link> que paraleliza)
  3. Preload de recursos críticos
  4. Usar HTTP/2 ou HTTP/3 (multiplexação, sem head-of-line blocking)
  5. Comprimir com Brotli (melhor que gzip para texto, ~15-20% menor)

JavaScript Performance — Code Splitting e Tree Shaking

Dynamic import() — Divisão de Código sob Demanda

// Bundlers (webpack, Rollup, esbuild) criam chunks separados para cada import()
// O chunk só é baixado quando a função é chamada.

// Route-based splitting — o mais impactante
const routes = {
  '/':        () => import('./pages/Home'),
  '/admin':   () => import('./pages/Admin'),     // Só baixa se acessar /admin
  '/reports': () => import('./pages/Reports'),   // Chunk separado
};

async function navigate(path) {
  const loader = routes[path];
  if (!loader) return render404();

  const { default: Page } = await loader();
  renderPage(Page);
}

// React.lazy — wrapper para componentes com code splitting
const AdminPanel = React.lazy(() => import('./AdminPanel'));
// O webpack gera: AdminPanel.[contenthash].js como chunk separado

// Preload de rota no hover — carrega antes do click
function NavLink({ to, children }) {
  const preload = () => routes[to]?.(); // Dispara o import() no hover

  return (
    <a
      href={to}
      onMouseEnter={preload}  // ~200-400ms de vantagem antes do click
      onFocus={preload}
      onClick={(e) => { e.preventDefault(); navigate(to); }}
    >
      {children}
    </a>
  );
}

Tree Shaking — Eliminação de Código Morto

// Tree shaking funciona APENAS com ES modules (import/export estáticos)
// CommonJS (require) NÃO é tree-shakeable — o bundler não sabe o que é usado

// RUIM — importa tudo, bundler não consegue eliminar nada
import _ from 'lodash';           // ~70kB no bundle
_.get(obj, 'a.b.c');

// BOM — importa apenas a função usada
import get from 'lodash/get';     // ~2kB no bundle

// MELHOR — use lodash-es (ES modules nativos, tree-shakeable)
import { get } from 'lodash-es';  // Bundler inclui apenas o get

// Analisar o bundle para encontrar surpresas:
// npx webpack-bundle-analyzer stats.json
// ou: npx vite-bundle-visualizer

// sideEffects no package.json — diz ao bundler que módulos são puros
// package.json:
{
  "sideEffects": false  // Nenhum módulo tem side effects → tree shaking agressivo
}
// Ou lista específica:
{
  "sideEffects": ["./src/polyfills.js", "*.css"]
}

Loading Strategies — Resource Hints

<!-- dns-prefetch: resolve DNS de origens terceiras (barato, ~0 risco) -->
<link rel="dns-prefetch" href="https://api.example.com" />

<!-- preconnect: DNS + TCP + TLS handshake antecipado (use para origens críticas) -->
<!-- CUIDADO: cada preconnect mantém uma conexão aberta — use no máximo 2-4 -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://cdn.example.com" crossorigin />

<!-- preload: baixa o recurso com alta prioridade AGORA (mesmo parser ainda não viu) -->
<!-- Use para: hero image, font crítica, CSS crítico, JS do critical path -->
<link rel="preload" href="/fonts/Inter-var.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/hero.avif" as="image" />

<!-- prefetch: baixa com prioridade BAIXA, para navegação futura -->
<!-- Use para: próxima página provável, recursos da rota seguinte -->
<link rel="prefetch" href="/admin/dashboard.js" />

<!-- modulepreload: como preload mas para ES modules (faz parse + compile também) -->
<link rel="modulepreload" href="/src/app.js" />

<!-- fetchpriority: controla prioridade dentro da mesma categoria -->
<img src="/hero.avif" fetchpriority="high" />       <!-- LCP image -->
<img src="/carousel-3.jpg" fetchpriority="low" />    <!-- Below the fold -->

<!-- Fetch priority para scripts -->
<script src="/critical.js" fetchpriority="high"></script>
<script src="/analytics.js" fetchpriority="low"></script>

Prioridades Internas do Browser (Chrome)

Recurso                    | Prioridade padrão | Notas
---------------------------|-------------------|----------------------------------
CSS (head)                 | Highest           | Render-blocking
Script (head, sync)        | High              | Parser-blocking
Script (async)             | Low → High        | Low durante download, High ao executar
Script (defer)             | Low               | Executa após DOMContentLoaded
Imagem (viewport)          | High              | Se visível no viewport inicial
Imagem (below fold)        | Low               | Lazy por padrão no Chrome 114+
Font (preloaded)           | High              | Precisa de crossorigin
Font (via CSS)             | High              | Mas só descoberta após CSSOM
Fetch/XHR                  | High              | API calls

Image Optimization — O Maior Impacto no LCP

<!-- Formato moderno com fallback -->
<picture>
  <!-- AVIF: ~50% menor que JPEG, suporte em Chrome/Firefox/Safari 16.4+ -->
  <source type="image/avif" srcset="/hero.avif" />
  <!-- WebP: ~25-35% menor que JPEG, suporte universal -->
  <source type="image/webp" srcset="/hero.webp" />
  <!-- Fallback JPEG -->
  <img src="/hero.jpg" alt="Hero" width="1200" height="600"
       loading="eager" fetchpriority="high" decoding="async" />
</picture>

<!-- Responsive images com srcset e sizes -->
<img
  srcset="
    /product-400.avif 400w,
    /product-800.avif 800w,
    /product-1200.avif 1200w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) 50vw,
    33vw
  "
  src="/product-800.avif"
  alt="Produto"
  width="800" height="600"
  loading="lazy"
  decoding="async"
/>
<!-- width/height são ESSENCIAIS: o browser calcula o aspect-ratio antes de carregar,
     prevenindo CLS. CSS: img { max-width: 100%; height: auto; } -->

LQIP — Low Quality Image Placeholder

// Gerar blur hash no build time (ex: com sharp ou blurhash)
// O placeholder inline (~200-400 bytes base64) aparece instantaneamente.

function OptimizedImage({ src, blurhash, alt, width, height }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <div style={{ position: 'relative', aspectRatio: `${width}/${height}` }}>
      {/* Placeholder inline — sem request extra */}
      {!loaded && (
        <img
          src={`data:image/svg+xml;base64,${generateBlurSVG(blurhash, width, height)}`}
          alt=""
          aria-hidden="true"
          style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}
        />
      )}
      <img
        src={src}
        alt={alt}
        width={width}
        height={height}
        loading="lazy"
        decoding="async"
        onLoad={() => setLoaded(true)}
        style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
      />
    </div>
  );
}

// CDN image transforms (Cloudflare, Imgix, Cloudinary):
// https://cdn.example.com/image.jpg?w=800&h=600&fit=cover&format=auto&quality=75
// format=auto → CDN retorna AVIF/WebP baseado no Accept header do browser

Font Optimization — Eliminar FOIT e FOUT

/* font-display controla o comportamento durante o carregamento */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-var.woff2') format('woff2-variations');
  font-weight: 100 900;        /* Variable font — um arquivo para todos os pesos */
  font-display: swap;          /* Mostra fallback imediatamente, troca quando carrega */
  /* font-display: optional;   Não mostra a font se não carregou em ~100ms
                                Melhor para performance, pior para brand consistency */
  unicode-range: U+0000-00FF;  /* Subsetting: só caracteres latin básicos */
}

/* Preload da font crítica (no <head>) */
/* <link rel="preload" href="/fonts/Inter-var.woff2" as="font"
         type="font/woff2" crossorigin /> */

/* Metric override — reduz CLS ao trocar fallback pela font custom */
@font-face {
  font-family: 'Inter-fallback';
  src: local('Arial');
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
  size-adjust: 107%;
}

body {
  font-family: 'Inter', 'Inter-fallback', system-ui, sans-serif;
}
Variable fonts vs Static fonts:
  Inter Regular + Bold + Italic + Bold Italic = 4 arquivos × ~90kB = ~360kB
  Inter Variable = 1 arquivo × ~290kB (suporta qualquer peso de 100-900)

  Regra: se usa 3+ variações, variable font compensa.

Subsetting com pyftsubset (fonttools):
  $ pyftsubset Inter.woff2 \
      --output-file=Inter-latin.woff2 \
      --flavor=woff2 \
      --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+2000-206F"
  # Redução típica: 290kB → 45kB

HTTP Caching — Cache Headers e Estratégias

# Cache imutável (assets com hash no nome: app.a1b2c3.js)
Cache-Control: public, max-age=31536000, immutable
# O browser NUNCA revalida — confia no hash do filename.

# Cache com revalidação (HTML, API responses)
Cache-Control: public, max-age=0, must-revalidate
ETag: "abc123"
# Browser sempre pergunta ao servidor: "mudou desde ETag abc123?"
# Se não mudou → 304 Not Modified (sem body, ~200 bytes vs megabytes)

# stale-while-revalidate (melhor UX)
Cache-Control: public, max-age=60, stale-while-revalidate=3600
# Serve do cache instantaneamente (mesmo stale), revalida em background.
# Próximo request pega a versão atualizada.

# Vary header — cache por variante
Vary: Accept-Encoding, Accept
# CDN cacheia versões separadas para br/gzip/identity e avif/webp/jpeg

# Padrão para SPAs:
#   index.html         → no-cache (sempre revalida)
#   *.js, *.css (hash) → immutable, 1 ano
#   /api/*              → no-store (nunca cacheia dados sensíveis)

Service Worker — Cache Programático

// sw.js — estratégia Cache-First para assets, Network-First para API
const CACHE_NAME = 'v1';
const STATIC_ASSETS = ['/app.js', '/styles.css', '/offline.html'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
  );
  self.skipWaiting(); // Ativa imediatamente sem esperar abas fecharem
});

self.addEventListener('activate', (event) => {
  // Limpa caches antigos
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
    )
  );
  self.clients.claim(); // Controla todas as abas imediatamente
});

self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // API: Network-First com fallback para cache
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(
      fetch(request)
        .then((response) => {
          const clone = response.clone();
          caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
          return response;
        })
        .catch(() => caches.match(request))
    );
    return;
  }

  // Assets: Cache-First (stale-while-revalidate manual)
  event.respondWith(
    caches.match(request).then((cached) => {
      const fetchPromise = fetch(request).then((response) => {
        caches.open(CACHE_NAME).then((cache) => cache.put(request, response.clone()));
        return response;
      });
      return cached || fetchPromise; // Retorna cache se existe, senão espera network
    })
  );
});

Runtime Performance — Main Thread e Concorrência

// requestAnimationFrame — sincroniza com o refresh rate do monitor (~16.6ms a 60Hz)
// Use para: animações, atualizações visuais contínuas
function animateScroll(targetY, duration = 300) {
  const startY = window.scrollY;
  const startTime = performance.now();

  function tick(currentTime) {
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);
    // Easing function: ease-out cubic
    const eased = 1 - Math.pow(1 - progress, 3);

    window.scrollTo(0, startY + (targetY - startY) * eased);

    if (progress < 1) {
      requestAnimationFrame(tick);
    }
  }

  requestAnimationFrame(tick);
}

// requestIdleCallback — executa quando o browser está ocioso
// Use para: analytics, pre-fetching, trabalho não-urgente
// CUIDADO: Safari não suporta nativamente (precisa de polyfill)
function sendAnalytics(data) {
  const deadline = { timeout: 2000 }; // Garante execução em até 2s

  requestIdleCallback((idleDeadline) => {
    // idleDeadline.timeRemaining() → ms restantes no idle period (~50ms max)
    if (idleDeadline.timeRemaining() > 10 || idleDeadline.didTimeout) {
      navigator.sendBeacon('/analytics', JSON.stringify(data));
    }
  }, deadline);
}

// Web Workers — processamento pesado fora da main thread
// worker.js
self.onmessage = ({ data }) => {
  // Roda em thread separada — NÃO bloqueia a UI
  const result = heavyComputation(data); // Ex: parse de CSV grande, crypto, sort
  self.postMessage(result);
};

// main.js
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
worker.postMessage(largeDataset);
worker.onmessage = ({ data }) => {
  updateUI(data); // Volta para main thread com o resultado
};

// Transferable objects — transfere ownership sem copiar (zero-copy)
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(buffer, [buffer]);
// Agora buffer.byteLength === 0 na main thread (foi transferido, não copiado)

Yielding para a Main Thread — Evitando Long Tasks

// Long Task = qualquer task > 50ms na main thread (bloqueia INP)
// Técnica: quebrar trabalho pesado com yield points

// Usando scheduler.yield() (Chrome 115+)
async function processLargeList(items) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);

    // A cada 10 items, devolve controle para o browser
    if (i % 10 === 0 && i > 0) {
      await scheduler.yield(); // Mantém prioridade na fila de tasks
    }
  }
}

// Fallback para browsers sem scheduler.yield()
function yieldToMain() {
  return new Promise((resolve) => {
    // setTimeout(fn, 0) coloca na task queue → browser pode processar eventos entre tasks
    setTimeout(resolve, 0);
  });
}

// isInputPending — verifica se tem input pendente antes de continuar
async function processWithInputCheck(items) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);

    if (navigator.scheduling?.isInputPending?.()) {
      await yieldToMain(); // Yield imediato se usuário está interagindo
    }
  }
}

Memory — Detectar e Prevenir Leaks

// Causas comuns de memory leak em SPAs:
// 1. Event listeners não removidos
// 2. Closures que capturam referências grandes
// 3. Timers (setInterval) não limpos
// 4. Referências em caches/maps que crescem indefinidamente
// 5. Detached DOM nodes (nó removido do DOM mas referenciado em JS)

// AbortController — cleanup unificado para listeners e fetch
function setupFeature(element) {
  const controller = new AbortController();
  const { signal } = controller;

  element.addEventListener('click', handleClick, { signal });
  element.addEventListener('mousemove', handleMove, { signal });
  window.addEventListener('resize', handleResize, { signal });

  // Um único abort limpa TODOS os listeners
  return () => controller.abort();
}

// WeakRef — referência que não impede garbage collection
const cache = new Map();

function getCachedOrCompute(key, computeFn) {
  const ref = cache.get(key);
  if (ref) {
    const value = ref.deref(); // Retorna undefined se o GC já coletou
    if (value !== undefined) return value;
  }

  const value = computeFn();
  cache.set(key, new WeakRef(value));
  return value;
}

// FinalizationRegistry — callback quando objeto é coletado pelo GC
const registry = new FinalizationRegistry((heldValue) => {
  cache.delete(heldValue); // Limpa a entrada do cache quando o valor é GC'd
  console.debug(`Cache entry "${heldValue}" was garbage collected`);
});

function cacheWithAutoCleanup(key, value) {
  cache.set(key, new WeakRef(value));
  registry.register(value, key); // Quando value for GC'd, executa o callback com key
}

// DevTools: detectar leaks
// 1. Memory tab → Take Heap Snapshot
// 2. Performar ação suspeita (navegar, abrir/fechar modal)
// 3. Take outro snapshot
// 4. Comparison view → ver objetos que só existem no snapshot 2
// 5. Procurar por: Detached HTMLElement, EventListener, Closure

Métricas — Como Cada Uma é Calculada

Métrica   | Tipo        | O que mede                      | Como é calculado
----------|-------------|----------------------------------|------------------------------------------
TTFB      | Server      | Tempo até o primeiro byte        | navigationStart → responseStart
FCP       | Loading     | Primeiro conteúdo pintado        | Primeiro texto/imagem renderizado
LCP       | Loading     | Maior conteúdo pintado           | Último LCP candidate antes da interação
FID       | Interação   | Delay da primeira interação      | Tempo entre event e handler start (deprecated)
INP       | Interação   | Responsividade geral             | p98 de todas as interações na sessão
TBT       | Interação   | Tempo total bloqueando main      | Soma de (duração - 50ms) para cada Long Task
TTI       | Interação   | Tempo até interatividade         | FCP + 5s quiet window (sem Long Tasks + ≤2 GETs)
CLS       | Visual      | Instabilidade do layout          | Max session window de layout shifts

Notas importantes:
  - FID foi substituído pelo INP em março de 2024.
  - TTI é instável e NÃO faz parte dos Core Web Vitals.
  - TBT é a melhor proxy lab para INP (correlação forte).
  - Use TBT no Lighthouse, INP no CrUX (dados reais).

Tools — Como e Quando Usar Cada Uma

Lighthouse (lab data):
  - Chrome DevTools → Lighthouse tab
  - CLI: npx lighthouse https://example.com --output=json --output=html
  - Roda em condições simuladas (4x CPU slowdown, 3G/4G throttling)
  - Bom para: debugging, CI/CD gates, comparações A/B

WebPageTest (lab + real):
  - webpagetest.org — teste de múltiplas localizações e browsers
  - Filmstrip view: frame a frame do carregamento
  - Waterfall: identifica recursos bloqueantes
  - Bom para: análise detalhada de terceiros, comparação de CDNs

Chrome DevTools Performance tab (lab):
  - Flame chart: visualiza call stack ao longo do tempo
  - Long Tasks marcados em vermelho (> 50ms)
  - Layout Shifts marcados na timeline
  - Bom para: debugging de jank, identificar Long Tasks específicas

CrUX (campo — dados reais):
  - BigQuery: `chrome-ux-report.all.YYYYMM`
  - API: CrUX API com origin ou URL-level data
  - PageSpeed Insights: combina Lighthouse + CrUX
  - Bom para: dados reais de produção, decisões de ranking

Framework-Specific — React Performance

// React.memo — evita re-render quando props não mudam (shallow compare)
const ExpensiveList = React.memo(function ExpensiveList({ items, onSelect }) {
  return items.map((item) => (
    <ListItem key={item.id} item={item} onSelect={onSelect} />
  ));
});
// CUIDADO: se onSelect é () => {} inline, SEMPRE é nova referência → memo inútil
// Solução: useCallback no parent

// useMemo — memoiza computação derivada
function Dashboard({ transactions }) {
  const summary = useMemo(() => {
    // Só recalcula quando transactions muda
    return transactions.reduce((acc, tx) => ({
      total: acc.total + tx.amount,
      count: acc.count + 1,
      average: (acc.total + tx.amount) / (acc.count + 1),
    }), { total: 0, count: 0, average: 0 });
  }, [transactions]);

  return <SummaryCard data={summary} />;
}

// Virtualização — renderiza apenas o visível (react-window)
import { FixedSizeList } from 'react-window';

function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>{items[index].name}</div>
  );

  // 10.000 items → ~15 DOM nodes (apenas os visíveis no viewport)
  return (
    <FixedSizeList
      height={600}
      width="100%"
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

// Streaming SSR — envia HTML progressivamente (React 18+)
// server.js
import { renderToPipeableStream } from 'react-dom/server';

app.get('/', (req, res) => {
  const { pipe } = renderToPipeableStream(<App />, {
    bootstrapScripts: ['/client.js'],
    onShellReady() {
      res.setHeader('Content-Type', 'text/html');
      pipe(res);
      // Shell (layout + Suspense fallbacks) é enviado imediatamente.
      // Conteúdo dentro de <Suspense> é streamed quando resolve.
    },
    onError(error) {
      console.error(error);
      res.statusCode = 500;
    },
  });
});

// React Server Components (RSC) — componentes que rodam APENAS no server
// Benefícios:
//   1. Zero JS enviado ao client para server components
//   2. Acesso direto a banco/filesystem no componente
//   3. Dependências pesadas (marked, highlight.js) ficam no server

// app/page.tsx (Next.js — server component por padrão)
import { db } from '@/lib/db';
import { marked } from 'marked'; // NÃO vai para o bundle do client

export default async function BlogPost({ params }) {
  const post = await db.posts.findUnique({ where: { slug: params.slug } });
  const html = marked(post.content); // Parse de markdown no server

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: html }} />
      {/* Componente interativo → precisa de "use client" */}
      <LikeButton postId={post.id} />
    </article>
  );
}

Checklist de Performance para Produção

Rede e Carregamento:
  □ Brotli habilitado no servidor/CDN
  □ HTTP/2 ou HTTP/3 habilitado
  □ CSS crítico inline, resto async
  □ JS com defer ou async (nenhum script sync no <head>)
  □ Resource hints: preconnect (API, CDN), preload (font, hero image)
  □ Code splitting por rota + preload no hover

Assets:
  □ Imagens em AVIF/WebP com fallback
  □ srcset + sizes em todas as imagens responsivas
  □ width/height em todas as <img> (previne CLS)
  □ lazy loading em imagens abaixo do fold
  □ fetchpriority="high" na LCP image
  □ Fonts: woff2, preload, font-display: swap, subsetting

Cache:
  □ Assets com content hash: Cache-Control: immutable, 1 ano
  □ HTML/API: max-age=0, must-revalidate + ETag
  □ Service worker para offline e stale-while-revalidate

Runtime:
  □ Nenhuma Long Task > 50ms no critical path
  □ Virtualização para listas > 100 items
  □ Web Workers para processamento pesado
  □ Cleanup de listeners, timers e subscriptions

Monitoramento:
  □ RUM (Real User Monitoring) com web-vitals lib
  □ Performance budget no CI (Lighthouse score ≥ 90)
  □ Alertas para regressão de Core Web Vitals (CrUX)

APIs Modernas de Performance

INP (Interaction to Next Paint) — Substituiu FID

FID (First Input Delay) media apenas o delay da primeira interação. INP mede a responsividade de todas as interações durante toda a sessão — o valor reportado é o p98 (pior caso representativo).

Lifecycle de uma interação:

│← Input Delay →│← Processing →│← Presentation Delay →│
│ (event queue)  │ (handler)    │ (paint + composite)   │
├────────────────┼──────────────┼───────────────────────┤
[user clicks]    [handler runs]  [browser paints]       [frame displayed]

INP = Input Delay + Processing Time + Presentation Delay

Thresholds:
  Good:           ≤ 200ms
  Needs Improve:  200-500ms
  Poor:           > 500ms

Como otimizar INP:

  1. Reduzir Input Delay: evitar Long Tasks que bloqueiam a main thread (yield com scheduler.yield())
  2. Reduzir Processing Time: mover trabalho pesado para Web Workers, usar requestAnimationFrame para visual updates
  3. Reduzir Presentation Delay: evitar layout thrashing (leituras e escritas DOM intercaladas), minimizar DOM size
// Medir INP programaticamente
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // entry.duration = total interaction time (input delay + processing + presentation)
    // entry.interactionId = agrupador de eventos relacionados (pointerdown + pointerup + click)
    console.log(`INP candidate: ${entry.duration}ms`, entry.name, entry.target);
  }
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });

View Transitions API

Animações nativas de transição entre páginas, sem JavaScript animation libraries.

// MPA (Multi-Page Applications) — precisa de opt-in via meta tag
// <meta name="view-transition" content="same-origin" />

// SPA — usando document.startViewTransition()
async function navigateTo(url) {
  const response = await fetch(url);
  const html = await response.text();

  // O browser captura snapshot da página atual,
  // aplica o novo DOM, e anima a transição entre os dois
  document.startViewTransition(() => {
    document.querySelector('main').innerHTML = html;
  });
}

// CSS para customizar a transição
// ::view-transition-old(root) { animation: fade-out 0.3s ease; }
// ::view-transition-new(root) { animation: fade-in 0.3s ease; }

// Morph animation: mesmo view-transition-name em ambas as páginas
// .hero-image { view-transition-name: hero; }
// O browser automaticamente anima a posição, tamanho e opacidade

Integração com frameworks: Astro suporta View Transitions nativamente. Next.js usa next/navigation com experimental view transitions.

Speculation Rules API

Pre-rendering de páginas antes do usuário clicar — substituindo <link rel="prefetch"> com controle mais granular.

<script type="speculationrules">
{
  "prerender": [
    {
      "where": { "href_matches": "/products/*" },
      "eagerness": "moderate"
    }
  ],
  "prefetch": [
    {
      "where": { "href_matches": "/blog/*" },
      "eagerness": "conservative"
    }
  ]
}
</script>

<!--
eagerness levels:
  "immediate": pre-render logo quando a regra é adicionada
  "eager": pre-render assim que um link matching é adicionado ao DOM
  "moderate": pre-render após 200ms de hover (bom default!)
  "conservative": pre-render apenas ao pointerdown (mousedown/touchstart)
-->

Benefício: navegação instantânea (abaixo de 50ms) pois a página já está renderizada em background. Chrome DevTools → Application → Preloading mostra o status.

Long Animation Frames (LoAF)

Nova API que substitui Long Tasks (50ms threshold) com informações mais detalhadas:

// LoAF: Long Animation Frames (Chrome 123+)
// Detecta frames que levaram > 50ms para processar
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // entry.duration: duração total do frame
    // entry.blockingDuration: tempo que bloqueou input
    // entry.scripts: quais scripts contribuíram (com URL + function name!)
    console.log(`LoAF: ${entry.duration}ms, blocking: ${entry.blockingDuration}ms`);
    for (const script of entry.scripts) {
      console.log(`  Script: ${script.sourceURL}:${script.sourceFunctionName} (${script.duration}ms)`);
    }
  }
}).observe({ type: 'long-animation-frame', buffered: true });

Por que LoAF > Long Tasks: Long Tasks só reporta duração. LoAF reporta quais scripts causaram o bloqueio, rendering time, e blocking duration separadamente. Isso facilita enormemente o debugging de INP.


Referencias e Fontes

  • Web.dev — Core Web Vitalshttps://web.dev/vitals — Guias oficiais do Google sobre as metricas essenciais de performance web (LCP, FID/INP, CLS) e como otimiza-las
  • “High Performance Browser Networking” — Ilya Grigorik — Livro referencia sobre protocolos de rede, otimizacao de conexoes, caching e tudo que afeta a performance de aplicacoes web
  • Lighthouse Documentationhttps://developer.chrome.com/docs/lighthouse — Documentacao da ferramenta de auditoria automatizada do Chrome para performance, acessibilidade e boas praticas
  • MDN Performance APIhttps://developer.mozilla.org/en-US/docs/Web/API/Performance_API — Referencia completa das APIs de performance do navegador para medicao precisa de metricas e timing de recursos