Um dos recursos do cenário atual de dispositivos é que há uma grande variedade de densidades de pixel de tela disponíveis. Alguns dispositivos têm telas de alta resolução, enquanto outros ficam para trás. Os desenvolvedores de aplicativos precisam oferecer suporte a uma variedade de densidades de pixel, o que pode ser bastante desafiador. Na Web para dispositivos móveis, os desafios são agravados por vários fatores:
- Grande variedade de dispositivos com diferentes formatos.
- Largura de banda de rede e duração da bateria restritas.
Em termos de imagens, o objetivo dos desenvolvedores de apps da Web é disponibilizar imagens da melhor qualidade da maneira mais eficiente possível. Este artigo vai abordar algumas técnicas úteis para fazer isso hoje e no futuro.
Evite imagens, se possível
Antes de abrir essa caixa de Pandora, lembre-se de que a Web tem muitas tecnologias poderosas que são em grande parte independentes de resolução e DPI. Especificamente, texto, SVG e grande parte do CSS "apenas funcionam" devido ao recurso de escalonamento automático de pixels da Web (usando devicePixelRatio).
No entanto, nem sempre é possível evitar imagens raster. Por exemplo, você pode receber recursos que seriam muito difíceis de replicar em SVG/CSS puro ou você está lidando com uma fotografia. Embora seja possível converter a imagem em SVG automaticamente, a vetorização de fotografias não faz muito sentido, porque as versões ampliadas geralmente não ficam boas.
Contexto
Um breve histórico da densidade de exibição
No início, as telas de computador tinham uma densidade de pixel de 72 ou 96 dpi (pontos por polegada).
As telas melhoraram gradualmente a densidade de pixels, principalmente devido ao caso de uso de dispositivos móveis, em que os usuários geralmente aproximam o smartphone do rosto, tornando os pixels mais visíveis. Em 2008, os smartphones de 150 dpi eram a nova norma. A tendência de aumento da densidade de tela continuou, e os novos smartphones atuais têm telas de 300 dpi (marca "Retina" da Apple).
O objetivo final, é claro, é uma tela em que os pixels são completamente invisíveis. Para o formato de smartphone, a geração atual de telas Retina/HiDPI pode estar próxima do ideal. No entanto, novas classes de hardware e wearables, como o Project Glass, provavelmente vão continuar aumentando a densidade de pixels.
Na prática, as imagens de baixa densidade precisam ter a mesma aparência nas novas telas que nas antigas, mas, em comparação com as imagens nítidas de alta densidade que os usuários estão acostumados a ver, as imagens de baixa densidade parecem irregulares e pixeladas. Confira a seguir uma simulação aproximada de como uma imagem 1x vai parecer em uma tela 2x. Em contraste, a imagem 2x parece muito boa.
Pixels na Web
Quando a Web foi projetada, 99% das telas tinham 96 dpi (ou pretendiam ser) e poucas disposições foram feitas para variações nesta área. Devido a uma grande variação nos tamanhos e densidades de tela, precisamos de uma maneira padrão para que as imagens ficassem bonitas em várias densidades e dimensões de tela.
A especificação HTML resolveu esse problema recentemente definindo um pixel de referência que os fabricantes usam para determinar o tamanho de um pixel CSS.
Usando o pixel de referência, um fabricante pode determinar o tamanho do pixel físico do dispositivo em relação ao pixel padrão ou ideal. Essa proporção é chamada de proporção de pixels do dispositivo.
Calculando a proporção de pixels do dispositivo
Suponha que um smartphone tenha uma tela com um tamanho físico de pixel de 180 pixels por polegada (ppi). Para calcular a proporção de pixels do dispositivo, siga estas etapas:
Compare a distância real em que o dispositivo é mantido com a distância do pixel de referência.
De acordo com a especificação, sabemos que, com 28 polegadas, o ideal é de 96 pixels por polegada. No entanto, como é um smartphone, as pessoas seguram o dispositivo mais perto do rosto do que seguram um laptop. Vamos estimar essa distância em 45 centímetros.
Multiplique a proporção de distância pela densidade padrão (96 ppi) para conseguir a densidade de pixels ideal para a distância especificada.
idealPixelDensity = (28/18) * 96 = 150 pixels por polegada (aproximadamente)
Use a proporção da densidade física de pixels em relação à densidade ideal de pixels para encontrar a proporção de pixels do dispositivo.
devicePixelRatio
= 180/150 = 1,2
Assim, quando um navegador precisa saber como redimensionar uma imagem para ajustar a tela de acordo com a resolução ideal ou padrão, ele se refere à taxa de pixels do dispositivo de 1,2, o que significa que, para cada pixel ideal, esse dispositivo tem 1,2 pixels físicos. A fórmula para alternar entre pixels ideais (conforme definido pela especificação da Web) e físicos (pontos na tela do dispositivo) é a seguinte:
physicalPixels = window.devicePixelRatio * idealPixels
Historicamente, os fornecedores de dispositivos tendem a arredondar devicePixelRatios
(DPRs, na sigla em inglês). O iPhone e o iPad da Apple informam um DPR de 1, e os equivalentes
da Retina informam 2. A especificação do CSS recomenda que
a unidade de pixel se refere ao número inteiro de pixels do dispositivo que mais se aproxima do pixel de referência.
Um motivo pelo qual as proporções redondas podem ser melhores é porque elas podem levar a menos artefatos de subpixel.
No entanto, a realidade do cenário de dispositivos é muito mais variada, e os smartphones Android geralmente têm DPRs de 1,5. O tablet Nexus 7 tem um DPR de ~1,33, que foi alcançado por um cálculo semelhante ao acima. Mais dispositivos com DPRs variáveis serão lançados no futuro. Por isso, nunca presuma que seus clientes terão DPRs inteiros.
Visão geral das técnicas de imagem HiDPI
Há muitas técnicas para resolver o problema de mostrar as imagens de melhor qualidade o mais rápido possível, que se dividem em duas categorias:
- Otimizar imagens únicas
- Otimização da seleção entre várias imagens.
Abordagens de imagem única: use uma imagem, mas faça algo inteligente com ela. Essas abordagens têm a desvantagem de inevitavelmente sacrificar o desempenho, já que você fará o download de imagens HiDPI mesmo em dispositivos mais antigos com DPI menor. Aqui estão algumas abordagens para o caso de uma única imagem:
- Imagem HiDPI altamente compactada
- Formato de imagem totalmente incrível
- Formato de imagem progressiva
Várias abordagens de imagem: use várias imagens, mas faça algo inteligente para escolher qual delas será carregada. Essas abordagens têm uma sobrecarga inerente para que o desenvolvedor crie várias versões do mesmo recurso e, em seguida, descubra uma estratégia de decisão. As opções são:
- JavaScript
- Entrega do lado do servidor
- Consultas de mídia CSS
- Recursos integrados do navegador (
image-set()
,<img srcset>
)
Imagem HiDPI altamente compactada
As imagens já representam 60% da largura de banda usada para fazer o download de um site médio. Ao exibir imagens HiDPI para todos os clientes, aumentaremos esse número. Quanto maior será o crescimento?
Fiz alguns testes que geraram fragmentos de imagem 1x e 2x com qualidade JPEG em 90, 50 e 20. Este é o script de shell que usei (usando o ImageMagick) para gerar os arquivos:
Com base nessa amostra pequena e não científica, parece que a compactação de imagens grandes oferece uma boa relação entre qualidade e tamanho. Para mim, imagens de 2x muito compactadas parecem melhores do que imagens de 1x não comprimidas.
É claro que veicular imagens de baixa qualidade e altamente compactadas para dispositivos 2x é pior do que veicular imagens de alta qualidade, e a abordagem acima gera penalidades na qualidade da imagem. Se você comparar a qualidade: 90 imagens com a qualidade: 20 imagens, vai notar uma queda na nitidez e um aumento do grão. Esses artefatos podem não ser aceitáveis nos casos em que imagens de alta qualidade são fundamentais (por exemplo, um aplicativo visualizador de fotos) ou para desenvolvedores de apps que não estejam dispostos a se comprometer.
A comparação acima foi feita inteiramente com JPEGs compactados. É importante observar que há muitas vantagens entre os formatos de imagem amplamente implementados (JPEG, PNG, GIF), o que nos leva a...
O formato da imagem é totalmente incrível
O WebP é um formato de imagem atraente que comprime muito bem, mantendo a alta fidelidade da imagem. É claro que ele ainda não está implementado em todos os lugares.
Uma maneira de verificar o suporte ao WebP é pelo JavaScript. Você carrega uma imagem de 1 px
por data-uri, espera que os eventos de carregamento ou de erro sejam acionados e
verifica se o tamanho está correto. O Modernizr é enviado com
um script de detecção de recursos, que está disponível
pelo Modernizr.webp
.
No entanto, uma maneira melhor de fazer isso é diretamente no CSS, usando a função image(). Se você tiver uma imagem WebP e um substituto JPEG, poderá escrever o seguinte:
#pic {
background: image("foo.webp", "foo.jpg");
}
Essa abordagem tem alguns problemas. Em primeiro lugar, image()
não é
amplamente implementado. Em segundo lugar, embora a compressão do WebP perca o formato do JPEG,
ela ainda é uma melhoria relativamente incremental,
cerca de 30% menor com base nesta galeria WebP (link em inglês). Portanto, o WebP
sozinho não é suficiente para resolver o problema de DPI alto.
Formatos de imagem progressiva
Formatos de imagem progressivos, como JPEG 2000, JPEG progressivo, PNG progressivo e GIF, têm a vantagem (um tanto debatida) de mostrar a imagem antes de ela ser totalmente carregada. Eles podem incorrer em algum overhead de tamanho, embora haja evidências conflitantes sobre isso. Jeff Atwood afirmou que o modo progressivo "adiciona cerca de 20% ao tamanho de imagens PNG e cerca de 10% ao tamanho de imagens JPEG e GIF". No entanto, Stoyan Stefanov afirmou que, para arquivos grandes, o modo progressivo é mais eficiente (na maioria dos casos).
À primeira vista, as imagens progressivas parecem muito promissoras no contexto de veicular as imagens de melhor qualidade o mais rápido possível. A ideia é que o navegador possa parar de fazer o download e a decodificação de uma imagem quando saber que dados adicionais não vão aumentar a qualidade da imagem (ou seja, todas as melhorias de fidelidade são de subpixel).
Embora as conexões sejam fáceis de encerrar, elas geralmente são caras para reiniciar. Para um site com muitas imagens, a abordagem mais eficiente é manter uma única conexão HTTP ativa, reutilizando-a pelo maior tempo possível. Se a conexão for encerrada prematuramente porque uma imagem foi transferida por download suficiente, o navegador precisará criar uma nova conexão, que pode ser realmente lenta em ambientes de baixa latência.
Uma solução alternativa para isso é usar a solicitação HTTP Range, que permite que os navegadores especifiquem um intervalo de bytes para buscar. Um navegador inteligente pode fazer uma solicitação HEAD para acessar o cabeçalho, processá-lo, decidir quanto da imagem é realmente necessário e, em seguida, fazer a busca. Infelizmente, o intervalo HTTP tem pouco suporte em servidores da Web, o que torna essa abordagem impraticável.
Por fim, uma limitação óbvia dessa abordagem é que você não pode escolher qual imagem carregar, apenas variar a fidelidade da mesma imagem. Como resultado, isso não aborda o caso de uso de direção de arte.
Usar o JavaScript para decidir qual imagem carregar
A primeira e mais óbvia abordagem para decidir qual imagem carregar é
usar JavaScript no cliente. Essa abordagem permite que você descubra
tudo sobre o user agent e faça a coisa certa. É possível
determinar a proporção de pixels do dispositivo usando window.devicePixelRatio
, conferir a largura
e a altura da tela e até mesmo fazer um pouco de sniffing de conexão de rede
usando navigator.connection ou emitindo uma solicitação falsa, como a
biblioteca foresight.js. Depois de coletar todas essas informações, você pode decidir qual imagem carregar.
Há aproximadamente um milhão de bibliotecas JavaScript que fazem algo como o acima, e, infelizmente, nenhuma delas é particularmente destacada.
Uma grande desvantagem dessa abordagem é que o uso do JavaScript significa que
você vai atrasar o carregamento da imagem até que o analisador look-ahead tenha
terminado. Isso significa que as imagens não vão começar
a ser transferidas até que o evento pageload
seja acionado. Saiba mais sobre isso no artigo de Jason Grigsby.
Decidir qual imagem carregar no servidor
É possível adiar a decisão para o lado do servidor escrevendo manipuladores de solicitação personalizados para cada imagem que você serve. Esse manipulador verificaria o suporte da Retina com base no User-Agent (a única informação transmitida ao servidor). Em seguida, com base em se a lógica do lado do servidor quer servir ativos HiDPI, carregue o ativo apropriado (nomeado de acordo com alguma convenção conhecida).
Infelizmente, o user agent não fornece necessariamente informações suficientes para decidir se um dispositivo precisa receber imagens de alta ou baixa qualidade. Além disso, nem tudo que está relacionado ao user agent é uma invasão e precisa ser evitado, se possível.
Usar consultas de mídia CSS
Por serem declarativas, as consultas de mídia do CSS permitem que você declare sua intenção e
permitem que o navegador faça a coisa certa em seu nome. Além do uso mais
comum de consultas de mídia, que corresponde ao tamanho do dispositivo, você também
pode corresponder a devicePixelRatio
. A consulta de mídia associada é
a proporção de pixels do dispositivo e tem variantes mínimas e máximas associadas, como
esperado. Se você quiser carregar imagens de alta DPI e a proporção de pixels do dispositivo
ultrapassar um limite, faça o seguinte:
#my-image { background: (low.png); }
@media only screen and (min-device-pixel-ratio: 1.5) {
#my-image { background: (high.png); }
}
Fica um pouco mais complicado com todos os prefixos de fornecedores misturados, principalmente por causa das diferenças na posição dos prefixos "min" e "max":
@media only screen and (min--moz-device-pixel-ratio: 1.5),
(-o-min-device-pixel-ratio: 3/2),
(-webkit-min-device-pixel-ratio: 1.5),
(min-device-pixel-ratio: 1.5) {
#my-image {
background:url(high.png);
}
}
Com essa abordagem, você recupera os benefícios da análise antecipada, que foi perdida com a solução JS. Você também ganha a flexibilidade de escolher pontos de interrupção responsivos (por exemplo, é possível ter imagens de DPI baixa, média e alta), o que era perdido com a abordagem do lado do servidor.
Infelizmente, ele ainda é um pouco difícil de usar e leva a um CSS
com aparência estranha (ou requer pré-processamento). Além disso, essa abordagem é restrita a
propriedades CSS. Portanto, não há como definir um <img src>
, e todas as imagens
precisam ser elementos com um plano de fundo. Por fim, ao depender exclusivamente da
proporção de pixels do dispositivo, você pode acabar em situações em que o smartphone
de alta DPI acaba fazendo o download de um recurso de imagem enorme 2x em uma
conexão EDGE. Essa não é a melhor experiência do usuário.
Usar novos recursos do navegador
Recentemente, houve muitas discussões sobre o suporte da plataforma da Web para
o problema de imagens de alta DPI. A Apple entrou recentemente no espaço,
trazendo a função CSS image-set() para o WebKit. Por isso, tanto o Safari quanto o Chrome são compatíveis com ela. Como é uma função CSS, image-set()
não resolve o problema para tags <img>
. Insira
@srcset, que resolve esse problema, mas (no momento
da escrita) não tem implementações de referência (ainda!). A próxima seção
aborda mais detalhadamente image-set
e srcset
.
Recursos do navegador para suporte a DPI alto
Em última análise, a decisão sobre qual abordagem você usa depende dos seus
requisitos específicos. Mas lembre-se de que todas as abordagens
mencionadas têm desvantagens. No entanto, quando
image-set
e srcset tiverem suporte amplo, eles serão as
soluções adequadas para esse problema. Por enquanto, vamos falar sobre algumas práticas recomendadas que podem nos aproximar o máximo possível desse futuro ideal.
Primeiro, qual a diferença entre eles? image-set()
é uma função CSS
adequada para uso como um valor da propriedade CSS de plano de fundo.
srcset é um atributo específico para elementos <img>
, com sintaxe semelhante.
Essas duas tags permitem especificar declarações de imagem, mas o atributo srcset permite configurar qual imagem carregar com base no tamanho da janela de visualização.
Práticas recomendadas para image-set
A função CSS image-set()
está disponível com o prefixo
-webkit-image-set()
. A sintaxe é bastante simples, usando uma ou mais
declarações de imagem separadas por vírgulas, que consistem em uma string de URL ou
função url()
seguida pela resolução associada. Exemplo:
background-image: -webkit-image-set(
url(icon1x.jpg) 1x,
url(icon2x.jpg) 2x
);
Isso informa ao navegador que há duas imagens para escolher. Um deles é otimizado para telas 1x e o outro para telas 2x. O navegador pode escolher qual carregar com base em vários fatores, que podem incluir até mesmo a velocidade da rede, se o navegador for inteligente o suficiente (não implementado no momento).
Além de carregar a imagem correta, o navegador também a dimensionará da forma adequada. Em outras palavras, o navegador presume que duas imagens são duas vezes mais grandes que imagens de 1x. Portanto, a imagem de 2x será reduzida por um fator de 2, de modo que a imagem pareça ter o mesmo tamanho na página.
Em vez de especificar 1x, 1,5x ou Nx, você também pode especificar uma determinada densidade de pixels do dispositivo em dpi.
Isso funciona bem, exceto em navegadores que não têm suporte à propriedade image-set
, que não exibirá nenhuma imagem. Isso é claramente ruim, então você
precisa usar um substituto (ou uma série de substitutos) para resolver esse problema:
background-image: url(icon1x.jpg);
background-image: -webkit-image-set(
url(icon1x.jpg) 1x,
url(icon2x.jpg) 2x
);
/* This will be useful if image-set gets into the platform, unprefixed.
Also include other prefixed versions of this */
background-image: image-set(
url(icon1x.jpg) 1x,
url(icon2x.jpg) 2x
);
O exemplo acima vai carregar o recurso apropriado em navegadores compatíveis com o
image-set. Caso contrário, ele vai usar o recurso de 1x. A ressalva óbvia
é que, embora o suporte ao navegador image-set()
seja baixo, a maioria dos user agents
recebe o recurso de 1x.
Esta demonstração usa o image-set()
para carregar a imagem
correta, voltando ao recurso 1x se essa função CSS não tiver
suporte.
Neste ponto, você pode estar se perguntando por que não usar um polifil (ou seja,
criar um shim JavaScript para) image-set()
e encerrar o dia. Como resultado, é muito difícil implementar polyfills eficientes para funções CSS. Para uma explicação detalhada sobre o motivo, consulte esta discussão no estilo www.
Imagem srcset
Veja um exemplo de srcset:
<img alt="my awesome image"
src="banner.jpeg"
srcset="banner-HD.jpeg 2x, banner-phone.jpeg 640w, banner-phone-HD.jpeg 640w 2x">
Como você pode ver, além das declarações x que image-set
fornece,
o elemento srcset também usa valores w e h que correspondem ao
tamanho da janela de visualização, tentando veicular a versão mais relevante. O
código acima serviria banner-phone.jpeg para dispositivos com largura de janela de visualização
menor que 640 px, banner-phone-HD.jpeg para dispositivos de alta DPI com tela pequena,
banner-HD.jpeg para dispositivos de alta DPI com telas maiores que 640 px e
banner.jpeg para todos os outros.
Como usar image-set para elementos de imagem
Como o atributo srcset em elementos img não é implementado na maioria
dos navegadores, pode ser tentador substituir esses elementos por <div>
s
com planos de fundo e usar a abordagem de conjunto de imagens. Isso vai funcionar, com
algumas ressalvas. A desvantagem é que a tag <img>
tem valor semântico
de longa duração. Na prática, isso é importante principalmente para rastreadores da Web
e por motivos de acessibilidade.
Se você acabar usando -webkit-image-set
, poderá ficar tentado a usar a
propriedade do CSS em segundo plano. A desvantagem dessa abordagem é que você precisa
especificar o tamanho da imagem, que é desconhecido se você estiver usando uma imagem que não seja 1x.
Em vez disso, use a propriedade CSS de conteúdo da seguinte maneira:
<div id="my-content-image"
style="content: -webkit-image-set(
url(icon1x.jpg) 1x,
url(icon2x.jpg) 2x);">
</div>
Isso dimensionará a imagem automaticamente com base em devicePixelRatio. Confira
este exemplo da técnica acima em ação,
com uma substituição adicional para url()
em navegadores que não oferecem suporte a
image-set
.
Polyfill de srcset
Um recurso útil do srcset
é que ele vem com uma substituição natural.
Quando o atributo srcset não é implementado, todos os navegadores sabem processar o atributo src. Além disso, como é apenas um atributo
HTML, é possível criar polyfills com
JavaScript.
Esse polyfill vem com testes de unidade para garantir que ele esteja o mais próximo possível da especificação. Além disso, há verificações que impedem o polyfill de executar qualquer código se o srcset for implementado de forma nativa.
Confira uma demonstração do polyfill em ação.
Conclusão
Não existe uma fórmula mágica para resolver o problema de imagens com DPI alto.
A solução mais fácil é evitar imagens completamente e optar por SVG e CSS. No entanto, isso nem sempre é realista, especialmente se você tem imagens de alta qualidade no site.
As abordagens em JS, CSS e no lado do servidor têm pontos fortes
e fracos. A abordagem mais promissora, no entanto, é aproveitar os novos
recursos do navegador. Embora o suporte do navegador para image-set
e srcset
ainda seja
incompleto, há substitutos razoáveis para usar hoje.
Para resumir, minhas recomendações são as seguintes:
- Para imagens de plano de fundo, use image-set com as alternativas adequadas para navegadores que não oferecem suporte a ele.
- Para imagens de conteúdo, use um polyfill srcset ou use image-set como substituto (consulte acima).
- Para situações em que você está disposto a sacrificar a qualidade da imagem, considere usar imagens de 2x muito compactadas.