Como criar um componente de imagem eficaz

Um componente de imagem encapsula as práticas recomendadas de performance e oferece uma solução pronta para uso para otimizar imagens.

Leena Sohoni
Leena Sohoni
Kara Erickson
Kara Erickson
Alex Castle
Alex Castle

As imagens são uma fonte comum de gargalos de performance para aplicativos da Web e uma área de foco principal para otimização. As imagens não otimizadas contribuem para o aumento do tamanho da página e representam mais de 70% do peso total da página em bytes no 90th percentil. Há várias maneiras de otimizar imagens, o que exige um "componente de imagem" inteligente com soluções de performance integradas por padrão.

A equipe do Aurora trabalhou com o Next.js para criar um desses componentes. O objetivo era criar um modelo de imagem otimizado que os desenvolvedores da Web pudessem personalizar ainda mais. O componente serve como um bom modelo e define um padrão para a criação de componentes de imagem em outros frameworks, sistemas de gerenciamento de conteúdo (CMSs) e pilhas de tecnologia. Colaboramos em um componente semelhante para Nuxt.js e estamos trabalhando com o Angular na otimização de imagens em versões futuras. Esta postagem discute como projetamos o componente de imagem do Next.js e as lições que aprendemos ao longo do caminho.

Componente de imagem como uma extensão de imagens

Problemas e oportunidades de otimização de imagens

As imagens afetam não apenas o desempenho, mas também os negócios. O número de imagens em uma página foi o segundo maior indicador de conversões de usuários que visitam sites. As sessões em que os usuários fizeram uma conversão tiveram 38% menos imagens do que as sessões em que não houve conversão. O Lighthouse lista várias oportunidades para otimizar imagens e melhorar os fundamentos da Web como parte da auditoria de práticas recomendadas. Confira algumas das áreas comuns em que as imagens podem afetar as principais métricas da Web e a experiência do usuário.

Imagens sem tamanho definido prejudicam a CLS

Imagens veiculadas sem tamanho especificado podem causar instabilidade de layout e contribuir para um alto Cumulative Layout Shift (CLS). Definir os atributos width e height em elementos <img> pode ajudar a evitar mudanças de layout. Exemplo:

<img src="flower.jpg" width="360" height="240">

A largura e a altura precisam ser definidas de modo que a proporção da imagem renderizada seja próxima à proporção natural. Uma diferença significativa na proporção pode distorcer a imagem. Uma propriedade relativamente nova que permite especificar proporção em CSS ajuda a dimensionar imagens de forma responsiva e evita o CLS.

Imagens grandes podem prejudicar a LCP

Quanto maior o tamanho do arquivo de uma imagem, mais tempo ela vai levar para ser baixada. Uma imagem grande pode ser a imagem principal da página ou o elemento mais significativo na janela de visualização responsável por acionar o Largest Contentful Paint (LCP). Uma imagem que faz parte do conteúdo essencial e leva muito tempo para ser baixada vai atrasar o LCP.

Em muitos casos, os desenvolvedores podem reduzir os tamanhos das imagens com uma compactação melhor e o uso de imagens responsivas. Os atributos srcset e sizes do elemento <img> ajudam a fornecer arquivos de imagem com tamanhos diferentes. O navegador pode escolher a opção certa dependendo do tamanho e da resolução da tela.

A compressão inadequada da imagem pode prejudicar a LCP

Formatos de imagem modernos, como AVIF ou WebP, podem oferecer uma compactação melhor do que os formatos JPEG e PNG usados com frequência. Uma compactação melhor reduz o tamanho do arquivo em 25% a 50% em alguns casos, mantendo a mesma qualidade da imagem. Essa redução resulta em downloads mais rápidos com menos consumo de dados. O app precisa veicular formatos de imagem modernos para navegadores que os aceitam.

Carregar imagens desnecessárias prejudica o LCP

As imagens abaixo da dobra ou que não estão na janela de visualização não são mostradas ao usuário quando a página é carregada. Eles podem ser adiados para não contribuir com o LCP e atrasá-lo. O carregamento lento pode ser usado para carregar essas imagens mais tarde, à medida que o usuário rola a tela em direção a elas.

Desafios de otimização

As equipes podem avaliar o custo de performance devido aos problemas listados anteriormente e implementar soluções de práticas recomendadas para resolvê-los. No entanto, isso geralmente não acontece na prática, e imagens ineficientes continuam deixando a Web mais lenta. Alguns possíveis motivos para isso:

  • Prioridades: os desenvolvedores da Web geralmente se concentram em código, JavaScript e otimização de dados. Por isso, talvez não saibam dos problemas com as imagens ou como otimizá-las. As imagens criadas por designers ou enviadas por usuários podem não estar no topo da lista de prioridades.
  • Solução pronta para uso: mesmo que os desenvolvedores conheçam as nuances da otimização de imagens, a ausência de uma solução pronta para uso e completa para o framework ou a pilha de tecnologia pode ser um obstáculo.
  • Imagens dinâmicas: além das imagens estáticas que fazem parte do aplicativo, as imagens dinâmicas são enviadas pelos usuários ou extraídas de bancos de dados externos ou CMSs. Pode ser difícil definir o tamanho dessas imagens quando a fonte é dinâmica.
  • Sobrecarga de marcação: as soluções para incluir o tamanho da imagem ou srcset para diferentes tamanhos exigem marcação adicional para cada imagem, o que pode ser cansativo. O atributo srcset foi lançado em 2014, mas é usado por apenas 26,5% dos sites atualmente. Ao usar srcset, os desenvolvedores precisam criar imagens em vários tamanhos. Ferramentas como just-gimme-an-img podem ajudar, mas precisam ser usadas manualmente para cada imagem.
  • Compatibilidade com navegadores: formatos de imagem modernos, como AVIF e WebP, criam arquivos de imagem menores, mas precisam de um tratamento especial em navegadores que não são compatíveis com eles. Os desenvolvedores precisam usar estratégias como negociação de conteúdo ou o elemento <picture> para que as imagens sejam veiculadas em todos os navegadores.
  • Complicações do carregamento lento: há várias técnicas e bibliotecas disponíveis para implementar o carregamento lento de imagens abaixo da dobra. Escolher a melhor opção pode ser um desafio. Os desenvolvedores também podem não saber a melhor distância da "dobra" para carregar imagens adiadas. Diferentes tamanhos de janela de visualização em dispositivos podem complicar ainda mais isso.
  • Mudança no cenário: à medida que os navegadores começam a oferecer suporte a novos recursos de HTML ou CSS para melhorar a performance, pode ser difícil para os desenvolvedores avaliar cada um deles. Por exemplo, o Chrome está lançando o recurso Prioridade de busca como um teste de origem. Ele pode ser usado para aumentar a prioridade de imagens específicas na página. No geral, os desenvolvedores achariam mais fácil se essas melhorias fossem avaliadas e implementadas no nível do componente.

Componente de imagem como solução

As oportunidades disponíveis para otimizar imagens e os desafios de implementá-las individualmente em cada aplicativo nos levaram à ideia de um componente de imagem. Um componente de imagem pode encapsular e aplicar práticas recomendadas. Ao substituir o elemento <img> por um componente de imagem, os desenvolvedores podem resolver melhor os problemas de performance de imagem.

No ano passado, trabalhamos com o framework Next.js para projetar e implementar o componente de imagem. Ele pode ser usado como um substituto direto para os elementos <img> atuais em apps Next.js da seguinte maneira.

// Before with <img> element:
function Logo() {
  return <img src="/logo.jpg" alt="logo" height="200" width="100" />
}

// After with image component:
import Image from 'next/image'

function Logo() {
  return <Image src="/logo.jpg" alt="logo" height="200" width="100" />
}

O componente tenta resolver problemas relacionados a imagens de forma genérica usando um conjunto avançado de recursos e princípios. Ele também inclui opções que permitem aos desenvolvedores personalizar o modelo para vários requisitos de imagem.

Proteção contra mudanças de layout

Como discutido anteriormente, imagens sem tamanho causam mudanças de layout e contribuem para a CLS. Ao usar o componente de imagem do Next.js, os desenvolvedores precisam fornecer um tamanho de imagem usando os atributos width e height para evitar mudanças de layout. Se o tamanho for desconhecido, os desenvolvedores precisarão especificar layout=fill para veicular uma imagem sem tamanho dentro de um contêiner dimensionado. Você também pode usar importações de imagens estáticas para recuperar o tamanho da imagem real no disco rígido no tempo de build e incluí-lo na imagem.

// Image component with width and height specified
<Image src="/logo.jpg" alt="logo" height="200" width="100" />

// Image component with layout specified
<Image src="/hero.jpg" layout="fill" objectFit="cover" alt="hero" />

// Image component with image import
import Image from 'next/image'
import logo from './logo.png'

function Logo() {
  return <Image src={logo} alt="logo" />
}

Como os desenvolvedores não podem usar o componente de imagem sem tamanho, o design garante que eles considerem o dimensionamento da imagem e evitem trocas de layout.

Facilitar a capacidade de resposta

Para tornar as imagens responsivas em todos os dispositivos, os desenvolvedores precisam definir os atributos srcset e sizes no elemento <img>. Queremos reduzir esse esforço com o componente de imagem. Projetamos o componente de imagem do Next.js para definir os valores de atributo apenas uma vez por aplicativo. Aplicamos esses estilos a todas as instâncias do componente "Imagem" com base no modo de layout. Criamos uma solução em três partes:

  1. Propriedade deviceSizes: pode ser usada para configurar pontos de interrupção uma única vez com base nos dispositivos comuns à base de usuários do aplicativo. Os valores padrão dos pontos de interrupção estão incluídos no arquivo de configuração.
  2. Propriedade imageSizes: também é uma propriedade configurável usada para receber os tamanhos de imagem correspondentes aos pontos de interrupção de tamanho do dispositivo.
  3. Atributo layout em cada imagem: usado para indicar como usar as propriedades deviceSizes e imageSizes em cada imagem. Os valores aceitos para o modo de layout são fixed, fill, intrinsic e responsive.

Quando uma imagem é solicitada com os modos de layout responsivo ou preencher, o Next.js identifica a imagem a ser veiculada com base no tamanho do dispositivo que solicita a página e define srcset e sizes na imagem de maneira adequada.

A comparação a seguir mostra como o modo de layout pode ser usado para controlar o tamanho da imagem em telas diferentes. Usamos uma imagem de demonstração compartilhada na documentação do Next.js, visualizada em um smartphone e um notebook padrão.

Tela de laptop Tela do smartphone
Layout = Intrinsic: reduz a escala para ajustar à largura do contêiner em janelas de visualização menores. Não escalona verticalmente além do tamanho intrínseco da imagem em uma janela de visualização maior. A largura do contêiner está em 100%
Imagem de montanhas mostrada no estado em que se encontra Imagem de montanhas reduzida
Layout = Fixed: a imagem não é responsiva. A largura e a altura são fixas, semelhantes ao elemento ``, independente do dispositivo em que ele é renderizado.
Imagem de montanhas mostrada no estado em que se encontra A imagem das montanhas mostrada como está não cabe na tela
Layout = Responsivo: reduz ou aumenta a escala dependendo da largura do contêiner em diferentes viewports, mantendo a proporção.
Imagem de montanhas ampliada para caber na tela Imagem de montanhas reduzida para caber na tela
Layout = Preencher: largura e altura esticadas para preencher o contêiner principal. (A largura do elemento pai <div> está definida como 300*500 neste exemplo)
Imagem de montanhas renderizada para caber no tamanho 300*500 Imagem de montanhas renderizada para caber no tamanho 300*500
Imagens renderizadas para diferentes layouts

Fornecer carregamento lento integrado

O componente Image oferece uma solução integrada e eficiente de carregamento lento como padrão. Ao usar o elemento <img>, há algumas opções de carregamento lento, mas todas têm desvantagens que dificultam o uso. Um desenvolvedor pode adotar uma das seguintes abordagens de carregamento lento:

  • Especifique o atributo loading: ele é compatível com todos os navegadores modernos.
  • Use a API Intersection Observer: criar uma solução personalizada de carregamento lento exige esforço e um design e implementação bem pensados. Os desenvolvedores nem sempre têm tempo para isso.
  • Importe uma biblioteca de terceiros para carregar imagens de forma lenta: talvez seja necessário mais esforço para avaliar e integrar uma biblioteca de terceiros adequada para carregamento lento.

No componente de imagem do Next.js, o carregamento é definido como "lazy" por padrão. O carregamento lento é implementado usando o Intersection Observer, que está disponível na maioria dos navegadores modernos. Os desenvolvedores não precisam fazer nada para ativar esse recurso, mas podem desativá-lo quando necessário.

Pré-carregar imagens importantes

Muitas vezes, os elementos da LCP são imagens, e imagens grandes podem atrasar a LCP. É uma boa ideia pré-carregar imagens críticas para que o navegador possa descobrir essa imagem mais cedo. Ao usar um elemento <img>, uma dica de pré-carregamento pode ser incluída no cabeçalho HTML da seguinte maneira.

<link rel="preload" as="image" href="important.png">

Um componente de imagem bem projetado deve oferecer uma maneira de ajustar a sequência de carregamento das imagens, independente da estrutura usada. No caso do componente de imagem do Next.js, os desenvolvedores podem indicar uma imagem que seja uma boa candidata para pré-carregamento usando o atributo priority do componente de imagens.

<Image src="/hero.jpg" alt="hero" height="400" width="200" priority />

Adicionar um atributo priority simplifica a marcação e é mais conveniente de usar. Os desenvolvedores de componentes de imagem também podem explorar opções para aplicar heurísticas e automatizar o pré-carregamento de imagens acima da dobra na página que atendem a critérios específicos.

Incentivar a hospedagem de imagens de alta performance

As CDNs de imagens são recomendadas para automatizar a otimização de imagens e também são compatíveis com formatos de imagem modernos, como WebP e AVIF. O componente de imagem do Next.js usa uma CDN de imagem por padrão com uma arquitetura de carregador. O exemplo a seguir mostra que o carregador permite a configuração da CDN no arquivo de configuração do Next.js.

module.exports = {
  images: {
    loader: 'imgix',
    path: 'https://ImgApp/imgix.net',
  },
}

Com essa configuração, os desenvolvedores podem usar URLs relativos na origem da imagem, e o framework vai concatenar o URL relativo com o caminho da CDN para gerar o URL absoluto. As CDNs de imagens mais usadas, como Imgix, Cloudinary e Akamai, são compatíveis. A arquitetura oferece suporte ao uso de um provedor de nuvem personalizado ao implementar uma função loader personalizada para o app.

Suporte a imagens auto-hospedadas

Em algumas situações, os sites não podem usar CDNs de imagens. Nesses casos, um componente de imagem precisa oferecer suporte a imagens auto-hospedadas. O componente de imagem do Next.js usa um otimizador de imagens como um servidor de imagens integrado que fornece uma API semelhante a CDN. O otimizador usa o Sharp para transformações de imagens de produção se ele estiver instalado no servidor. Essa biblioteca é uma boa opção para quem quer criar o próprio pipeline de otimização de imagens.

Compatibilidade com carregamento progressivo

O carregamento progressivo é uma técnica usada para manter o interesse dos usuários mostrando uma imagem marcadora de posição, geralmente de qualidade significativamente menor, enquanto a imagem real é carregada. Ele melhora o desempenho percebido e a experiência do usuário. Ele pode ser usado em combinação com o carregamento lento para imagens abaixo ou acima da dobra.

O componente de imagem do Next.js oferece suporte ao carregamento progressivo da imagem usando a propriedade placeholder. Isso pode ser usado como um LQIP (marcador de posição de imagem de baixa qualidade) para mostrar uma imagem de baixa qualidade ou desfocada enquanto a imagem real é carregada.

Impacto

Com todas essas otimizações incorporadas, tivemos sucesso com o componente de imagem do Next.js em produção e também estamos trabalhando com outras pilhas de tecnologia em componentes de imagem semelhantes.

Quando o Leboncoin migrou o front-end JavaScript legado para o Next.js, ele também fez upgrade do pipeline de imagens para usar o componente de imagem do Next.js. Em uma página migrada de <img> para next/image, o LCP caiu de 2,4 s para 1,7 s. O total de bytes de imagem baixados para a página passou de 663 kB para 326 kB (com cerca de 100 kB de bytes de imagem carregados de forma lenta).

Lições aprendidas

Qualquer pessoa que esteja criando um app Next.js pode se beneficiar do uso do componente de imagem do Next.js para otimização. No entanto, se você quiser criar abstrações de performance semelhantes para outro framework ou CMS, confira algumas lições que aprendemos ao longo do caminho e que podem ser úteis.

As válvulas de segurança podem causar mais danos do que benefícios

Em uma versão inicial do componente de imagem do Next.js, fornecemos um atributo unsized que permitia aos desenvolvedores ignorar o requisito de dimensionamento e usar imagens com dimensões não especificadas. Achamos que isso seria necessário em casos em que era impossível saber a altura ou a largura da imagem com antecedência. No entanto, notamos que os usuários recomendam o atributo unsized em problemas do GitHub como uma solução abrangente para problemas com o requisito de dimensionamento, mesmo em casos em que eles poderiam resolver o problema de maneiras que não piorassem o CLS. Depois, descontinuamos e removemos o atributo unsized.

Separe o atrito útil do incômodo desnecessário

O requisito de dimensionamento de uma imagem é um exemplo de "fricção útil". Isso restringe o uso do componente, mas oferece benefícios de desempenho muito maiores em troca. Os usuários vão aceitar a restrição se tiverem uma ideia clara dos possíveis benefícios de performance. Portanto, vale a pena explicar essa troca na documentação e em outros materiais publicados sobre o componente.

No entanto, é possível encontrar soluções alternativas para esse atrito sem sacrificar a performance. Por exemplo, durante o desenvolvimento do componente de imagem do Next.js, recebemos reclamações de que era irritante procurar tamanhos de imagens armazenadas localmente. Adicionamos importações de imagens estáticas, que simplificam esse processo recuperando automaticamente as dimensões de imagens locais no tempo de build usando um plug-in do Babel.

Encontre um equilíbrio entre recursos de conveniência e otimizações de desempenho

Se o componente de imagem não fizer nada além de impor "atrito útil" aos usuários, os desenvolvedores não vão querer usá-lo. Descobrimos que, embora os recursos de performance, como dimensionamento de imagens e geração automática de valores srcset, sejam os mais importantes. Recursos convenientes para desenvolvedores, como o carregamento lento automático e espaços reservados desfocados integrados, também impulsionaram o interesse no componente de imagem do Next.js.

Definir um plano para recursos que impulsionem a adoção

É muito difícil criar uma solução que funcione perfeitamente em todas as situações. Pode ser tentador criar algo que funcione bem para 75% das pessoas e dizer aos outros 25% que "nesses casos, este componente não é para você".

Na prática, essa estratégia acaba sendo contrária às suas metas como designer de componentes. Você quer que os desenvolvedores adotem seu componente para aproveitar os benefícios de performance dele. Isso é difícil de fazer se houver um grupo de usuários que não consegue migrar e se sente excluído da conversa. É provável que eles expressem decepção, o que leva a percepções negativas que afetam a adoção.

É recomendável ter um roadmap para seu componente que abranja todos os casos de uso razoáveis a longo prazo. Também é útil ser explícito na documentação sobre o que não é compatível e por quê, para definir expectativas sobre os problemas que o componente pretende resolver.

Conclusão

O uso e a otimização de imagens são complicados. Os desenvolvedores precisam encontrar o equilíbrio entre a performance e a qualidade das imagens, garantindo uma ótima experiência do usuário. Isso torna a otimização de imagens uma tarefa de alto custo e alto impacto.

Em vez de cada app reinventar a roda a cada vez, criamos um modelo de práticas recomendadas que desenvolvedores, frameworks e outras pilhas de tecnologia podem usar como referência para as próprias implementações. Essa experiência será valiosa à medida que oferecermos suporte a outros frameworks nos componentes de imagem deles.

O componente Image do Next.js melhorou os resultados de desempenho em aplicativos Next.js, aprimorando a experiência do usuário. Acreditamos que é um ótimo modelo que funcionaria bem no ecossistema mais amplo, e gostaríamos de saber de desenvolvedores que queiram adotar esse modelo nos projetos deles.