Um componente de imagem encapsula as práticas recomendadas de desempenho e oferece uma solução pronta para otimizar imagens.
As imagens são uma fonte comum de gargalos de performance para aplicativos da Web e uma área de foco importante para otimização. Imagens não otimizadas contribuem para o inchaço da página e representam mais de 70% do peso total da página em bytes no percentil 90th. Várias maneiras de otimizar imagens exigem um "componente de imagem" inteligente com soluções de desempenho integradas por 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 outras estruturas, sistemas de gerenciamento de conteúdo (CMS) e pilhas de tecnologias. Colaboramos em um componente semelhante para o 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.
Problemas e oportunidades de otimização de imagens
As imagens afetam não apenas a performance, 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 converteram tiveram 38% menos imagens do que as sessões em que não converteram. O Lighthouse lista várias oportunidades para otimizar imagens e melhorar as métricas 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 prejudicam a CLS
Imagens exibidas sem que o tamanho especificado seja especificado podem causar instabilidade no layout e contribuir para uma alta Cumulative Layout Shift (CLS). Definir os atributos width
e height
nos 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 da proporção natural. Uma diferença significativa na proporção pode distorcer a imagem. Uma propriedade relativamente nova que permite especificar aspect-ratio no CSS pode ajudar a dimensionar as imagens de forma responsiva e, ao mesmo tempo, evitar CLS.
Imagens grandes podem prejudicar a LCP
Quanto maior o tamanho do arquivo de uma imagem, mais tempo levará 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 demora muito para ser transferida atrasará o 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 imagem certa dependendo do tamanho e da resolução da tela.
A compactação de imagem ruim pode prejudicar o 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, mantendo a mesma qualidade da imagem. Essa redução leva a downloads mais rápidos com menos consumo de dados. O app precisa veicular formatos de imagem modernos para navegadores compatíveis com esses formatos.
O carregamento de 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 que não contribuam para o LCP e o atrasem. O carregamento lento pode ser usado para carregar essas imagens mais tarde, conforme o usuário rola a página.
Desafios de otimização
As equipes podem avaliar o custo de desempenho devido aos problemas listados anteriormente e implementar as práticas recomendadas para superá-los. No entanto, isso geralmente não acontece na prática, e as imagens ineficientes continuam a desacelerar a Web. Alguns possíveis motivos para isso:
- Prioridades: os desenvolvedores da Web geralmente se concentram em código, JavaScript e otimização de dados. Assim, eles podem não estar cientes dos problemas com as imagens ou de 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 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 provenientes de bancos de dados ou CMSs externos. Pode ser difícil definir o tamanho dessas imagens em que a origem é dinâmica.
- Marcação excessiva: as 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 atributosrcset
foi introduzido em 2014, mas é usado por apenas 26,5% dos sites atualmente. Ao usarsrcset
, os desenvolvedores precisam criar imagens em vários tamanhos. Ferramentas como o just-gimme-an-img podem ajudar, mas precisam ser usadas manualmente para cada imagem. - Suporte do navegador: formatos de imagem modernos, como AVIF e WebP, criam arquivos de imagem menores, mas precisam de um tratamento especial em navegadores que não oferecem suporte a eles. Os desenvolvedores precisam usar estratégias como a negociação de conteúdo ou o elemento
<picture
> para que as imagens sejam veiculadas para 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. Os desenvolvedores também podem não saber a melhor distância da "dobra" para carregar imagens adiadas. Tamanhos diferentes de janelas de visualização em dispositivos podem complicar ainda mais isso.
- Mudança de cenário: à medida que os navegadores começam a oferecer suporte a novos recursos de HTML ou CSS para melhorar o desempenho, 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. Pode ser usado para aumentar a prioridade de imagens específicas na página. No geral, seria mais fácil para os desenvolvedores 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 para 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 da imagem.
No ano passado, trabalhamos com a estrutura Next.js para projetar e implementar o componente de imagem. Ele pode ser usado como uma substituição para os elementos <img>
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 de recursos e princípios. Ele também inclui opções que permitem aos desenvolvedores personalizar 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 no layout. Se o tamanho for desconhecido, os desenvolvedores precisarão especificar layout=fill
para veicular uma imagem sem tamanho dentro de um contêiner de tamanho. Como alternativa, use importações de imagem estática para recuperar o tamanho da imagem real no disco rígido no momento da criação 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 Image sem tamanho, o design garante que eles considerem o tamanho da imagem e evitem 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 do Next.js para definir os valores de atributo apenas uma vez por aplicativo. Eles são aplicados a todas as instâncias do componente de imagem com base no modo de layout. Criamos uma solução com três partes:
- Propriedade
deviceSizes
: essa propriedade pode ser usada para configurar pontos de interrupção únicos com base nos dispositivos comuns à base de usuários do app. Os valores padrão para pontos de interrupção estão incluídos no arquivo de configuração. - 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. - Atributo
layout
em cada imagem: usado para indicar como usar as propriedadesdeviceSizes
eimageSizes
para cada imagem. Os valores aceitos para o modo de layout sãofixed
,fill
,intrinsic
eresponsive
.
Quando uma imagem é solicitada com os modos de layout responsivo ou preenchimento, o Next.js identifica a imagem a ser veiculada com base no tamanho do dispositivo que solicita a página e define o srcset
e o 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 nos documentos do Next.js, visualizada em um smartphone e um laptop padrão.
Tela de laptop | Tela do smartphone |
---|---|
Layout = Intrinsic: é dimensionado para caber na largura do contêiner em janelas de visualização menores. Não aumenta além do tamanho intrínseco da imagem em uma janela de visualização maior. A largura do contêiner está em 100% | |
Layout = Fixo: a imagem não é responsiva. A largura e a altura são fixadas de forma semelhante ao elemento , independentemente do dispositivo em que ele é renderizado. | |
Layout = responsivo: reduza ou aumente a escala dependendo da largura do contêiner em diferentes janelas de visualização, mantendo a proporção. | |
Layout = preenchimento: largura e altura esticadas para preencher o contêiner pai. (A largura da <div> pai está definida como 300*500 neste exemplo)
|
|
Oferecer carregamento lento integrado
O componente Image oferece uma solução integrada e eficiente de carregamento lento (em inglês) por 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 uma implementação cuidadosos. Os desenvolvedores nem sempre têm tempo para isso.
- Importar uma biblioteca de terceiros para carregar imagens lentamente: pode ser necessário mais esforço para avaliar e integrar uma biblioteca de terceiros adequada para o 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, disponível na maioria dos navegadores modernos. Os desenvolvedores não precisam fazer nada extra para ativar esse recurso, mas podem desativá-lo quando necessário.
Pré-carregar imagens importantes
Muitas vezes, os elementos de LCP são imagens, e imagens grandes podem atrasar a LCP. É recomendável pré-carregar imagens essenciais para que o navegador possa detectá-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 do framework usado. No caso do componente de imagem do Next.js, os desenvolvedores podem indicar uma imagem que é um bom candidato para pré-carregar 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
Os CDNs de imagens são recomendados para automatizar a otimização de imagens e também oferecem suporte a formatos de imagem modernos, como WebP e AVIF. O componente de imagem do Next.js usa um CDN de imagem por padrão com uma arquitetura de loader. O exemplo a seguir mostra que o loader permite a configuração do 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 do CDN para gerar o URL absoluto. CDNs de imagem conhecidos, como Imgix, Cloudinary e Akamai, são compatíveis. A arquitetura oferece suporte ao 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 ser compatível com imagens auto-hospedadas. O componente de imagem do Next.js usa um otimizador de imagem como um servidor de imagem integrado que fornece uma API semelhante a um CDN. O otimizador usa o Sharp para transformações de imagens de produção se ele estiver instalado no servidor. Essa biblioteca é uma boa escolha 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, mostrando uma imagem de marcador de posição geralmente de qualidade significativamente menor enquanto a imagem real é carregada. Ele melhora a performance percebida e a experiência do usuário. Ele pode ser usado em combinação com o carregamento lento para imagens abaixo ou acima do limite.
O componente de imagem do Next.js oferece suporte ao carregamento progressivo da imagem usando a propriedade placeholder. Isso pode ser usado como um marcador de posição de imagem de baixa qualidade (LQIP, na sigla em inglês) para exibir 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 na produção e também estamos trabalhando com outras tecnologias em componentes de imagem semelhantes.
Quando a Leboncoin migrou o front-end JavaScript legado para a Next.js, ela também atualizou o pipeline de imagem para usar o componente Next.js. Em uma página que migrou de <img>
para a próxima/imagem, o LCP caiu de 2,4 para 1,7 segundos. O total de bytes de imagem transferidos para a página passou de 663 kB para 326 kB (com cerca de 100 kB de bytes de imagem carregados com atraso).
Lições aprendidas
Qualquer pessoa que crie um app Next.js pode se beneficiar do uso do componente de imagem 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 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 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 fosse impossível saber a altura ou a largura da imagem com antecedência. No entanto, notamos que os usuários recomendavam o atributo unsized
em problemas do GitHub como uma solução geral para problemas com o requisito de dimensionamento, mesmo em casos em que eles podiam resolver o problema de maneiras que não pioravam o CLS. Descontinuamos e removemos o atributo unsized
.
Separe a fricção útil da irritação inútil
A necessidade de dimensionar uma imagem é um exemplo de "fricção útil". Ele restringe o uso do componente, mas oferece benefícios de desempenho muito grandes em troca. Os usuários vão aceitar a restrição com facilidade se tiverem uma ideia clara dos possíveis benefícios de performance. Portanto, vale a pena explicar essa compensação na documentação e em outros materiais publicados sobre o componente.
No entanto, é possível encontrar soluções alternativas para essa dificuldade 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 das imagens locais no momento do build usando um plug-in do Babel.
Encontre o 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 "fricção útil" aos usuários, os desenvolvedores não vão querer usá-lo. Descobrimos que, embora recursos de desempenho como o dimensionamento de imagens e a geração automática de valores srcset
fossem os mais importantes, Recursos de conveniência para desenvolvedores, como o carregamento lento automático e os marcadores de posição desfocados integrados, também geraram interesse no componente de imagem do Next.js.
Definir um cronograma 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 "nestes casos, esse componente não é para você".
Na prática, essa estratégia acaba conflitando com suas metas 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 conseguem migrar e se sentem excluídos 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 cronograma para o 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 tem suporte e por que, para definir as 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 toda vez, criamos um modelo de práticas recomendadas que desenvolvedores, frameworks e outras tecnologias podem usar como referência para as próprias implementações. Essa experiência vai ser valiosa, já que oferecemos suporte a outros frameworks nos componentes de imagem.
O componente de imagem Next.js melhorou os resultados de performance dos aplicativos Next.js, aprimorando a experiência do usuário. Acreditamos que esse é um ótimo modelo que funcionaria bem no ecossistema mais amplo. Gostaríamos de saber dos desenvolvedores que gostariam de adotar esse modelo nos projetos.