Telles Logo
Pular para o conteúdo
Ver todos os artigos
nov 19, 2023
·
views
·
likes
·
6 min

Entrega & Rendering: CDN, Lazy Loading e Estratégias de Render

Como CDN, caching, SSR, SSG, ISR, lazy loading, resource hints, compressão e fontes afetam a performance de entrega do seu frontend.

Performance não é só o que acontece no browser — começa muito antes, na forma como os recursos chegam até o usuário. Como você entrega e renderiza seu conteúdo tem impacto direto no TTFB, no LCP e na experiência percebida.

Carregamento de Recursos

A ordem e forma como os recursos são carregados determina o waterfall de rede — quanto mais paralelo e antecipado, melhor.

HTML
CSS crítico
Fontes
JS defer
Imagens
0ms500ms1s1.5s2s+

Resource hints são dicas para o browser sobre o que buscar antecipadamente.

Ordem recomendada no head:

<!-- 1. preconnect para origens críticas -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 
<!-- 2. preload para recursos críticos da rota atual -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<link rel="preload" href="/fonte.woff2" as="font" type="font/woff2" crossorigin>
 
<!-- 3. prefetch para a próxima navegação provável -->
<link rel="prefetch" href="/pagina-seguinte.html">
  • preload

    Recursos necessários logo na página atual (fontes do hero, CSS crítico, LCP image). Usar com moderação — preloads desnecessários consomem bandwidth

  • preconnect

    Origens de terceiros críticas: CDN, APIs, serviços de fonte. Estabelece DNS + TCP + TLS antecipadamente

  • prefetch

    Recursos necessários na próxima navegação provável (ex: página de produto ao hoverar um card)

  • dns-prefetch

    Fallback de preconnect para domínios menos prioritários. Apenas resolve o DNS

CDN e Caching

🌍CDN — Edge Nodes

Servidores distribuídos geograficamente. O usuário recebe o conteúdo do node mais próximo. Um server no Brasil responde em ~5ms para SP; sem CDN, o mesmo server responde em ~200ms para Tóquio.

💾Cache-Control

Assets com hash no nome: max-age=31536000, immutable. Páginas HTML: stale-while-revalidate=86400. O stale-while-revalidate serve cache imediatamente e revalida em background — zero latência para o usuário.

# Assets com fingerprint — cache para sempre
Cache-Control: public, max-age=31536000, immutable

# Páginas HTML — serve cache, revalida em background
Cache-Control: public, max-age=0, stale-while-revalidate=86400

Estratégias de Rendering

A forma como você renderiza páginas determina o TTFB, o LCP e o custo de infraestrutura. Clique entre as estratégias para ver como cada uma impacta o waterfall de carregamento:

Comparador de estratégias de rendering

CSRClient Side Rendering

HTML vazio + bundle JS. O browser monta o DOM após executar o JS.

❌ LCP Ruim
TTFB
~80ms
HTML útil
~1800ms
JS download
~1200ms
LCP
~3200ms
01s2s3.5s+
LCP alto — conteúdo só aparece após o JS carregar e executar.

Valores simulados em conexão 4G — variam conforme servidor, CDN e tamanho do conteúdo

Static Site Generation — HTML gerado em build time. O servidor entrega HTML completo e pronto, servido direto da CDN.

Melhor para: blogs, documentação, landing pages — conteúdo que não muda frequentemente.

✅ TTFB mínimo, LCP excelente, custo mínimo de infra ❌ Rebuild necessário para atualizar conteúdo

Lazy Loading

🖼️loading="lazy" nativo

Adia o download de imagens e iframes até que estejam próximos da viewport. Sem JavaScript, sem configuração. Nunca use na imagem LCP.

👁️IntersectionObserver

Para componentes React que só devem renderizar quando entram na tela. Evita trabalho desnecessário para conteúdo below the fold.

📜Virtualização de listas

Listas com 100+ itens criam 100+ nós DOM. Virtualização mantém no DOM apenas os itens visíveis — essencial para listas longas.

import { useState, useEffect, useRef } from "react";

function HeavyComponent() {
  return (
    <div style={{ padding: 24, background: "#1e1e2e", borderRadius: 8 }}>
      <h2 style={{ color: "#cba6f7" }}>Componente Carregado! 🎉</h2>
      <p style={{ color: "#cdd6f4" }}>
        Este componente só foi renderizado quando entrou na viewport.
        O IntersectionObserver evitou trabalho desnecessário antes disso.
      </p>
    </div>
  );
}

export default function App() {
  const [visible, setVisible] = useState(false);
  const [loaded, setLoaded] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setVisible(true);
          observer.disconnect();
        }
      },
      { threshold: 0.1 }
    );
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    if (visible) {
      setTimeout(() => setLoaded(true), 800);
    }
  }, [visible]);

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p style={{ color: "#a6e3a1" }}>
        ✅ Componente acima do fold — renderizado imediatamente
      </p>
      <div
        ref={ref}
        style={{
          marginTop: 16,
          padding: 24,
          background: "#313244",
          borderRadius: 8,
          minHeight: 80,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        {!visible && (
          <p style={{ color: "#6c7086" }}>
            👆 Aguardando entrar na viewport...
          </p>
        )}
        {visible && !loaded && (
          <p style={{ color: "#fab387" }}>⏳ Carregando componente...</p>
        )}
        {loaded && <HeavyComponent />}
      </div>
    </div>
  );
}

import { useState, useRef, useMemo } from "react";

const ITEM_HEIGHT = 48;
const VISIBLE_COUNT = 8;

const allItems = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  label: `Item #${i + 1} — dado simulado`,
}));

export default function App() {
  const [scrollTop, setScrollTop] = useState(0);
  const containerRef = useRef(null);

  const startIndex = Math.floor(scrollTop / ITEM_HEIGHT);
  const endIndex = Math.min(startIndex + VISIBLE_COUNT + 1, allItems.length);

  const visibleItems = useMemo(
    () => allItems.slice(startIndex, endIndex),
    [startIndex, endIndex]
  );

  const totalHeight = allItems.length * ITEM_HEIGHT;

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16 }}>
      <p style={{ color: "#a6e3a1", marginBottom: 8 }}>
        ✅ 10.000 itens — apenas {VISIBLE_COUNT + 1} renderizados no DOM
      </p>
      <div
        ref={containerRef}
        onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
        style={{
          height: ITEM_HEIGHT * VISIBLE_COUNT,
          overflowY: "auto",
          position: "relative",
          background: "#1e1e2e",
          borderRadius: 8,
        }}
      >
        <div style={{ height: totalHeight, position: "relative" }}>
          {visibleItems.map((item) => (
            <div
              key={item.id}
              style={{
                position: "absolute",
                top: item.id * ITEM_HEIGHT,
                left: 0,
                right: 0,
                height: ITEM_HEIGHT,
                display: "flex",
                alignItems: "center",
                padding: "0 16px",
                borderBottom: "1px solid #313244",
                color: "#cdd6f4",
                fontSize: 14,
              }}
            >
              {item.label}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Fontes

Controla o comportamento durante o carregamento da fonte:

  • block — texto invisível até a fonte carregar (FOIT)

    Bloqueia a leitura completamente enquanto a fonte baixa. Evite em quase todos os casos.

  • swap — fallback imediato, troca quando carregar

    Melhor para legibilidade imediata, mas pode causar CLS se as métricas de fonte diferirem muito.

  • fallback — 100ms invisível, depois fallback; troca em 3s

    Compromisso entre FOIT e FOUT. Se a fonte não carregar em 3s, o fallback permanece.

  • optional — 100ms invisível; sem troca após isso

    Melhor para CLS. O browser decide se usa a fonte baseado na velocidade da conexão. Recomendado para body text.

Para a fonte principal do corpo: optional. Para títulos críticos above the fold: swap com size-adjust no fallback.


No próximo artigo, focamos no JavaScript — como analisar seu bundle, implementar code splitting e usar React.lazy + Suspense de forma eficaz.

As minhas notas

Selecione um trecho e use Destacar no menu (somente neste aparelho).

Artigos relacionados