Um dos recursos do cenário complexo de dispositivos de hoje é que há uma variedade muito ampla de densidades de pixels de tela disponíveis. Alguns dispositivos têm telas de resolução muito alta, enquanto outros ficam atrás. Os desenvolvedores de aplicativos precisam oferecer suporte a uma variedade de densidades de pixels, o que pode ser bastante desafiador. Na Web para dispositivos móveis, os desafios são compostos por vários fatores:
- Grande variedade de dispositivos com diferentes formatos.
- Largura de banda de rede e duração da bateria limitadas.
Em termos de imagens, o objetivo dos desenvolvedores de apps da Web é disponibilizar as imagens de melhor qualidade da maneira mais eficiente possível. Neste artigo, abordaremos algumas técnicas úteis para fazer isso hoje e no futuro próximo.
Evite imagens, se possível
Antes de abrir essa lata de worms, lembre-se de que a Web tem muitas tecnologias poderosas que são, em grande parte, independentes de resolução e DPI. Especificamente, textos, SVG e grande parte do CSS vão "simplesmente funcionar" devido ao recurso de escalonamento automático de pixels da Web (via devicePixelRatio).
Mas nem sempre é possível evitar imagens rasterizadas. Por exemplo, talvez você receba recursos que seriam muito difíceis de replicar em SVG/CSS puro ou esteja lidando com uma fotografia. Embora seja possível converter a imagem em SVG automaticamente, vetorizar fotografias não faz muito sentido, porque as versões ampliadas geralmente não têm uma boa aparência.
Contexto
Um histórico muito curto da densidade de exibição
No início, as telas de computadores tinham uma densidade de pixel de 72 ou 96 dpi (pontos por polegada).
As telas melhoram gradualmente a densidade de pixels, em grande parte devido ao caso de uso para dispositivos móveis, em que os usuários geralmente seguram o smartphone mais perto do rosto, tornando os pixels mais visíveis. Em 2008, os celulares de 150 dpi eram o novo padrão. A tendência de aumento da densidade de exibição continuou, e os novos smartphones de hoje apresentam telas de 300 dpi (marcadas como "Retina" da Apple).
O segredo é, obviamente, uma tela em que os pixels ficam completamente invisíveis. Para o formato de smartphones, a geração atual de telas Retina/HiDPI pode estar parecida com esse ideal. No entanto, novas classes de hardware e wearables, como o Project Glass, provavelmente continuarão a aumentar a densidade de pixels.
Na prática, as imagens de baixa densidade têm a mesma aparência em telas novas e das antigas. No entanto, em comparação com as imagens nítidas que os usuários de alta densidade estão acostumados a ver, as imagens de baixa densidade parecem chocantes e pixels. Confira a seguir uma simulação aproximada de como uma imagem de 1x ficará em uma tela de 2x. Por outro lado, a imagem de 2x é muito boa.
Pixels na Web
Quando a Web foi projetada, 99% das telas tinham 96 dpi (ou fingiam ser), e poucas disposições para variação nesse aspecto. Devido a uma grande variação nos tamanhos e densidades de tela, precisávamos de uma maneira padrão para que as imagens tivessem boa aparência em várias densidades e dimensões de tela.
A especificação HTML recentemente resolveu esse problema 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.
Calcular 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). O cálculo da proporção de pixels do dispositivo requer três 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 é 96 pixels por polegada. No entanto, como se trata de um smartphone, as pessoas seguram o dispositivo mais perto do rosto do que segurando um laptop. Vamos estimar essa distância como 18 polegadas.
Multiplique a taxa 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)
Considere a proporção entre a densidade física de pixels e a densidade de pixels ideal para chegar à proporção de pixels do dispositivo.
devicePixelRatio
= 180/150 = 1,2
Agora, quando um navegador precisa saber como redimensionar uma imagem para caber na tela de acordo com a resolução ideal ou padrão, ele se refere à proporção de pixels do dispositivo de 1,2, ou seja, para cada pixel ideal, o 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) é:
physicalPixels = window.devicePixelRatio * idealPixels
Historicamente, os fornecedores de dispositivos costumam arredondar devicePixelRatios
(DPRs). O iPhone e o iPad da Apple informam o DPR de 1, e os equivalentes do Retina
informam o relatório 2. A especificação CSS (em inglês) recomenda que
a unidade de pixel refere-se ao número inteiro de pixels do dispositivo que melhor se aproxima do pixel de referência.
Um motivo para as proporções de arredondamento serem melhores é que 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 uma DPR de cerca de 1,33, calculada por um cálculo semelhante ao mostrado acima. Espere ver mais dispositivos com DPRs variáveis no futuro. Por isso, nunca presuma que seus clientes terão DPRs de números 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, dividindo-se em duas categorias:
- Otimizar imagens únicas
- Otimizando a 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 mais baixo. Confira algumas abordagens para o caso de imagem única:
- Imagem HiDPI altamente compactada
- Formato de imagem totalmente incrível
- Formato de imagem progressivo
Várias abordagens de imagens: use várias imagens, mas escolha algo inteligente para escolher qual carregar. Essas abordagens têm um overhead inerente para o desenvolvedor criar várias versões do mesmo recurso e, em seguida, definir uma estratégia de decisão. As opções são:
- JavaScript
- Entrega no lado do servidor
- Consultas de mídia CSS
- Recursos integrados do navegador (
image-set()
,<img srcset>
)
Imagem HiDPI altamente compactada
As imagens já correspondem a 60% da largura de banda gasta no download de um site comum. Ao fornecer imagens HiDPI para todos os clientes, vamos aumentar esse número. Quanto ele vai crescer?
Executei alguns testes que geraram fragmentos de imagem de 1x e 2x com qualidade JPEG a 90, 50 e 20. Este é o script de shell que usei (com a implementação do ImageMagick) para gerá-los:
Com base nessa pequena amostragem não científica, parece que a compactação de imagens grandes proporciona uma boa relação entre qualidade e tamanho. Para mim, imagens 2x muito compactadas realmente ficam melhores do que imagens 1x não compactadas.
Obviamente, veicular imagens 2x e altamente compactadas de baixa qualidade para o dobro de dispositivos é pior do que veicular imagens de mais qualidade. A abordagem acima incorre em penalidades de qualidade da imagem. Se você comparar a qualidade: 90 imagens com qualidade (20 imagens), vai notar uma queda na nitidez e maior granulação. Esses artefatos podem não ser aceitáveis nos casos em que imagens de alta qualidade são essenciais (por exemplo, um aplicativo visualizador de fotos) ou para desenvolvedores de apps que não estão dispostos a comprometer.
A comparação acima foi feita inteiramente com JPEGs compactados. É importante notar que há muitas vantagens e desvantagens entre os formatos de imagem amplamente implementados (JPEG, PNG, GIF), o que nos leva a...
Formato de imagem totalmente incrível
WebP é um formato de imagem bastante interessante que compacta muito bem, mantendo a alta fidelidade da imagem. É claro que ele ainda não foi implementado em todos os lugares.
Uma maneira é verificar o suporte para WebP é via JavaScript. Carregue uma imagem de 1 px por meio de data-uri, aguarde os eventos carregados ou de erro disparados e, em seguida, verifique se o tamanho está correto. O Modernizr é fornecido com
um script de detecção de recursos (em inglês), que está disponível
pelo Modernizr.webp
.
Uma maneira melhor de fazer isso é diretamente no CSS usando a função image(). Portanto, se você tiver uma imagem WebP e um substituto de JPEG, poderá criar o seguinte:
#pic {
background: image("foo.webp", "foo.jpg");
}
Há alguns problemas com essa abordagem. Em primeiro lugar, o image()
não foi
amplamente implementado. Em segundo lugar, embora a compactação do WebP supere o formato JPEG
fora, essa ainda é uma melhoria relativamente incremental:
cerca de 30% menor, com base nesta galeria WebP. Assim, o WebP
só não é suficiente para resolver o problema do alto DPI.
Formatos de imagem progressivos
Formatos de imagem progressivos, como JPEG 2000, JPEG progressivo, PNG e GIF progressivos, têm o benefício (um pouco discutido) de ver a imagem entrar no lugar antes de ser totalmente carregada. Eles podem gerar uma sobrecarga de tamanho, embora haja evidências conflitantes sobre isso. Jeff Atwood reivindicou que o modo progressivo "aumenta cerca de 20% do tamanho das imagens PNG e 10% do tamanho das 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 quando usadas para exibir imagens de melhor qualidade o mais rápido possível. A ideia é que o navegador possa interromper o download e decodificar uma imagem quando saber que outros dados não aumentam a qualidade da imagem, ou seja, todas as melhorias de fidelidade são subpixels.
Embora as conexões sejam fáceis de encerrar, reiniciá-las costuma ser cara. 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 o download de uma imagem foi suficiente, o navegador precisará criar uma nova conexão, o que pode ser muito lento em ambientes de baixa latência.
Uma solução para isso é usar a solicitação HTTP Range, que permite que os navegadores especifiquem um intervalo de bytes a serem buscados. Um navegador inteligente pode fazer uma solicitação HEAD para chegar ao cabeçalho, processá-la, decidir quanto da imagem é realmente necessário e, então, fazer a busca. Infelizmente, o intervalo HTTP é mal suportado 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 as fidelidades variadas da mesma imagem. Como resultado, ele não aborda o caso de uso de "direção da arte".
Usar o JavaScript para decidir qual imagem carregar
A primeira e mais óbvia abordagem para decidir qual imagem carregar é usar o JavaScript no cliente. Essa abordagem permite que você descubra tudo
sobre o user agent e faça a coisa certa. Você pode
determinar a proporção de pixels do dispositivo com window.devicePixelRatio
, conferir a largura e a altura
da tela e até mesmo possivelmente identificar a conexão de rede
via Navigator.connection ou emitir 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 semelhante ao descrito acima, e nenhuma delas é excepcional.
Uma grande desvantagem dessa abordagem é que o uso do JavaScript significa que
você atrasará o carregamento da imagem até que o analisador de visualização prévia seja
concluído. Isso significa que o download das imagens não
será iniciado até que o evento pageload
seja disparado. Saiba mais sobre isso no
artigo de Jason Grigsby.
Decida qual imagem carregar no servidor
Você pode adiar a decisão para o lado do servidor escrevendo gerenciadores de solicitações personalizados para cada imagem exibida. Esse gerenciador verificaria o suporte à Retina com base no user agent (a única informação transmitida ao servidor). Em seguida, dependendo de a lógica do lado do servidor querer disponibilizar recursos HiDPI, carregue o recurso 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 receberá imagens de alta ou baixa qualidade. Além disso, algo relacionado ao user agent é uma invasão e precisa ser evitada, se possível.
Usar consultas de mídia CSS
Por serem declarativas, as consultas de mídia CSS permitem indicar sua intenção e
permitir que o navegador faça a coisa certa por você. Além do uso mais
comum de consultas de mídia (correspondente ao tamanho do dispositivo), também é possível
corresponder devicePixelRatio
. A consulta de mídia associada é
proporção de pixels de dispositivo e tem variantes mínima e máxima associadas, como
esperado. Se você quiser carregar imagens com DPI alto e a proporção de pixels do dispositivo
exceder 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); }
}
Isso fica um pouco mais complicado com todos os prefixos de fornecedor misturados, especialmente devido às diferenças de posicionamento insanas de 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 foram perdidos com a solução JS. Você também ganha a flexibilidade de escolher seus pontos de interrupção responsivos (por exemplo, pode ter imagens de DPI baixo, médio e alto), o que foi perdido com a abordagem do lado do servidor.
Infelizmente, ele ainda é um pouco difícil de administrar e causa um CSS com aparência estranha (ou exige pré-processamento). Além disso, essa abordagem é restrita a
propriedades CSS. Portanto, não é possível definir um <img src>
, e todas as imagens
precisam ter um plano de fundo. Por fim, ao usar estritamente a
proporção de pixels do dispositivo, você pode acabar em situações em que seu smartphone de alto DPI
faz o download de um grande recurso de imagem de 2x durante uma
conexão EDGE. Essa não é a melhor experiência do usuário.
Usar novos recursos do navegador
Tem havido muita discussão recente sobre o suporte à plataforma da Web para
o problema de imagens com alto DPI. A Apple recentemente entrou no espaço, trazendo a função CSS image-set() para o WebKit. Por isso, o Safari e o Chrome são compatíveis com ele. Como é uma função CSS, image-set()
não resolve o problema das tags <img>
. Digite
@srcset, que resolve esse problema, mas ainda não tem
implementações de referência. A próxima seção
é mais aprofundada em image-set
e srcset
.
Recursos do navegador para suporte a DPI alto
Em última análise, a decisão sobre qual abordagem adotar depende dos seus
requisitos específicos. Tenha em mente que todas as abordagens
mencionadas acima têm desvantagens. No entanto, quando
image-set
e srcset tiverem ampla compatibilidade, serão as
soluções adequadas para esse problema. Por enquanto, vamos falar sobre algumas práticas recomendadas que podem nos aproximar desse futuro ideal.
Primeiro, qual é a diferença entre eles? Bem, image-set()
é uma função CSS, adequada para uso como um valor da propriedade CSS de segundo plano. srcset é um atributo específico para elementos <img>
, com sintaxe semelhante.
As duas tags permitem especificar declarações de imagem, mas o atributo srcset
também permite configurar qual imagem carregar com base no tamanho da
janela de visualização.
Práticas recomendadas para conjunto de imagens
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írgula, 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 escolhe qual deles carregar, com base em vários fatores, que podem até incluir a velocidade da rede, se o navegador for inteligente o suficiente (no momento não implementado que eu saiba).
Além de carregar a imagem correta, o navegador também a dimensionará da maneira adequada. Em outras palavras, o navegador presume que duas imagens são duas vezes maiores que 1x. Portanto, ele reduz a imagem de 2x por um fator de 2 para que a imagem pareça ter o mesmo tamanho na página.
Em vez de especificar 1x, 1,5x ou Nx, também é possível especificar uma determinada densidade de pixel do dispositivo em dpi.
Isso funciona bem, exceto em navegadores que não são compatíveis com a propriedade image-set
, que não mostram 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 carregará o recurso apropriado em navegadores compatíveis com image-set. Caso contrário, o recurso usará o de 1x. A ressalva óbvia é que, embora o suporte ao navegador image-set()
seja baixo, a maioria dos user agents receberá o recurso de 1x.
Esta demonstração usa o image-set()
para carregar a imagem
correta, usando o recurso de 1x se essa função CSS não for
compatível.
Neste ponto, você pode estar se perguntando por que não apenas o polyfill, ou seja, criar um paliativo JavaScript para image-set()
, e chamá-lo de dia. É muito difícil implementar polyfills eficientes para funções CSS. (Para uma explicação detalhada, consulte esta discussão no estilo www).
srcset da imagem
Aqui está 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 notar, além das declarações x fornecidas por image-set
,
o elemento srcset também usa valores w e h que correspondem ao
tamanho da janela de visualização, tentando exibir a versão mais relevante. O conteúdo acima veicularia banner-phone.jpeg para dispositivos com largura da janela de visualização abaixo de 640 px, banner-phone-HD.jpeg para dispositivos de tela pequena com alto DPI, banner-HD.jpeg para dispositivos de alto DPI com telas maiores que 640 px e banner.jpeg para todo o restante.
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 ressalvas. A desvantagem disso é que a tag <img>
tem um valor semântico
de longo prazo. Na prática, isso é importante principalmente para rastreadores da Web e motivos de acessibilidade.
Se você acabar usando -webkit-image-set
, talvez fique tentado a usar a
propriedade CSS em segundo plano. A desvantagem dessa abordagem é que você precisa
especificar o tamanho da imagem, o que é desconhecido se você estiver usando uma imagem que não seja de 1x.
Em vez de fazer isso, você pode usar 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 vai dimensionar a imagem automaticamente com base no devicePixelRatio. Confira
este exemplo da técnica acima em ação,
com um outro substituto para url()
para navegadores que não oferecem suporte a
image-set
.
srcset de polyfilling
Um recurso útil do srcset
é que ele vem com um substituto natural.
Quando o atributo srcset não é implementado, todos os navegadores
sabem que precisam 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, existem 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 totalmente imagens, optando por SVG e CSS. No entanto, isso nem sempre é realista, especialmente se você tiver imagens de alta qualidade no seu site.
As abordagens em JS, CSS e o uso do lado do servidor têm pontos fortes
e pontos fracos. No entanto, a abordagem mais promissora é aproveitar os novos recursos do navegador. Embora o suporte dos navegadores a image-set
e srcset
ainda esteja incompleto, há substitutos razoáveis para o uso no momento.
Resumindo, minhas recomendações são:
- Para imagens de plano de fundo, use image-set com os substitutos apropriados para navegadores que não são compatíveis com ela.
- Para imagens de conteúdo, use um polyfill srcset ou substitua usando image-set (confira acima).
- Para situações em que você quer sacrificar a qualidade da imagem, considere o uso de imagens 2x compactadas.