Pular para o conteúdo principal

Imagens (Cloudflare Images)

Todas as imagens vindas do backend passam pelo Cloudflare Image Delivery. O helper canônico é getResponsiveImageProps (@cactus-agents/utils), e a arte de jogo tem um wrapper obrigatório em cima dele — getGameArtworkProps (app/utils/game-artwork.ts).

Duas regras dominam esta página, e ambas vêm da mesma propriedade do Cloudflare: o transform é codificado no PATH da URL.

A regra de base: o transform vai no path

Uma URL transformada tem esta forma (buildNormalizedCloudflareImageUrl em packages/utils/src/image.ts):

/cdn-cgi/imagedelivery/<accountHash>/<imageId>/width=240,quality=75,format=auto

Consequências diretas:

1. Cada combinação de parâmetros é um recurso DISTINTO. Cache-key de browser própria, entrada própria no cache do Cloudflare, download próprio. Duas telas que mostram a mesma imagem com ladders de largura diferentes fazem dois downloads.

2. Qualidade e formato NUNCA funcionam em querystring. O Cloudflare ignora. Medido em 2026-08-04 contra a CDN real: …/w=240, …/w=240?quality=60 e …/w=240?quality=20 devolvem bytes idênticos (16.3KB), enquanto …/width=240,quality=60,format=auto devolve 9.2KB — 43% menos pela mesma imagem. Se você viu ?quality= numa URL, ou é o caminho sem width (variante nomeada public, cujos params são inertes e mantidos só pra não trocar a cache-key de todas as marcas) ou é bug.

Sem width, a URL cai na variante nomeada public e qualidade/formato vêm da config da variante no dashboard do Cloudflare — não tente ajustar qualidade por querystring; passe width pra entrar no caminho de path.

O default global de qualidade fica em app/config/scripts/images.ts (quality: 75), que chama setImageDefaults do core. useProxy reescreve imagedelivery.net/cdn-cgi/imagedelivery (same-origin, elimina DNS + TLS extra) e é desligado em dev local, onde não há proxy do Workers.

Arte de jogo: perfil ÚNICO, sem exceção

game.image aparece em muitas superfícies — card de listagem, carrossel, game page, pre-game drawer, top-10, lista de busca, widget de último jogo. Todas usam o mesmo helper:

import { getGameArtworkProps } from "~/utils/game-artwork";

const artwork = getGameArtworkProps(game.image, "(max-width: 640px) 115px, 180px");

<img src={artwork.src} srcSet={artwork.srcSet} sizes={artwork.sizes} />

sizes é o único parâmetro por superfície: descreve o slot de layout em CSS px. O ladder de larguras é compartilhado.

O que NÃO fazer

  • Não monte um ladder de widths próprio. Ladder divergente = a mesma arte baixada duas vezes. Era exatamente o que acontecia (medido em 2026-08-04, 7k mobile, DPR 3): o card do carrossel pedia width=240,height=300 (10.0KB) e a game page pedia width=320,height=400 (13.5KB) — 23.5KB pra mostrar a mesma arte duas vezes, com o segundo request entrando frio no 3G do usuário.
  • Não passe aspectRatio nem height. O resize é proporcional; o enquadramento é 100% CSS.
  • Não "corrija" um sizes de game page pro slot real sem checar a convergência em DPR 1 — vira download extra silencioso.

Recorte é CSS

Cada slot tem caixa de aspect fixa + object-cover (ou object-contain). Isso dá três ganhos:

  1. Uma URL por largura pra todo o app — o card 4/5, o poster 59/79 do donald e o thumb de proporção natural da game page compartilham o mesmo arquivo em vez de gerar três recortes.
  2. Zero upscale — a altura sai do próprio master.
  3. Menos bytes no mesmo degrau — sem forçar 4/5, width=240 devolve 240×280 em vez de 240×300.

Teto do ladder ≈ 300px

O master da arte de jogo é 300×350 (amostrado sobre a biblioteca real). Pedir acima disso faz o Cloudflare inventar pixel: width=480,height=600 devolvia 21.5KB de upscale — o dobro dos bytes do degrau que já estava em cache, com zero detalhe novo e cache frio.

O default global respeita o teto: GAME_CARD_THUMB_WIDTHS = [128, 192, 288], fallback 192 (app/types/game-card.ts).

O knob por marca

gameCardConfig.thumbWidths / thumbFallbackWidth (app/config/widgets/game-card.ts, overrideável por marca) é o único knob — e desde 2026-08-04 vale pra toda arte de jogo, não só pros cards. Subir o ladder de uma marca encarece toda a arte dela; é o trade-off desejado (é o mesmo knob que a 7k usa pra cortar memória de bitmap decodado e atacar o flicker de re-decode no scroll).

A regra do degrau do meio (DPR 1)

Ladder compartilhado não basta. Em DPR ≥ 2 (todo mobile real) todo slot satura no teto e qualquer declaração converge. Em DPR 1 os slots divergem de verdade e um degrau no meio do ladder separa card de game page — a mesma arte baixada 2x.

Duas regras, ambas cobertas por teste (game-artwork-brands.test.ts):

  1. O degrau do meio fica logo acima do maior slot de desktop, e não pode existir degrau entre o menor slot de card de desktop e ele.
  2. As superfícies de game page declaram o slot do CARD no mobile — sub-declaram de propósito (~19% no preview, ~2% no hero showcase) pra cair no mesmo degrau que o card. O custo é 1.19x de upscale só em DPR 1 estreito, imperceptível numa arte de jogo; o ganho é uma request em vez de duas.

Nunca super-declare. Era o caso de todas as superfícies de game page (220px onde o slot é 186px): gasta bytes a mais e empurra pro degrau errado.

O docblock de app/utils/game-artwork.ts mantém a tabela viva de slots medidos por superfície — consulte lá antes de mexer em qualquer sizes de arte de jogo.

Imagens que não são arte de jogo

Pra banners, logos, thumbs de conteúdo e afins, use getResponsiveImageProps direto:

import { getResponsiveImageProps } from "@cactus-agents/utils";

const props = getResponsiveImageProps(banner.url, {
widths: [460, 768, 1200],
sizes: "(max-width: 768px) 90vw, 1200px",
});

Aqui aspectRatio é aceito (resolveHeight gera height + fit=cover por degrau) — a proibição vale só pra arte de jogo, que precisa convergir entre superfícies. Ainda assim, mantenha o ladder de um mesmo asset consistente entre telas pelo mesmo motivo de cache.

Docs relacionadas