Sintaxes descritivas

Neste módulo, você aprenderá a permitir que o navegador escolha imagens para que ele tome as melhores decisões sobre o que exibir. srcset não é um método para trocar origens de imagem em pontos de interrupção específicos e não foi criado para trocar uma imagem por outra. Essas sintaxes permitem que o navegador resolva um problema muito difícil, independente de nós: solicitar e renderizar uma origem de imagem personalizada de acordo com o contexto de navegação do usuário. Isso inclui o tamanho da janela de visualização, a densidade de exibição, as preferências do usuário, a largura de banda e inúmeros outros fatores.

Essa é uma grande solicitação. Certamente mais do que queremos considerar quando estamos simplesmente marcando uma imagem para a Web, e fazer isso bem envolve mais informações do que podemos acessar.

Descrever a densidade com x

Uma <img> com largura fixa ocupará a mesma quantidade da janela de visualização em qualquer contexto de navegação, independentemente da densidade da tela do usuário, ou seja, do número de pixels físicos que compõem a tela. Por exemplo, uma imagem com largura inerente de 400px ocupará quase toda a janela de visualização do navegador no Google Pixel original e no Pixel 6 Pro, muito mais recente. Os dois dispositivos têm uma janela de visualização larga de 412px pixel lógico normalizada.

No entanto, o Pixel 6 Pro tem uma tela muito mais nítida: o 6 Pro tem uma resolução física de 1.440 x 3.120 pixels, enquanto o Pixel tem 1.080 x 1.920 pixels, ou seja, o número de pixels de hardware que compõem a tela.

A proporção entre os pixels lógicos e físicos de um dispositivo é a proporção de pixels do dispositivo para essa tela (DPR). A DPR é calculada dividindo a resolução real da tela do dispositivo pelos pixels CSS da janela de visualização.

Um DPR de 2 exibido em uma janela do console.

Assim, o Pixel original tem uma DPR de 2,6, enquanto o Pixel 6 Pro tem uma DPR de 3,5.

O iPhone 4, o primeiro dispositivo com uma DPR maior que 1, informa uma proporção de pixels de dispositivo de 2. A resolução física da tela é o dobro da resolução lógica. Qualquer dispositivo anterior ao iPhone 4 tinha uma DPR de 1: um pixel lógico para um pixel físico.

Se você visualizar essa imagem com largura de 400px em uma tela com uma DPR de 2, cada pixel lógico vai ser renderizado em quatro dos pixels físicos da tela: dois horizontais e dois verticais. A imagem não se beneficia da tela de alta densidade. Ela terá a mesma aparência que seria em uma tela com uma DPR de 1. Obviamente, tudo o que for "desenhado" pelo mecanismo de renderização do navegador (texto, formas CSS ou SVGs, por exemplo) será desenhado para se adequar à exibição de densidade mais alta. No entanto, como você aprendeu em Formatos de imagem e compactação, imagens rasterizadas são grades fixas de pixels. Embora nem sempre seja óbvio, uma imagem de varredura aprimorada para se adequar a uma tela de densidade mais alta terá resolução baixa em comparação com a página ao redor.

Para evitar esse aumento da escala, a imagem renderizada precisa ter uma largura intrínseca de pelo menos 800 pixels. Quando reduzida para caber em um espaço em um layout de 400 pixels de largura, essa origem de imagem de 800 pixels tem o dobro da densidade de pixels. Em uma tela com DPR de 2, ela terá uma aparência bonita e nítida.

Close de uma pétala de flor mostrando disparidade de densidade.

Como uma tela com uma DPR de 1 não pode usar o aumento da densidade de uma imagem, ela será reduzida para corresponder à tela e, como você sabe, uma imagem reduzida não terá problemas. Em uma tela de baixa densidade, uma imagem adequada para telas de densidade maior será semelhante a qualquer outra imagem de baixa densidade.

Como você aprendeu em Imagens e desempenho, um usuário com uma tela de baixa densidade que visualiza uma fonte de imagem reduzida para 400pxprecisa de uma fonte com uma largura inerente de 400px. Embora uma imagem muito maior funcione para todos os usuários, uma fonte de imagem enorme de alta resolução renderizada em uma tela pequena e de baixa densidade se parece com qualquer outra imagem pequena e de baixa densidade, mas parece muito mais lenta.

Como você pode imaginar, os dispositivos móveis com uma DPR de 1 são extremamente raros, embora ainda sejam comuns em contextos de navegação em "computadores". De acordo com dados compartilhados por Matt Hobbs, aproximadamente 18% das sessões de navegação do GOV.UK em novembro de 2022 informaram uma DPR de 1. Embora as imagens de alta densidade tenham a aparência desses usuários, elas têm um custo de largura de banda e processamento muito maior, o que é uma preocupação especial para os usuários de dispositivos mais antigos e menos potentes, que provavelmente ainda têm telas de baixa densidade.

O uso de srcset garante que apenas dispositivos com telas de alta resolução recebam origens de imagem grandes o suficiente para parecerem nítidas, sem transmitir o mesmo custo de largura de banda aos usuários com telas de resolução mais baixa.

O atributo srcset identifica um ou mais candidatos separados por vírgula para renderizar uma imagem. Cada candidato é composto de duas coisas: um URL, exatamente como você usaria em src, e uma sintaxe que descreve essa origem da imagem. Cada candidato em srcset é descrito pela largura inerente ("sintaxe w") ou pela densidade pretendida ("sintaxe x").

A sintaxe x é uma abreviação para "esta fonte é adequada para uma tela com essa densidade". Um candidato seguido por 2x é adequado para uma tela com uma DPR de 2.

<img src="low-density.jpg" srcset="double-density.jpg 2x" alt="...">

Navegadores com suporte a srcset vão ter dois candidatos: double-density.jpg, que 2x descreve como apropriado para telas com um DPR de 2, e low-density.jpg no atributo src. O candidato selecionado se nada mais apropriado for encontrado em srcset. Para navegadores sem suporte a srcset, o atributo e o conteúdo dele serão ignorados. O conteúdo de src será solicitado, como de costume.

É fácil confundir os valores especificados no atributo srcset com as instruções. Esse 2x informa ao navegador que o arquivo de origem associado seria adequado para uso em uma tela com um DPR de 2, ou seja, informações sobre a própria fonte. Ele não informa ao navegador como usar essa fonte, apenas informa como ela pode ser usada. É uma distinção sutil, mas importante: essa é uma imagem de dupla densidade, não uma imagem para uso em uma tela de dupla densidade.

A diferença entre uma sintaxe que diz "esta fonte é adequada para telas 2x" e uma que diz "use essa fonte em telas 2x" é pequena na impressão, mas a densidade de exibição é apenas um dos muitos fatores interligados que o navegador usa para decidir qual candidato será renderizado, e somente alguns desses fatores podem ser conhecidos. Por exemplo: individualmente, é possível determinar que um usuário ativou uma preferência de navegador de economia de largura de banda na consulta de mídia prefers-reduced-data e usar essa opção para sempre ativar imagens de baixa densidade, independente da densidade de exibição. No entanto, a menos que isso seja implementado de forma consistente, por todos os desenvolvedores, em todos os sites, isso não seria muito útil para um usuário. Eles podem ter sua preferência respeitada em um local e se deparar com uma parede de imagens que elimina a largura de banda no próximo.

O algoritmo de seleção de recursos deliberadamente vago usado por srcset/sizes deixa espaço para que os navegadores decidam selecionar imagens de densidade mais baixa com quedas na largura de banda ou com base na preferência de minimizar o uso de dados, sem que nos responsabilizamos por como, quando ou em que limite. Não faz sentido assumir responsabilidades e ter mais trabalho de que o navegador esteja mais bem equipado para lidar com você.

Descrever larguras com w

srcset aceita um segundo tipo de descritor para candidatos a origem da imagem. Ele é muito mais eficiente e, para nossos objetivos, é muito mais fácil de entender. Em vez de sinalizar um candidato com as dimensões adequadas para uma determinada densidade de exibição, a sintaxe w descreve a largura inerente de cada origem candidata. Novamente, cada candidato é salvo em relação às dimensões: o mesmo conteúdo, o mesmo corte e a mesma proporção. No entanto, nesse caso, você quer que o navegador do usuário escolha entre duas opções: small.jpg, uma origem com largura inerente de 600 px, e grande.jpg, uma fonte com largura inerente de 1.200 px.

srcset="small.jpg 600w, large.jpg 1200w"

O navegador não diz o que fazer com essas informações, apenas fornece uma lista de candidatos para exibir a imagem. Antes que o navegador decida sobre a origem da renderização, você precisa fornecer algumas informações a mais: uma descrição de como a imagem será renderizada na página. Para fazer isso, use o atributo sizes.

Descrevendo o uso com sizes

Os navegadores são muito eficientes na transferência de imagens. As solicitações de recursos de imagem são iniciadas muito antes das solicitações de folhas de estilo ou JavaScript, geralmente mesmo antes de a marcação ser totalmente analisada. Quando o navegador faz essas solicitações, ele não tem informações sobre a página em si, além da marcação. É possível que ele ainda não tenha iniciado solicitações de folhas de estilo externas, muito menos as aplicadas. No momento em que o navegador analisa sua marcação e começa a fazer solicitações externas, ele só tem informações no nível do navegador: o tamanho da janela de visualização, a densidade de pixels da tela, as preferências do usuário e assim por diante.

Isso não nos diz nada sobre como uma imagem deve ser renderizada no layout da página. Ela não pode nem usar a janela de visualização como proxy para o limite superior do tamanho da img, já que ela pode ocupar um contêiner de rolagem horizontal. Precisamos fornecer essas informações ao navegador, usando marcação. Isso é tudo o que poderemos usar para essas solicitações.

Assim como srcset, o objetivo do sizes é disponibilizar informações sobre uma imagem assim que a marcação é analisada. Assim como o atributo srcset é a abreviação de "Aqui estão os arquivos de origem e os tamanhos inerentes deles", o atributo sizes é uma abreviação para "aqui está o tamanho da imagem renderizada no layout". A forma de descrever a imagem é relativa à janela de visualização. Novamente, o tamanho da janela de visualização é a única informação de layout que o navegador tem quando a solicitação de imagem é feita.

Isso pode parecer um pouco complicado na mídia impressa, mas é muito mais fácil de entender na prática:

<img
 sizes="80vw"
 srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
 src="fallback.jpg"
 alt="...">

Aqui, esse valor de sizes informa ao navegador que o espaço no layout que img ocupa tem uma largura de 80vw a 80% da janela de visualização. Isso não é uma instrução, mas uma descrição do tamanho da imagem no layout da página. Ele não diz "faça com que essa imagem ocupe 80% da janela de visualização", mas "ela vai ocupar 80% da janela de visualização quando a página for renderizada".

Como desenvolvedor, seu trabalho está feito. Você descreveu com precisão uma lista de origens candidatas em srcset e a largura da sua imagem em sizes. Assim como na sintaxe x em srcset, o restante depende do navegador.

Mas, para entender totalmente como essas informações são usadas, vamos analisar as decisões que o navegador do usuário toma ao encontrar essa marcação:

Você informou ao navegador que essa imagem ocupará 80% da janela de visualização disponível. Portanto, se renderizarmos essa img em um dispositivo com uma janela de visualização de 1.000 pixels de largura, essa imagem ocupará 800 pixels. O navegador usa esse valor e o divide pelas larguras de cada um dos candidatos de origem de imagem que especificamos em srcset. A menor origem tem um tamanho inerente de 600 pixels, então: 600÷800=0,75. Nossa imagem média tem 1.200 pixels de largura: 1.200 ÷ 800=1,5. A maior imagem tem 2.000 pixels de largura: 2.000÷800=2,5.

Os resultados desses cálculos (.75, 1.5 e 2.5) são, na verdade, opções de DPR adaptadas especificamente ao tamanho da janela de visualização do usuário. Como o navegador também tem informações sobre a densidade de exibição do usuário, ele toma uma série de decisões:

Nesse tamanho da janela de visualização, o candidato small.jpg é descartado, independente da densidade de exibição do usuário. Com uma DPR calculada menor que 1, essa origem exigiria um aumento da escala para qualquer usuário, então não é apropriada. Em um dispositivo com uma DPR de 1, medium.jpg fornece a correspondência mais próxima: essa fonte é apropriada para exibição em um DPR de 1.5, por isso é um pouco maior que o necessário, mas lembre-se de que a redução é um processo visualmente integrado. Em um dispositivo com uma DPR de 2,large.jpg é a correspondência mais próxima, por isso ela é selecionada.

Se a mesma imagem fosse renderizada em uma janela de visualização de 600 pixels de largura, o resultado seria completamente diferente: 80 vw agora é 480 pixels. Quando dividimos as larguras das origens, o resultado foi 1.25, 2.5 e 4.1666666667. Nesse tamanho da janela de visualização, small.jpg será escolhido em dispositivos 1x, e medium.jpg será escolhido em dispositivos 2x.

Essa imagem será idêntica em todos esses contextos de navegação: todos os nossos arquivos de origem são exatamente iguais, exceto pelas dimensões, e cada um é renderizado com a maior nitidez que a densidade de exibição do usuário permite. No entanto, em vez de exibir large.jpg para todos os usuários para acomodar as maiores janelas de visualização e as telas de maior densidade, sempre será exibido o menor candidato adequado. Com uma sintaxe descritiva em vez de uma sintaxe prescritiva, não é necessário definir pontos de interrupção manualmente e considerar futuras janelas de visualização e DPRs. Basta fornecer informações ao navegador e permitir que ele determine as respostas para você.

Como o valor de sizes é relativo à janela de visualização e totalmente independente do layout da página, ele adiciona uma camada de complicação. É raro ter uma imagem que ocupe apenas uma porcentagem da janela de visualização, sem margens de largura fixa, padding ou influência de outros elementos na página. Com frequência, você precisará expressar a largura de uma imagem usando uma combinação de unidades, como porcentagens, em, px e assim por diante.

Felizmente, você pode usar calc() aqui. Qualquer navegador com suporte nativo para imagens responsivas também será compatível com calc(), permitindo misturar e corresponder unidades CSS. Por exemplo, uma imagem que ocupa toda a largura da janela de visualização do usuário, menos uma margem de 1em em ambos os lados:

<img
    sizes="calc(100vw-2em)"
    srcset="small.jpg 400w, medium.jpg 800w, large.jpg 1600w, x-large.jpg 2400w"
    src="fallback.jpg"
    alt="...">

Como descrever pontos de interrupção

Se você passou muito tempo trabalhando com layouts responsivos, provavelmente percebeu que falta algo nesses exemplos: o espaço que uma imagem ocupa em um layout muito provavelmente muda nos pontos de interrupção do layout. Nesse caso, é necessário transmitir um pouco mais de detalhes para o navegador: sizes aceita um conjunto de candidatos separados por vírgula para o tamanho renderizado da imagem, assim como srcset aceita candidatos separados por vírgula para origens de imagem. Essas condições usam a conhecida sintaxe de consulta de mídia. Essa sintaxe é de primeira correspondência: assim que uma condição de mídia é correspondente, o navegador para de analisar o atributo sizes e o valor especificado é aplicado.

Digamos que você tenha uma imagem para ocupar 80% da janela de visualização, menos um em de padding em ambos os lados, em janelas acima de 1.200 px. Em janelas menores, ela ocupa toda a largura da janela.

  <img
     sizes="(min-width: 1200px) calc(80vw - 2em), 100vw"
     srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
     src="fallback.jpg"
     alt="...">

Se a janela de visualização do usuário for maior que 1.200 px, calc(80vw - 2em) descreve a largura da imagem no nosso layout. Se a condição (min-width: 1200px) não for correspondente, o navegador passará para o próximo valor. Como não há uma condição de mídia específica vinculada a esse valor, 100vw é usado como padrão. Se você escrevesse esse atributo sizes usando consultas de mídia max-width:

  <img
     sizes="(max-width: 1200px) 100vw, calc(80vw - 2em)"
     srcset="small.jpg 600w, medium.jpg 1200w, large.jpg 2000w"
     src="fallback.jpg"
     alt="...">

Em uma linguagem simples: "(max-width: 1200px) corresponde? Caso contrário, siga em frente. O próximo valor (calc(80vw - 2em)) não tem condição de qualificação, então este é o selecionado.

Agora que você forneceu ao navegador todas essas informações sobre seu elemento img (possíveis fontes, larguras inerentes e como você pretende apresentar a imagem ao usuário), o navegador usa um conjunto impreciso de regras para determinar o que fazer com essas informações. Se isso soa vago, bem, é porque é, por design. O algoritmo de seleção de origem codificado na especificação HTML é explicitamente vago sobre como uma fonte precisa ser escolhida. Depois de analisar as fontes, os descritores e a forma como a imagem será renderizada, o navegador fica livre para fazer o que quiser. Não é possível saber com certeza qual fonte ele vai escolher.

Uma sintaxe que diz "usar essa fonte em uma tela de alta resolução" seria previsível, mas não resolveria o problema principal com imagens em um layout responsivo: conservar a largura de banda do usuário. A densidade de pixels de uma tela é apenas tangencialmente relacionada à velocidade da conexão com a Internet, se for o caso. Se você estiver usando um laptop de ponta, mas navegando na Web com uma conexão limitada, conectado ao smartphone ou usando uma conexão Wi-Fi instável de avião, convém desativar as fontes de imagem de alta resolução, independentemente da qualidade da tela.

Deixar a palavra final para o navegador possibilita muito mais melhorias de desempenho do que poderíamos gerenciar com uma sintaxe estritamente prescritiva. Por exemplo: na maioria dos navegadores, um img que usa a sintaxe srcset ou sizes nunca vai solicitar uma origem com dimensões menores do que aquela que o usuário já tem no cache do navegador. Qual seria o sentido de fazer uma nova solicitação de uma origem que pareceria idêntica, quando o navegador pode reduzir sem problemas a origem da imagem que já tem? No entanto, se o usuário dimensionar a janela de visualização até o ponto em que uma nova imagem seja necessária para evitar o aumento da escala, essa solicitação ainda será feita. Assim, tudo terá a aparência esperada.

Essa falta de controle explícito pode parecer um pouco assustadora, mas, como você está usando arquivos de origem com conteúdo idêntico, é provável que não ofereçamos aos usuários uma experiência "corrompida" do que teríamos com um src de fonte única, independentemente das decisões tomadas pelo navegador.

Como usar sizes e srcset

São muitas informações, tanto para você, como o leitor, quanto para o navegador. srcset e sizes são sintaxes densas, descrevendo uma quantidade chocante de informações em relativamente poucos caracteres. Ou seja, para o bem ou para o lado, desde o design, tornar essas sintaxes menos concisas e analisadas mais facilmente por nós, humanos, poderia tê-las tornado mais difíceis de analisar para um navegador. Quanto mais complexidade for adicionada a uma string, maior será o potencial de erros de analisador ou de diferenças não intencionais no comportamento de um navegador para outro. No entanto, há uma vantagem: uma sintaxe lida mais facilmente pelas máquinas é uma sintaxe escrita mais facilmente por elas.

srcset é um caso claro para automação. É raro você criar manualmente várias versões das imagens para um ambiente de produção, automatizando o processo usando um executor de tarefas como Gulp, um bundler como Webpack, uma CDN de terceiros, como Cloudinary, ou funcionalidade já integrada ao CMS escolhido. Com informações suficientes para gerar as fontes, o sistema teria informações suficientes para gravá-las em um atributo srcset viável.

sizes é um pouco mais difícil de automatizar. Como você sabe, a única maneira de um sistema calcular o tamanho de uma imagem em um layout renderizado é renderizando o layout. Felizmente, várias ferramentas para desenvolvedores surgiram para abstrair o processo de escrever atributos sizes à mão, com uma eficiência que você nunca conseguiria fazer manualmente. O respImageLint, por exemplo, é um snippet de código destinado a verificar a precisão dos atributos sizes e fornecer sugestões de melhoria. O projeto Lazysizes compromete um pouco de velocidade para aumentar a eficiência, adiando solicitações de imagem até que o layout seja estabelecido, permitindo que o JavaScript gere valores sizes para você. Se você estiver usando um framework de renderização totalmente do lado do cliente, como React ou Vue, há várias soluções para criar e/ou gerar atributos srcset e sizes, que vamos discutir mais em CMS e frameworks.