Hospedagem eficiente de imagens: CDNs e processamento
A entrega estática e a transformação de imagens resolvem problemas diferentes. Uma CDN estática
armazena em cache arquivos que já existem; ela não redimensiona um JPEG simplesmente porque você
acrescenta ?width=800. Aqui, você vai gerar arquivos versionados a partir de uma
fotografia local confiável e abrir uma prévia responsiva com uma única tentativa de substituição da
imagem em caso de erro. Depois, você poderá publicar esses mesmos arquivos na sua CDN de hospedagem
de imagens.
Escolher arquivos estáticos ou transformações dinâmicas
Gerar os arquivos antecipadamente evita o trabalho de decodificação quando uma nova URL de imagem é solicitada. Você armazena e publica um conjunto conhecido de arquivos, incluindo variantes que talvez nunca sejam solicitadas. Uma URL versionada identifica um arquivo com dimensões e formato fixos, o que facilita distinguir falhas de geração de falhas de entrega.
Escolha de forma deliberada entre variantes pré-geradas e um serviço de transformação. Se precisar de redimensionamento arbitrário, use uma origem de processamento ou um serviço como o Smart CDN da Transloadit. Verifique o contrato de parâmetros e assinatura desse serviço; objetos estáticos no S3/CloudFront não interpretam parâmetros de transformação na consulta da URL.
Cloudinary
Os serviços hospedados de transformação definem sua própria sintaxe de URL e os locais de origem permitidos. Siga a referência de transformações da Cloudinary ou a documentação equivalente do seu provedor. Uma URL de um provedor não pode ser usada como um contrato genérico de redimensionamento em outra CDN.
Ferramentas e bibliotecas para processamento de imagens
Sharp (Node.js)
Use uma versão do Node.js 26 com manutenção ativa e o Corepack instalado separadamente. Este exemplo foi testado com Node.js 26.12.1 e 26.13.0, além do Yarn 4.12.0. O Sharp 0.35.4 é a versão fixada aqui para garantir a reprodutibilidade, sem afirmar que seja o patch mais recente. Consulte os pré-requisitos de instalação do Sharp para conhecer as plataformas compatíveis e os pacotes nativos opcionais.
Execute este bloco Bash em um diretório com permissão de escrita. Ele cria um projeto Yarn separado e mantém seu shell no diretório original. O arquivo de lock vazio estabelece um limite para o projeto mesmo dentro de um workspace existente; o Yarn ainda lê as configurações aplicáveis dos diretórios superiores.
(
command -v node >/dev/null &&
command -v corepack >/dev/null &&
mkdir image-publish &&
cd image-publish &&
printf '%s\n' '{"private":true,"packageManager":"yarn@4.12.0"}' > package.json &&
touch yarn.lock &&
corepack yarn add --exact sharp@0.35.4
)
Se image-publish já existir, escolha outro nome para o novo diretório em vez de
remover seu conteúdo. Se a instalação falhar após a criação, inspecione esse diretório e escolha um
nome novo para a próxima tentativa de configuração. Após uma instalação bem-sucedida, abra um
terminal em image-publish. Use corepack yarn node para executar o script,
para que funcionem tanto a instalação Plug’n’Play padrão do Yarn quanto seu vinculador
node-modules.
Forneça photo.jpg: seu conteúdo pode ser JPEG ou PNG, independentemente desse
nome de arquivo fixo. Use uma imagem sRGB confiável de 8 bits, sem canal alfa. A entrada não pode
estar vazia nem exceder 8 MiB, com no máximo 12.000.000 pixels e pelo menos 1280 pixels de largura
após a aplicação da orientação EXIF. O programa rejeita outros formatos decodificados, espaços de cor
e profundidades de amostra. Este é um processo sequencial de publicação, não um endpoint de upload
nem um validador geral para arquivos maliciosos. Os contêineres JPEG e PNG são lidos como uma imagem
estática principal; isso não comprova que uma imagem complementar MPO, uma animação APNG ou um mapa
de ganho HDR tenha sido processado.
Salve o código como build-images.mts:
import { mkdir, open, writeFile } from 'node:fs/promises'
import sharp, { type SharpOptions } from 'sharp'
async function main() {
const file = await open('photo.jpg', 'r')
let source
try {
const stat = await file.stat()
if (!stat.isFile() || stat.size < 1 || stat.size > 8 * 1024 * 1024) {
throw new Error('Use a regular source image no larger than 8 MiB.')
}
source = Buffer.alloc(stat.size)
let offset = 0
while (offset < source.length) {
const { bytesRead } = await file.read(source, offset, source.length - offset, null)
if (bytesRead === 0) throw new Error('Source changed during publishing.')
offset += bytesRead
}
} finally {
await file.close()
}
const options: SharpOptions = { limitInputPixels: 12_000_000, failOn: 'warning' }
const metadata = await sharp(source, options).metadata()
if (!['jpeg', 'png'].includes(metadata.format) || (metadata.pages ?? 1) !== 1) {
throw new Error('Use a JPEG or PNG primary image.')
}
if (metadata.space !== 'srgb' || metadata.depth !== 'uchar' || metadata.hasAlpha) {
throw new Error('Use an 8-bit sRGB image without an alpha channel.')
}
const orientedWidth = metadata.autoOrient?.width ?? metadata.width
if (!orientedWidth || orientedWidth < 1280) throw new Error('Source must be at least 1280px wide.')
await mkdir('images', { recursive: true })
for (const width of [320, 640, 1280]) {
const result = await sharp(source, options).rotate().resize({ width }).webp({ quality: 80 })
.toBuffer({ resolveWithObject: true })
await writeFile('images/photo-' + width + '-v1.webp', result.data, { flag: 'wx' })
}
const fallback = await sharp(source, options).rotate().resize({ width: 640 })
.jpeg({ quality: 80 }).toBuffer({ resolveWithObject: true })
await writeFile('images/photo-640-v1.jpg', fallback.data, { flag: 'wx' })
console.log(JSON.stringify({ width: fallback.info.width, height: fallback.info.height }))
}
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : 'Image publishing failed.')
process.exitCode = 1
})
Execute o programa salvo a partir desse diretório:
corepack yarn node build-images.mts
Ele grava três variantes WebP definidas apenas pela largura, com 320, 640 e 1280 pixels, além de uma alternativa JPEG com 640 pixels de largura. Todas preservam a proporção da imagem após a orientação. A última linha JSON informa a largura e a altura reais do JPEG. Abra a prévia local e inspecione a imagem antes de fazer qualquer upload; a qualidade 80 é uma configuração do codificador, não uma promessa de tamanho de arquivo ou qualidade perceptual específicos.
Não modifique a entrada durante a execução. As gravações exclusivas recusam nomes de arquivos de
saída já existentes, incluindo a alternativa JPEG. Uma falha pode deixar arquivos de saída parciais;
inspecione-os e use uma nova versão ao tentar novamente. Altere v1 em
todos os nomes de saída e referências HTML ao mesmo tempo ao publicar uma nova versão. O processo
não desfaz a gravação dos arquivos que já criou.
A orientação automática do Sharp aplica a orientação EXIF antes do redimensionamento. Seu comportamento padrão de saída remove os metadados da origem. Inspecione as cores renderizadas e a orientação por conta própria; esses limites não comprovam que um original danificado esteja íntegro.
ImageMagick
Se você já tem o ImageMagick 7 com suporte a JPEG e WebP, este comando Bash cria um WebP pequeno a partir da mesma origem aprovada. Ele foi testado com o ImageMagick 7.1.2-32; obtenha atualizações com manutenção ativa na página de downloads do ImageMagick.
(
set -o noclobber
magick photo.jpg -auto-orient -resize '640x640>' -strip -quality 80 webp:- > photo-small.webp
)
A geometria de redimensionamento entre aspas ajusta a imagem aos
limites de 640 × 640 sem ampliá-la. Uma imagem de 1280 × 720 passa a ter 640 × 360; uma imagem de
320 × 180 mantém esse tamanho. Isso difere das variantes definidas apenas pela largura do script
Sharp e não cria o conjunto completo da prévia. O shell recusa um photo-small.webp já
existente antes de iniciar a conversão. Uma falha na conversão pode deixar um novo arquivo vazio ou
parcial; inspecione-o antes de tentar novamente com um nome novo. O comando não tem nenhuma das
proteções de entrada do script Sharp. Use aqui apenas a origem sRGB de 8 bits já aprovada; arquivos
não confiáveis precisam de isolamento de processos e de políticas para decodificadores e recursos.
Visualizar imagens responsivas pré-geradas
Salve esta página completa como index.html ao lado do diretório gerado
images/. Salve o bloco de tratamento de erros logo abaixo como
fallback.js e abra index.html no navegador. Seus caminhos
relativos funcionam diretamente a partir do diretório local.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Responsive image preview</title>
<body style="margin: 0">
<picture>
<source
type="image/webp"
data-srcset="images/photo-320-v1.webp 320w, images/photo-640-v1.webp 640w, images/photo-1280-v1.webp 1280w"
sizes="(max-width: 640px) 100vw, 640px"
>
<img
id="hero"
data-src="images/photo-640-v1.jpg"
data-fallback="images/placeholder.png"
alt="A description of your photograph"
width="640"
height="360"
style="display: block; width: 100%; max-width: 640px; height: auto"
>
</picture>
<script src="fallback.js"></script>
</body>
</html>
Defina width e height com as dimensões reais da
alternativa JPEG informadas pelo script. Os valores 640 × 360 correspondem a uma origem 16:9;
outras proporções exigem alturas diferentes. Substitua o texto alternativo por uma descrição da sua
imagem. O valor de sizes corresponde ao layout desta página, que limita a
largura em pixels CSS a 640. O navegador pode escolher entre as opções WebP de acordo com sua área de
visualização e densidade de pixels; o JPEG é o padrão quando WebP não é selecionado.
Os atributos data-src e data-srcset adiam as requisições até
que fallback.js instale seu manipulador. Esta prévia precisa de JavaScript.
Forneça um PNG real seu como images/placeholder.png ou use uma cópia de um PNG aprovado.
Para testar a recuperação, renomeie temporariamente o arquivo WebP selecionado e recarregue a
página. Restaure o nome depois. Se a imagem de substituição estiver ausente, o texto alternativo da
imagem deverá permanecer, sem novas tentativas repetidas.
Tratar erros de carregamento de imagens
Salve o código como fallback.js ao lado de index.html. Ele
registra o manipulador antes de ativar as URLs da prévia. A remoção das opções responsivas permite
que a imagem de substituição se torne a imagem selecionada:
function installImageFallback(image, fallbackUrl) {
image.addEventListener('error', () => {
for (const source of image.closest('picture')?.querySelectorAll('source') ?? []) {
source.remove()
}
image.removeAttribute('srcset')
image.removeAttribute('sizes')
image.src = fallbackUrl
}, { once: true })
}
const hero = document.getElementById('hero')
if (hero instanceof HTMLImageElement) {
installImageFallback(hero, hero.dataset.fallback ?? '/images/placeholder.png')
for (const source of hero.closest('picture')?.querySelectorAll('source') ?? []) {
if (source.dataset.srcset) source.srcset = source.dataset.srcset
}
if (hero.dataset.src) hero.src = hero.dataset.src
}
O manipulador é executado uma vez por elemento de imagem. Se a imagem de substituição também falhar, o navegador mantém o texto alternativo acessível e nenhuma outra imagem alternativa é atribuída. Em uma aplicação existente, associe o manipulador antes de definir as URLs das imagens, para que uma falha imediata não possa ocorrer antes do registro.
Publicar arquivos versionados na sua CDN
Faça o upload dos arquivos WebP gerados e da alternativa JPEG de images/,
junto com sua imagem de substituição, para um prefixo aprovado para conteúdo público. Preserve os
caminhos relativos ou substitua cada URL de imagem na prévia pela respectiva URL da CDN. Defina os
tipos de conteúdo image/webp, image/jpeg e
image/png para os respectivos arquivos. Não faça upload de originais privados
para esse prefixo.
Use tempos de retenção longos no cache para objetos imutáveis e versionados. Defina os metadados de controle de cache de forma deliberada e inspecione os cabeçalhos de resposta da CDN; alterar os metadados da origem não substitui instantaneamente as respostas em cache.
Você não precisa de uma conta na nuvem para este exemplo local. Para provisionar a infraestrutura, consulte o guia separado de configuração do S3 e do CloudFront (English). Ele é uma leitura complementar, não um pré-requisito para a prévia.
Para essa implantação, confira os requisitos atuais da AWS para origens privadas. O OAC usa uma origem de bucket S3 comum, não um endpoint de site. Com a propriedade de todos os objetos obrigatoriamente atribuída ao proprietário do bucket, requisições OAC sempre assinadas e uma política de bucket restrita à distribuição, o CloudFront pode ler uma origem privada. Objetos criptografados com SSE-KMS também precisam da política adequada para a chave KMS. O HTTPS para visitantes e a autorização de visitantes são configurações separadas; o OAC não autentica visitantes de uma URL pública da CDN.
Considerações de segurança ao usar CDNs
Origens privadas e entrega privada são controles separados. Para acesso privado de visitantes, configure URLs assinadas do CloudFront e o grupo de chaves confiáveis associado. Uma URL pré-assinada do S3 não assina automaticamente o CloudFront.
Mantenha as chaves de assinatura no lado do servidor, autorize o objeto solicitado antes de conceder acesso e evite registrar strings de consulta assinadas nos logs. Não publique respostas privadas com uma política de cache público compartilhado.
Solucionar problemas comuns de hospedagem de imagens em CDNs
Invalidação de cache
Prefira um novo nome de arquivo imutável para uma nova imagem. Quando a invalidação for necessária, restrinja-a à distribuição e aos caminhos pretendidos. Uma invalidação é uma operação real na conta e pode ter um custo; não a execute como um comando genérico de depuração em produção.
Monitoramento de desempenho
Use o painel de rede do navegador para inspecionar a requisição da imagem selecionada, o tamanho da resposta e os cabeçalhos de cache. Teste dispositivos representativos e caches tanto frios quanto aquecidos. A prévia local verifica a seleção de arquivos e a recuperação; ela não mede a latência de uma CDN implantada nem comprova economia de largura de banda.

