Como criar um componente de imagem eficaz

Um componente de imagem encapsula as práticas recomendadas de desempenho 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 desempenho para aplicativos da Web e uma área de foco importante para a otimização. Imagens não otimizadas contribuem para a ocupação excessiva da página e atualmente representam mais de 70% do peso total da página em bytes no 90o percentil. Diversas maneiras de otimizar imagens exigem um "componente de imagem" inteligente com soluções de desempenho incorporadas como padrão.

A equipe do Aurora trabalhou com a 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 (CMS) e conjuntos de tecnologias. 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 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 não só afetam a performance, mas também os negócios. O número de imagens em uma página foi o segundo maior preditor de conversões de usuários que acessavam 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 as Web Vitals como parte da auditoria de práticas recomendadas. Confira abaixo algumas das áreas comuns em que as imagens podem afetar as Core Web Vitals e a experiência do usuário.

Imagens sem tamanho prejudicam a CLS

Imagens veiculadas sem o tamanho especificado podem causar instabilidade no layout e contribuir para uma alta Mudança de layout cumulativa (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 forma que a proporção da imagem renderizada fique próxima à proporção natural. Uma diferença significativa na proporção pode fazer com que a imagem pareça distorcida. Uma propriedade relativamente nova que permite especificar a proporção no CSS pode ajudar a dimensionar imagens de forma responsiva e evitar a CLS.

Imagens grandes podem prejudicar a LCP

Quanto maior o tamanho do arquivo de uma imagem, mais tempo levará para o download. 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 a Maior exibição de conteúdo (LCP). Uma imagem que faz parte do conteúdo essencial e que leva muito tempo para ser baixada vai atrasar a LCP.

Em muitos casos, os desenvolvedores podem reduzir o tamanho 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 baixa compactação de imagens 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 mais usados. Uma melhor compactação reduz o tamanho do arquivo em 25% a 50% em alguns casos para 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 compatíveis com esses formatos.

Carregar imagens desnecessárias prejudica a LCP

As imagens abaixo da dobra ou fora da janela de visualização não são exibidas para o usuário quando a página é carregada. Eles podem ser adiados para que não contribuam para a LCP. O carregamento lento pode ser usado para carregar essas imagens mais tarde, à medida que o usuário rola até elas.

Desafios de otimização

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

  • Prioridades: os desenvolvedores da Web geralmente se concentram no código, em JavaScript e na otimização de dados. Por isso, talvez eles não estejam cientes dos problemas com as imagens ou de como otimizá-las. 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 completa e pronta para uso para o framework ou conjunto de tecnologias 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 coletadas de bancos de dados externos ou CMSs. Pode ser difícil definir o tamanho dessas imagens quando a origem delas é dinâmica.
  • Sobrecarga de marcação: soluções para incluir o tamanho da imagem ou srcset para tamanhos diferentes exigem marcação adicional para cada imagem, o que pode ser tedioso. O atributo srcset foi introduzido 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 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 de carregamento lento: há várias técnicas e bibliotecas disponíveis para implementar o carregamento lento em imagens abaixo da dobra. Escolher a melhor pode ser um desafio. Também é possível que os desenvolvedores não saibam a melhor distância da "dobra" para carregar imagens adiadas. Tamanhos diferentes de janela de visualização nos dispositivos podem complicar ainda mais esse processo.
  • Mudança do cenário: à medida que os navegadores começam a oferecer suporte a novos recursos HTML ou CSS para melhorar o desempenho, os desenvolvedores podem ter dificuldade para avaliar cada um deles. Por exemplo, o Chrome está lançando o recurso Buscar prioridade como um teste de origem. Ele pode ser usado para aumentar a prioridade de imagens específicas na página. Em 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 para 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 desempenho das imagens.

No ano passado, trabalhamos com o framework Next.js para projetar e implement o componente de imagem. Ele pode ser usado como uma substituição simples para os elementos <img> existentes nos apps Next.js, conforme mostrado a seguir.

// 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 à imagem de maneira genérica usando um amplo conjunto de recursos e princípios. Ele também inclui opções que permitem aos desenvolvedores personalizá-lo para vários requisitos de imagem.

Proteção contra mudanças de layout

Como discutido anteriormente, imagens não dimensionadas causam mudanças de layout e contribuem para a CLS. Ao usar o componente de imagem 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 não dimensionada que fica dentro de um contêiner dimensionado. Alternativamente, é possível usar importações de imagens estáticas para recuperar o tamanho da imagem real no disco rígido no tempo de compilação e incluí-la 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 definido, o design garante que eles levarão tempo para considerar o dimensionamento da imagem e evitar mudanças de layout.

Facilitar a capacidade de resposta

Para tornar as imagens responsivas em vários dispositivos, os desenvolvedores precisam definir os atributos srcset e sizes no elemento <img>. Queríamos reduzir esse esforço com o componente de imagem. Projetamos o componente de imagem Next.js para definir os valores do atributo apenas uma vez por aplicativo. Elas são aplicadas a todas as instâncias do componente de imagem com base no modo de layout. Criamos uma solução em três partes:

  1. Propriedade deviceSizes: essa propriedade 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 do tamanho do dispositivo.
  3. Atributo layout em cada imagem: usado para indicar como usar as propriedades deviceSizes e imageSizes em cada uma delas. Os valores compatíveis com o modo de layout são fixed, fill, intrinsic e responsive.

Quando uma imagem é solicitada com os modos de layout reply ou fill, 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 corretamente.

A comparação a seguir mostra como o modo de layout pode ser usado para controlar o tamanho da imagem em diferentes telas. Usamos uma imagem de demonstração compartilhada nos documentos do Next.js, exibida em um telefone e em um laptop padrão.

Tela de laptop Tela do smartphone
Layout = Intrínseco: reduz para se ajustar à largura do contêiner em janelas de visualização menores. Não dimensiona além do tamanho intrínseco da imagem em uma janela de visualização maior. A largura do contêiner está em 100%
Imagem das montanhas no estado em que se encontra Imagem de montanhas reduzida
Layout = Fixo: a imagem não é responsiva. A largura e a altura são fixas de maneira semelhante ao elemento "", independentemente do dispositivo em que são renderizados.
Imagem das montanhas no estado em que se encontra A imagem das montanhas não cabe na tela
Layout = Responsivo: reduza ou aumente de acordo com a largura do contêiner em diferentes janelas de visualização, mantendo a proporção.
Imagem de montanhas redimensionada para caber na tela Imagem de montanhas reduzida para caber na tela
Layout = Preenchimento: largura e altura ampliadas para preencher o contêiner pai. (Pai `
` 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

Oferecer carregamento lento integrado

O componente de imagem oferece uma solução de carregamento lento integrada e de alta performance como padrão. Ao usar o elemento <img>, há algumas opções nativas para carregamento lento, mas todas têm desvantagens que o tornam difíceis de usar. Um desenvolvedor pode adotar uma das seguintes abordagens de carregamento lento:

  • Especifique o atributo loading: ele é fácil de implementar, mas não tem suporte em alguns navegadores no momento.
  • Use a API Intersection Observer: criar uma solução personalizada de carregamento lento exige esforço, além de um design e uma implementação cuidadosos. Nem sempre os desenvolvedores têm tempo para isso.
  • Importar uma biblioteca de terceiros para o carregamento lento de imagens: pode ser necessário mais esforço para avaliar e integrar uma biblioteca de terceiros adequada para carregamento lento.

No componente de imagem Next.js, o carregamento é definido como "lazy" por padrão. O carregamento lento é implementado com o uso do Intersection Observer, que está disponível na maioria dos navegadores mais recentes. Os desenvolvedores não são obrigados a fazer nada extra para ativá-lo, mas podem desativá-lo quando necessário.

Pré-carregue imagens importantes

Muitas vezes, os elementos da LCP são imagens, e imagens grandes podem atrasar a LCP. É recomendável pré-carregar imagens críticas para que o navegador possa descobri-las mais rapidamente. 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 precisa oferecer uma maneira de ajustar a sequência de carregamento de imagens, independentemente da estrutura usada. No caso do componente de imagem Next.js, os desenvolvedores podem indicar uma imagem que seja uma boa candidata para pré-carregamento usando o atributo priority desse componente.

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

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

Incentive a hospedagem de imagens de alto desempenho

CDNs de imagem são recomendados para automatizar a otimização de imagens e também são compatíveis com formatos de imagem modernos, como WebP e AVIF. Por padrão, o componente de imagem Next.js usa uma CDN de imagem com uma arquitetura de carregador. O exemplo a seguir mostra que o carregador permite a configuração da CDN no arquivo de configuração 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 concatena o URL relativo com o caminho da CDN para gerar o URL absoluto. Há suporte para CDNs de imagens conhecidas, como Imgix, Cloudinary e Akamai. A arquitetura é compatível com o uso de um provedor de nuvem personalizado, implementando uma função loader personalizada para o app.

Suporte a imagens auto-hospedadas

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

Suporte ao carregamento progressivo

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

O componente de imagem Next.js é compatível com o carregamento progressivo da imagem por meio da propriedade placeholder. Isso pode ser usado como um LQIP (marcador de posição de imagem de baixa qualidade) para exibir uma imagem de baixa qualidade ou desfocada enquanto a imagem real é carregada.

Impacto

Com todas as otimizações acima incorporadas, tivemos sucesso com o componente de imagem Next.js na produção e também estamos trabalhando com outros conjuntos de tecnologias em componentes de imagem semelhantes.

Quando o Leboncoin migrou o front-end JavaScript legado para o Next.js, também atualizou o pipeline de imagem para usar o componente de imagem Next.js. Em uma página que migrou de <img> para "next/image", a LCP diminuiu de 2,4s para 1,7s. O total de bytes da imagem transferidos por download para a página passou de 663 KB para 326 KB (com aproximadamente 100 KB de bytes de imagem de carregamento lento).

Lições aprendidas

Qualquer pessoa que criar 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 desempenho semelhantes para outra estrutura ou CMS, as lições a seguir podem ser úteis.

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

Em uma versão inicial do componente de imagem Next.js, fornecemos um atributo unsized que permitiu aos desenvolvedores ignorar o requisito de dimensionamento e usar imagens com dimensões não especificadas. Achamos que isso seria necessário nos 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 nos problemas do GitHub como uma solução abrangente para questões de dimensionamento, mesmo nos casos em que podiam resolver o problema de maneiras que não piorassem a CLS. Posteriormente, descontinuamos e removemos o atributo unsized.

Separar o atrito útil do aborrecimento sem sentido

O requisito de dimensionar uma imagem é um exemplo de "atrito útil". Ela restringe o uso do componente, mas oferece grandes benefícios de desempenho em troca. Os usuários aceitarão prontamente a restrição se tiverem uma imagem clara dos possíveis benefícios de desempenho. Portanto, vale a pena explicar essa compensação na documentação e em outros materiais publicados sobre o componente.

No entanto, você pode encontrar soluções alternativas para esse atrito sem sacrificar o desempenho. Por exemplo, durante o desenvolvimento do componente de imagem Next.js, recebemos reclamações de que era irritante pesquisar tamanhos de imagens armazenadas localmente. Adicionamos importações de imagens estáticas, que simplificam esse processo recuperando automaticamente as dimensões das imagens locais no tempo de compilação 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 tenderão a não querer usá-lo. Descobrimos que os recursos de performance, como dimensionamento de imagem e geração automática de valores srcset, eram os mais importantes. Recursos de conveniência voltados para desenvolvedores, como carregamento lento automático e marcadores desfocados integrados, também geraram interesse no componente de imagem Next.js.

Definir um roteiro de recursos para impulsionar a adoção

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

Na prática, essa estratégia está em desacordo com seus objetivos como designer de componentes. Você quer que os desenvolvedores adotem seu componente para aproveitar os benefícios de desempenho. Isso é difícil de fazer se houver um contingente de usuários que não podem migrar e se sentirem excluídos da conversa. É provável que eles fiquem decepcionados, o que leva a percepções negativas que afetam a adoção.

É aconselhável ter um roteiro para seu componente que abranja todos os casos de uso razoáveis a longo prazo. Também ajuda a deixar claro na documentação o que não tem suporte e o motivo para definir as expectativas sobre os problemas que o componente vai resolver.

Conclusão

O uso e a otimização das imagens são complicados. Os desenvolvedores precisam encontrar o equilíbrio entre desempenho e qualidade das imagens, além de garantir uma ótima experiência do usuário. Isso torna a otimização de imagens um esforço de alto custo e alto impacto.

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

O componente de imagem Next.js melhorou os resultados de desempenho dos aplicativos Next.js, melhorando a experiência do usuário. Acreditamos que esse é um ótimo modelo que funcionaria bem no ecossistema em geral, e gostaríamos de ouvir os desenvolvedores que gostariam de adotar esse modelo nos projetos deles.