Pular para o conteúdo
~/.primo-academy.sh
☰ Aulas · Performance Web: Core Web Vitals, bundle, imagens, CDN e CI · 0/7
Recomendado: essencial

Imagens e fontes: AVIF/WebP, srcset, font-display, preload

9 min de leitura

fonte

Em 2026, imagens representam ~50% do peso total de uma pagina media (HTTP Archive). Fontes custom representam outros ~5%. Esses 2 tipos de recurso sao os que mais afetam LCP (imagens) e CLS (font swap) - 2 das 3 Core Web Vitals. Ignorar otimizacao de imagens/fonts e' literalmente entregar CWV ruim por design.

Se voce entende AVIF vs WebP, <picture> com srcset/sizes, font-display: swap, e o pattern de preload pra LCP, voce consegue reduzir 70-90% do peso visual de uma landing page sem perder qualidade.

O essencial 🟢

Os 3 formatos de imagem modernos (e quando usar cada). Cada formato tem um trade-off de compressao, suporte de browser, e caso de uso:

FormatoCompressao vs JPEG/PNGSuporte 2026Uso ideal
JPEGBaseline (1x)100%Fotos com fall-through
PNGLossless100%Icones com transparencia (raro em 2026)
WebP~30% menor que JPEG97%Default moderno
AVIF~50% menor que JPEG92%Imagens de alta qualidade, hero
SVGVetorial100%Logos, icones, ilustracoes simples

A regra pratica em 2026: AVIF > WebP > JPEG/PNG. Use <picture> pra fornecer multiplos formatos (browser pega o melhor que suporta). Fallback JPEG pra browsers antigos.

<picture> com multiplos formatos - o pattern canonico.

<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero" width="1200" height="600">
</picture>

O browser:

  1. Tenta AVIF (suporta? usa).
  2. Se nao, tenta WebP.
  3. Se nao, usa JPEG (fallback).

Sempre inclua <img> como fallback com width/height explicitos (evita CLS). O <picture> envolve o <img>, nao o substitui.

srcset e sizes - servir imagem do tamanho certo. Uma imagem de 4000px num celular de 375px = 10x mais pixels que o necessario = 10x mais download. srcset destrava oferecer multiplas versoes da mesma imagem, em tamanhos diferentes, e o browser pega a melhor pro device:

<img
  src="hero-800.jpg"
  srcset="
    hero-400.jpg 400w,
    hero-800.jpg 800w,
    hero-1200.jpg 1200w,
    hero-2000.jpg 2000w
  "
  sizes="
    (max-width: 600px) 100vw,
    (max-width: 1200px) 80vw,
    1200px
  "
  alt="Hero"
  width="1200"
  height="600"
>

O sizes diz ao browser quanto espaco a imagem ocupa no layout (CSS pixels). O browser multiplica pelo device-pixel-ratio pra escolher a versao ideal. Em celular 3x DPR, sizes="100vw" (375px CSS) = 1125px fisicos = escolhe hero-1200.jpg.

Combinando AVIF + srcset = o estado da arte. Voce gera as imagens em AVIF e WebP, em 4-5 tamanhos cada, e serve tudo via <picture> + srcset:

<picture>
  <source
    type="image/avif"
    srcset="
      hero-400.avif 400w,
      hero-800.avif 800w,
      hero-1200.avif 1200w,
      hero-2000.avif 2000w
    "
    sizes="(max-width: 600px) 100vw, 1200px"
  >
  <source
    type="image/webp"
    srcset="
      hero-400.webp 400w,
      hero-800.webp 800w,
      hero-1200.webp 1200w,
      hero-2000.webp 2000w
    "
    sizes="(max-width: 600px) 100vw, 1200px"
  >
  <img
    src="hero-1200.jpg"
    alt="Hero"
    width="1200"
    height="600"
  >
</picture>

E' verboso, mas a diferenca e' brutal: hero JPEG 2000px = 800KB. Hero AVIF 800px = 50KB. 16x menor.

Ferramentas pra gerar variantes. Em 2026:

  • CLI: sharp (Node, mais rapido), cwebp, avifenc (libaom).
  • Build plugin: vite-imagetools (Vite faz em build time).
  • CDN com transformacao: Cloudflare Polish, Cloudinary, ImageKit, imgix. Voce serve 1 imagem original, a CDN gera variantes on-demand com query string (?w=800&format=avif).

<img loading="lazy"> - lazy load nativo. Imagens abaixo da dobra nao precisam baixar ate o usuario rolar ate elas. loading="lazy" faz isso:

<!-- Acima da dobra: SEM lazy (LCP critico) -->
<img src="hero.avif" alt="Hero" width="1200" height="600">

<!-- Abaixo da dobra: COM lazy -->
<img src="product-1.avif" alt="Produto 1" loading="lazy" width="400" height="400">

NAO use lazy na imagem do LCP (hero) - isso atrasa o LCP. So use em imagens abaixo da dobra. O browser tem heuristica interna: se a imagem esta visivel no load, baixa mesmo com loading="lazy". Mas explicitar loading="eager" no hero e claro e seguro.

<img decoding="async"> - decode nao bloqueia. Decode de imagem (JPG/PNG parsing) e' CPU-intensive. Em 2026, mobile mid-range pode levar 100-500ms pra decodar uma imagem grande. decoding="async" diz ao browser "faz isso em background, nao bloqueia o thread principal":

<img src="hero.avif" alt="Hero" decoding="async" width="1200" height="600">

Quase sempre use async. Excecao: a imagem precisa estar pronta antes de uma animacao JS (raro).

<img fetchpriority="high"> - LCP boost. Pra imagens criticas pro LCP, diga ao browser pra priorizar:

<img
  src="hero.avif"
  alt="Hero"
  fetchpriority="high"
  width="1200"
  height="600"
>

Em conjunto com <link rel="preload">:

<head>
  <link rel="preload" as="image" href="hero.avif" fetchpriority="high">
</head>
<body>
  <img src="hero.avif" alt="Hero" fetchpriority="high" width="1200" height="600">
</body>

O preload + fetchpriority="high" diz "baixa o AVIF do hero AGORA, com maxima prioridade". O browser antecipa. Resultado: LCP tipicamente cai 200-500ms.

width e height sempre - pra evitar CLS. Imagens sem dimensao definida causam CLS (a tela "pula" quando carregam). Sempre, sem excecao, defina:

<!-- BOM: width e height (ou aspect-ratio em CSS) -->
<img src="x.avif" width="800" height="400" alt="...">

<!-- RUIM: sem dimensao - CLS quando carregar -->
<img src="x.avif" alt="...">

O browser usa width/height pra calcular o aspect-ratio e reservar o espaco antes da imagem carregar. Sem isso, o espaco e' 0 ate a imagem chegar, depois "pula" pra o tamanho real.

Web fonts: o problema do FOIT vs FOUT. Quando o browser ve @font-face no CSS, o texto que usa essa fonte tem 2 comportamentos possiveis ate a fonte carregar:

  • FOIT (Flash of Invisible Text): texto invisivel ate a fonte custom carregar. Padrao em Safari e browsers antigos. Ruim pra UX - usuario ve espaco vazio.
  • FOUT (Flash of Unstyled Text): texto visivel com fonte do sistema ate a custom carregar, depois troca. Padrao com font-display: swap. Muito melhor - usuario ve conteudo imediato.
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-display: swap;  /* FOUT em vez de FOIT */
  font-weight: 400;
}

Quase sempre font-display: swap. A reformata quando a fonte chega e' visivel, mas muito menos prejudicial que tela branca. Excecao: fontes que mudam muito o layout (ex: width 20% maior). Aí use optional (mostra fallback se nao carregar em 100ms) ou size-adjust (ajusta o fallback pra ter width similar).

Preload de fontes criticas. Se sua fonte principal e' "Inter" e ela e' usada no LCP, preload:

<head>
  <link
    rel="preload"
    href="/fonts/inter-regular.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  >
</head>

O crossorigin e' obrigatorio pra preload de fontes (fonts sao treated como CORS). O browser baixa o WOFF2 em paralelo com o HTML, antes mesmo do CSS ser parseado. Quando o CSS pede a fonte, ja ta em cache - sem FOUT.

Self-host de fontes vs Google Fonts. Google Fonts e' conveniente mas tem custos:

  • DNS lookup extra (fonts.googleapis.com, fonts.gstatic.com).
  • Conexao TCP/TLS extra (RTT adicional).
  • Sem preload automatico.
  • Sem controle de formato (Google serve WOFF2 moderno, OK).

Em 2026, self-host e' o default pra performance. Coloque os WOFF2 no seu servidor/CDN, com Cache-Control: public, max-age=31536000, immutable (1 ano, imutavel). A font e' cacheada 1 ano.

<!-- Self-host -->
<link
  rel="preload"
  href="/fonts/inter-regular.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

<!-- vs Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter">

Self-host e' 10-100ms mais rapido na primeira visita (sem RTT extra pro Google).

Quantos pesos de fonte carregar. Cada peso/italic = 1 arquivo WOFF2 de 20-50KB. Nunca carregue todos os pesos. Escolha:

  • 400 (regular) - sempre.
  • 600 (semibold) - se usa em titulos.
  • 700 (bold) - se usa em CTAs.

Maximo 3-4 pesos. Mais que isso, fica caro. Se o design exige todos os 9 pesos (100-900), questione - quase nunca e' necessario.

Aprofundamento 🟡

font-display: optional vs swap vs fallback vs block. Os 4 valores controlam trade-offs diferentes:

  • block: texto invisivel por ate 3s enquanto a fonte carrega. FOIT. Ruim.
  • swap: texto com fallback ate a fonte custom carregar, depois troca. FOUT. Default sensato.
  • fallback: texto com fallback invisivel por 100ms (FOIT curto), depois FOUT. Compromisso entre block e swap.
  • optional: browser decide se baixa ou nao baseado na conexao. Em conexao lenta, fica no fallback. Em rapida, baixa e troca. Melhor pra mobile.

optional e' sub-utilizado. Em mobile 3G, evita o download da fonte (fica no fallback), o que e' mais responsivo que esperar 3s e fazer FOUT.

size-adjust e @font-face trick pra zero-CLS swap. O FOUT causa CLS quando a fonte custom tem width diferente do fallback. Solucao: ajustar o fallback pra ter width similar:

@font-face {
  font-family: "Inter Fallback";
  src: local("Arial");
  size-adjust: 107%;   /* Inter e' 7% mais largo que Arial */
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
}

body {
  font-family: "Inter", "Inter Fallback", sans-serif;
}

Resultado: a fonte fallback tem metrics proximas da Inter, entao quando a Inter carrega, a diferenca de width e' sub-perceptivel. CLS ~0. Padrao moderno em 2026.

aspect-ratio em CSS - alternativa a width/height em HTML. Se voce quer controlar aspect ratio via CSS (mais flexivel), use aspect-ratio:

.hero {
  aspect-ratio: 16 / 9;
  width: 100%;
  background-image: url("hero.avif");
  background-size: cover;
}

Browser reserva espaco (16:9) antes da imagem carregar. Sem CLS. Funciona com <img> tambem:

img {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

image-set() em CSS - srcset sem HTML. A mesma logica de srcset, mas em CSS:

.hero {
  background-image: image-set(
    url("hero.avif") type("image/avif") 1x,
    url("hero.avif") type("image/avif") 2x,
    url("hero-2x.avif") type("image/avif") 2x
  );
}

Util pra background-image (que nao tem <picture>). Suporte ainda limitado (Safari 17+, Chrome 88+, Firefox 113+).

@font-face variable fonts - 1 arquivo, infinitos pesos. Variable fonts (WOFF2 com axis) substituem multiplos pesos por 1 unico arquivo com axis (wght, slnt, ital):

@font-face {
  font-family: "Inter Variable";
  src: url("/fonts/inter-variable.woff2") format("woff2-variations");
  font-weight: 100 900;   /* range */
  font-display: swap;
}

body { font-family: "Inter Variable", sans-serif; }
h1 { font-weight: 700; }
h2 { font-weight: 600; }
.bold { font-weight: 800; }

1 arquivo de 80KB substitui 5 arquivos de 30KB cada. 150KB -> 80KB. Em 2026, a maioria dos designers ja entrega variable fonts. Se o design usa Inter, Roboto, Source Sans - todos tem versao variable.

<img srcset> automatic com build tools. Voce nao precisa escrever 5 URLs no srcset manualmente. Ferramentas como vite-imagetools fazem em build time:

import hero from "./hero.jpg?w=400;800;1200;2000&format=avif;webp&as=picture";
// retorna { src, sources, img }

O build gera as variantes, e o componente emite o <picture> correto. Em Next.js, o componente <Image> faz isso automaticamente.

Pra quem quer ir mais alem 🔴

Por que JPEG XL morreu. JPEG XL (jxl) era um sucessor moderno do JPEG com 30% menos peso. Chrome e Safari adicionaram suporte em 2022-2023. Em 2024, Chrome descontinuou (removido em Chrome 130, 2024). O argumento: AVIF ja cobre o caso de uso com compressao similar e ecossistema maior. Licao: nao aposte em formato novo sem verificar engajamento de longo prazo do vendor. WebP e AVIF sao apostas seguras.

priority hints API e o futuro de fetchpriority. fetchpriority e' estavel em 2026. Mas o "low priority" ainda e' pouco usado:

<img src="banner-ad.avif" fetchpriority="low" loading="lazy">

Util pra imagens claramente "abaixo da dobra" (footer, sidebar). Em conjunto com lazy load, o browser sabe "baixa isso em background, nao concorre com o LCP".

AVIF vs WebP: a escolha pragmatica. AVIF tem compressao melhor (~20% vs WebP), mas encoding e' 10x mais lento (CPU). Em sites com 100+ imagens, build com AVIF demora 10min vs 1min com WebP. Em 2026:

  • Use AVIF pra imagens criticas (hero, LCP, banners).
  • Use WebP pra imagens "abaixo da dobra" (galeria, cards).
  • Use JPEG so pra fallback (browsers antigos).

A economia marginal de AVIF em imagens abaixo da dobra (que carregam lazy) nao justifica o tempo de build.

placeholder="blur" e LQIP. Padrao moderno: gerar um placeholder bem pequeno e embacado (20x15px, base64 inline), mostrar enquanto a imagem real baixa:

<Image
  src="hero.avif"
  alt="Hero"
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,..."  // 200 bytes
  width="1200"
  height="600"
/>

UX: o usuario ve o "blur" da imagem (indicacao visual de que algo ta vindo), sem perceber o delay. Quando a imagem chega, faz fade-in. LCP nao muda (o blur nao conta como LCP), mas a perceived performance melhora muito.

Content-Type correto pra AVIF/WebP. Servidor precisa retornar Content-Type: image/avif (e nao image/jpeg). Nginx/Apache precisam de config:

# nginx
types {
  image/avif avif;
  image/webp webp;
}

Sem isso, o browser rejeita a imagem (mesmo que o arquivo seja valido). Erro classico: "AVIF configurado, mas Chrome nao carrega".

Leitura recomendada:

Dica: o erro mais comum em imagens e' mandar 1 imagem gigante e rezar. Hero de 4000px no celular de 375px = 10x mais bytes que o necessario. Sempre use <picture> + srcset + sizes. E lembre: a imagem do LCP precisa de preload + fetchpriority="high". Sem isso, o browser prioriza o CSS (que bloqueia), e a imagem so comeca a baixar tarde.

No proximo no, vamos CDN e cache: Cache-Control headers, ETag, como CDNs funcionam (edge, POP, TTL), e o pattern de stale-while-revalidate que entrega "instantaneo" mesmo com cache expirado.

// Quiz

Por que `font-display: swap` e' o default recomendado em 2026 (em vez de `block` ou `optional`)?

Escolha uma alternativa

// recursos

// avaliação da trilha

—
ainda sem avaliações