Immagini responsive in pratica

Il diavolo ha messo una penalità sulle cose della vita che ci piacciono.

L’articolo prosegue sotto

Albert Einstein

Il 62% del peso del web è costituito dalle immagini e più passano i giorni più inviamo byte di immagini. Sarebbe fantastico se tutti questi byte fossero usati in maniera ottimale, ma su schermi piccoli o a bassa risoluzione la maggior parte dei dati viene sprecata.

Perché? Nonostante il web sia stato progettato per essere accessibile da tutti, con qualunque dispositivo, è solo recentemente che il panorama dei device si è diversificato abbastanza da forzare un movimento grande quanto il settore verso il responsive design. Quando progettiamo in maniera responsive, il nostro contenuto fluisce elegantemente ed efficientemente in ogni device. Tutto il nostro contenuto, ad eccezione delle bitmap. Le immagini bitmap hanno una risoluzione fissa e il loro contenitore, il venerabile img con la sua purtroppo singola src non permette alcun adattamento.

Posti di fronte ad una scelta di Sophie – se rendere le pagine sfuocate per alcuni o lente per tutti – la maggior parte dei designer sceglie l’ultima, mandando a tutti le immagini che dovrebbero riempire gli schermi più grandi e a maggior risoluzione. Quindi, spreco.

Ebbene, dopo tre anni di dibattito, sono emersi alcuni markup per risolvere il problema delle immagini responsive:

  • srcset
  • sizes
  • picture
  • e la nostra vecchia amica source (presa in prestito da audio e video)

Questi nuovi elementi ed attributi ci permettono di inserire nel markup più sorgenti alternative e mandano ad ogni client la sorgente che meglio gli si adatta. Si sono fatte strada nelle specifiche ufficiali e la loro prima implementazione completa, in Chrome 38, è uscita a Settembre. Con fallback eleganti e un polyfill per tamponare il problema, possiamo e dovremmo implementare le immagini responsive fin d’ora. Allora facciamolo!

Prendiamo una pagina web esistente e rendiamo le sue immagini responsive. Lo faremo in tre passaggi, applicando ciascun pezzo del nuovo markup a turno:

  1. Ci assicureremo che le nostre immagini si ridimensionino efficientemente con srcset e sizes.
  2. Applicheremo l’art direction alle nostre immagini con picture e source media.
  3. Forniremo un formato di immagine alternativo usando picture e source type.

In questo processo vedremo di persona gli incredibili guadagni in termini di performance permessi dalle nuove feature.

Lo status quo#section1

Credo non mi interessi così tanto essere vecchio quanto mi interessi essere grasso e vecchio.

Benjamin Franklin (o era Peter Gabriel?)

Prendiamo come tema una piccola pagina sulle bizzarre coperte denominate quilt. È una semplice pagina responsive. Non c’è molto che possa intralciare il suo contenuto primario: immagini giganti (di coperte!). Vogliamo mostrare sia il design generale di ogni coperta sia quanti più intricati dettagli possibile. Così, per ciascuna, presentiamo due immagini:

  1. l’intera coperta, della larghezza del paragrafo
  2. un dettaglio della stessa, che riempia il 100% della larghezza della viewport.

Come potremmo assegnare la dimensione e che markup dovremmo usare per le nostre immagini senza usare quello nuovo?

Per prima cosa prendiamo in considerazione le coperte intere. Per essere sicuri che appaiano sempre ben definite, dobbiamo capire la dimensione più grande possibile del layout. Ecco il CSS che ci interessa:

* {
	box-sizing: border-box;
}
body {
	font-size: 1.25em;
}
figure {
	padding: 0 1em;
	max-width: 33em;
}
img { 
	display: block;
	width: 100%;
}

Possiamo anche calcolare la larghezza più grande possibile per il display di img prendendo la max-width di figure, sottraendone il padding e convertendo gli em in pixel:

  100% <img> width
x ( 33em <figure> max-width
   - 2em <figure> padding )
x 1.25em <body> font-size
x 16px default font-size
= 620px

Oppure possiamo imbrogliare rendendo davvero grande la finestra e dando un sbirciata ai dev tools:

Immagine del box mode nei dev tools di Chrome che mostra che l'elemento è largo 620px.

(io preferisco il secondo metodo).

In entrambe i modi arriviamo alla massima larghezza del display che mostra l’img dell’intero quilt con larghezza di 620px. Renderemo le nostre immagini sorgente al doppio di questa per accomodare gli schermi con risoluzione 2x: 1240 pixel di larghezza.

Ma cosa facciamo con le nostre immagini del dettaglio? Si espandono fino a riempire l’intera viewport, la cui dimensione non ha un limite superiore fissato. Così scegliamo qualcosa di leggermente più grande e dal sapore di standard e le rendiamo, diciamo, al massimo a 1920 pixel di larghezza.

Quando rendiamo le nostre immagini a queste dimensioni la nostra pagina dello status quo pesa un notevole 3.5MB. Tutto tranne 5.7kB di quel peso è costituito da immagini. Possiamo intuire che molti dei byte di quelle immagini costituiscono un overhead invisibile quando vengono inviati a piccoli schermi a bassa risoluzione, ma quanti? Mettiamoci al lavoro.

Primo passaggio: ridimensionamento con scrset e sizes#section2

Teatherball with a tennis ball for his shoelaces

Naturally adapt to have more than two faces

Kool AD, Dum Diary

Il primo problema che affronteremo è far sì che le nostre immagini si ridimensionino in maniera efficiente tra le varie larghezze della viewport e le risoluzioni di schermo. Offriremo più risoluzioni della nostra immagine, così che possiamo inviare file sorgenti giganti solo agli schermi giganti e/o ad alta risoluzione e versioni più piccole a tutti gli altri. In che modo? Con srcset.

Ecco una delle nostre immagini dei dettagli larga quanto la viewport:

<img
	src="quilt_2-detail.jpg"
	alt="Detail of the above quilt, highlighting the embroidery 
       and exotic stitchwork." />

quilt_2-detail.jpg ha una larghezza di 1920 pixel. Rendiamo due versioni più piccole che devono andare con questa e scriviamo il markup in questo modo:

<img
	srcset="quilt_2/detail/large.jpg  1920w, 
	        quilt_2/detail/medium.jpg  960w,
	        quilt_2/detail/small.jpg   480w"
	src="quilt_2/detail/medium.jpg"
	alt="Detail of the above quilt, highlighting the embroidery
      and exotic stitchwork.">

La prima cosa da notare riguardo a questa img è che ha ancora src, che verrà caricata dai browser che non supportano la nuova sintassi.

Per i client più capaci, abbiamo aggiunto qualcosa di nuovo: un attributo srcset, che contiene una lista separata da virgole di URL per le risorse. Dopo ciascun URL includiamo un “descrittore di larghezza”, che specifica la larghezza in pixel di ciascuna immagine. La vostra immagine è 1024 x 768? Mettete un 1024w dopo il suo URL in srcset. I browser che riconoscono srcset usano queste larghezze in pixel e tutto il resto che sanno dell’attuale ambiente di browsing per scegliere una sorgente da caricare tra quelle specificate nell’insieme.

In che modo fanno la loro scelta? Ecco quel che preferisco di srcset: non lo sappiamo! Non possiamo saperlo perché la logica che sta dietro alla scelta è stata intenzionalmente lasciata non specificata.

Le prime soluzioni proposte per il problema delle immagini responsive cercavano di dare più controllo agli autori. Avremmo dovuto farcene carico noi, creando insiemi esaustivi di media query – piani di emergenza che elencano tutte le combinazioni di dimensioni di schermo e di risoluzioni, con una sorgente creata su misura per ciascuna.

srcset ci salva da noi stessi. Il controllo dei particolari è ancora disponibile quando ci serve (di più su questo a breve), ma la maggior parte delle volte è meglio se lasciamo che sia il browser a decidere. I browser hanno un patrimonio di conoscenza dello schermo, della viewport, della connessione e delle preferenze di una persona. Cedendo il controllo, descrivendo le nostre immagini piuttosto che prescrivendo sorgenti specifiche per una miriade di destinazioni, permettiamo al browser di far valere la sua conoscenza. Otteniamo una funzionalità migliore (future-friendly!) da molto meno codice.

Tuttavia c’è un inghippo: scegliere una sorgente sensata richiede la conoscenza della dimensione del layout dell’immagine. Però non possiamo chiedere ai browser di ritardare la scelta finché tutto l’HTML, il CSS e il JavaScript siano stati tutti caricati e ne abbia fatto il parsing. Quindi dobbiamo dare ai browser una stima della larghezza del display dell’immagine usando un altro attributo nuovo: sizes.

Come ho fatto a nascondervi questa scomoda verità fino ad ora? Le immagini dei dettagli nella nostra pagina di esempio sono un caso particolare. Occupano l’intera larghezza della viewport—100vw—che quindi diventa il valore di default delle sizes. Tuttavia, le nostre immagini delle coperte intere, hanno la stessa larghezza del paragrafo e spesso occupano significativamente meno spazio. È necessario che noi si dica al browser esattamente quanto grandi debbano essere con sizes.

sizes accetta le lunghezze CSS. Quindi:

sizes="100px"

…dice il browser: questa immagine verrà mostrata alla larghezza fissa di 100px. Semplice!

Il nostro esempio è più complesso. Mentre le img della coperta hanno come stile una semplice regola width: 100%, le figure che le ospitano hanno una max-width di 33em.

Fortunamtamente, sizes ci permette di fare due cose:

  1. Ci permette di dare misure diverse elencandole separate da virgole.
  2. Ci permette di agganciare delle media condition alle misure.

In questo modo:

sizes="(min-width: 33em) 33em, 100vw"

Dice: la viewport è più larga di 33em? Questa immagine sarà larga 33em. Altrimenti, sarà 100vw.

Questo si avvicina a quello che serve a noi, ma non va completamente bene. Il nostro esempio è reso complicato dalla natura relativa degli em. Il corpo della nostra pagina ha un font-size di 1.25em, quindi “1em” nel contesto del CSS delle nostre figure sarà 1.25 volte la dimensione di default del font del browser. Ma all’interno delle media condition (e quindi, all’interno di sizes), un eem è sempre uguale al font size di default. Occorre fare delle moltiplicazioni per 1.25: 1.25 x 33 = 41.25.

sizes="(min-width: 41.25em) 41.25em,
       100vw"

Questo cattura piuttosto bene la larghezza delle nostre coperte e, francamente, probabilmente è sufficientemente buono. È accettabile al 100% che sizes fornisca la browser una stima grezza della larghezza del layout delle img. Spesso, barattare una piccola quantità di precisione per degli enormi guadagni in termini di leggibilità e manutenibilità è la scelta giusta. Ciò detto, proseguiamo e rendiamo preciso il nostro esempio calcolando gli em di padding su entrambe i lati della figura: 2 lati x 1.25 em della media-condition ciascuno = 2.5em di padding da tenere presenti.

<img 
	srcset="quilt_3/large.jpg  1240w, 
	        quilt_3/medium.jpg  620w,
	        quilt_3/small.jpg   310w"
	sizes="(min-width: 41.25em) 38.75em,
	       calc(100vw - 2.5em)"
	src="quilt_3/medium.jpg"
	alt="A crazy quilt whose irregular fabric scraps are fit 
       into a lattice of diamonds." />

Rivediamo cosa abbiamo fatto in questo caso. Abbiamo dato al browser la versione grande, media e piccola della nostra immagine usando srcset e abbiamo dato loro delle larghezze in pixel usando i descrittori w, e abbiamo detto al browser quanto spazio del layout dovranno occupare mediante sizes.

Se questo fosse un semplice esempio, avremmo potuto dare al browser una singola lunghezza nel CSS come sizes="100px" o sizes="50vw". Ma non siamo stati così fortunati. Abbiamo dovuto dare al browser due lunghezze CSS e stabilire che la prima lunghezza si applica solo quando è vera una certa media condition.

Fortunatamente, tutto quel lavoro non è stato fatto in vano. Utilizzando srcset e sizes, abbiamo dato al browser tutto quello di cui ha bisogno per scegliere una sorgente. Una volta che conosce le larghezze in pixel delle sorgenti e la larghezza del layout della img, il browser calcola la proporzione della larghezza sorgente-layout per ogni sorgente. Quindi, supponiamo che sizes ritorni 620px. Una sorgente di 620w avrebbe 1x i pixel della img. Una sorgente di 1240w ne avrebbe 2x. 310w? 0.5x. Il browser individua queste proporzioni e poi sceglie qualsiasi sorgente voglia.

Vale la pena notare che la specifica permette di fornire direttamente proporzioni e che alle sorgenti senza un descrittore verrà assegnata la proporzione di default 1x, permettendoci di scrivere markup come questo:

<img src="standard.jpg" srcset="retina.jpg 2x, super-retina.jpg 3x" />

Questo è un modo carino e compatto per dare immagini ad hi-DPI. Ma, funziona solo per immagini con larghezza fissa. Tutte le immagini della nostra pagine delle coperte folli sono fluide, quindi questa è l’ultima volta in cui sentiremo parlare dei descrittori x.

Misurare#section3

Ora che abbiamo riscritto la nostra pagina delle coperte folli usando srcset e sizes, cosa abbiamo guadagnato in termini di performance?

Il peso della nostra pagina è ora (gloriosamente!) reattivo alle condizione del browsing. Varia, pertanto non possiamo rappresentarlo con un solo numero. Ho ricaricato la pagina molte volte in Chrome e ho messo su un grafico il suo peso in una varietà di viewport di larghezza diversa:

Un grafico del peso della pagina per larghezza della finestra. C'è una riga piatta in cima e dei gradini sotto.

La prima linea grigia e piatta in alto rappresenta il peso dello status quo, 3.5MB. Le linee spesse (schermi 1x) e sottili (2x) di colore verde rappresentano il peso della nostra pagina per ogni larghezza di viewport tra 320px e 1280px aggiornata con srcset e size.

Su schermi 2x di 320px di larghezza abbiamo tagliato il peso della nostra pagina di due terzi: prima la pagina era di 3.5MB, adesso inviamo solo 1.1MB. Su schermi 1x di 320px, la nostra pagina è meno di un decimo il peso che era: 306kB.

Da qui, le dimensioni in by crescono man mano che carichiamo sorgenti più grandi per riempire viewport più grandi. Sui device 2x facciamo un salto significativo alle larghezze della viewport di circa 350px e torniamo al peso dello status quo dopo 480px. Sugli schermi 1x, il risparmio è notevole: abbiamo risparmiato tra il 70 e l’80% del peso originale della pagina finché non abbiamo passato i 960px. Là raggiungiamo il massimo con una pagina che è più piccola del 40% circa rispetto a quella da cui siamo partiti.

Questi tipi di riduzioni – 40%, 70%, 90% – dovrebbero farvi fermare di colpo. Abbiamo tolto quasi due megabyte e mezzo di carico per ogni iPhone Retina. Misuratelo in millisecondi o moltiplicatelo per migliaia di pageview e vedrete per che cosa è tutta questa agitazione.

Secondo step: picture e art direction#section4

srcset se sei pigro, picture se sei matto™

Mat Marquis

Allora, per le immagini che devono semplicemente ridimensionarsi, facciamo una lista delle sorgenti e delle loro larghezze in pixel in srcset, facciamo sapere al browser quale sarà la larghezza dell’img visualizzata usando sizes e lasciamo perdere il nostro desiderio da pazzi di avere il controllo. Però, ci saranno momenti in cui vorremo che le nostre immagini si adattino in modi che vanno oltre il ridimensionamento. Quando lo facciamo, abbiamo bisogno di riappropriarci di una parte di quel controllo sulla scelta della sorgente. Faccia il suo ingresso picture.

Le nostre immagini dei dettagli hanno un aspect ratio grande, 16:9. Su schermi grandi vanno benissimo ma sui telefoni appaiono piccole. Le cuciture e i ricami che dovrebbero essere mostrati nei dettagli sono troppo piccoli per risaltare.

Sarebbe bello se potessimo fare un ingrandimento sugli schermi piccoli, presentando un crop più alto e stretto.

Illustrazione animata che mostra quanto diventano piccole le immagini dei dettagli sugli schermi più stretti e quanti più dettagli otteniamo presentando un crop diverso.

Questo tipo di ritaglio su misura del contenuto dell’immagine perché si adatti ad ambienti specifici è detto “art direction”. Ogni volta che facciamo un crop o alteriamo un immagine perché sia adatta ad un breakpoint (piuttosto che fare un semplice ridimensionamento dell’intera immagine), stiamo facendo art direction.

Se includessimo i crop ingranditi in un srcset, non si potrebbe dire quando verrebbero scelti e quando no. Utilizzando picture e source media, possiamo rendere espliciti i nostri desideri: carica solo i crop rettangolari larghi quando la viewport è più larga di 36em. Nelle viewport più piccole, usa sempre quelle quadrate.

<picture>
	<!-- 16:9 crop -->
	<source
		media="(min-width: 36em)"
		srcset="quilt_2/detail/large.jpg  1920w,
		        quilt_2/detail/medium.jpg  960w,
		        quilt_2/detail/small.jpg   480w" />
	<!-- square crop -->
	<source
		srcset="quilt_2/square/large.jpg  822w,
		        quilt_2/square/medium.jpg 640w,
		        quilt_2/square/small.jpg  320w" />
	<img
		src="quilt_2/detail/medium.jpg"
		alt="Detail of the above quilt, highlighting the 
             embroidery and exotic stitchwork." />
</picture>

Un elemento picture contiene un qualsiasi numero di elementi sorgente e un’img. Il browser esamina le source di picture finché trova quella il cui attributo media si abbina all’ambiente attuale. Invia il corrispondente srcset della source all’img, che è ancora l’elemento che “vediamo” sulla pagina.

Ecco un caso più semplice:

<picture>
	<source media="(orientation: landscape)" srcset="landscape.jpg" />
	<img src="portrait.jpg" alt="A rad wolf." />
</picture>

Nelle viewport in modalità landscape, img riceve landscape.jpg. Quando siamo in protrait (o se il browser non supporta picture) img rimane così com’è e si carica portrait.jpg.

Questo comportamento può sorprendere in parte se si è abituati ad audio e video. A differenza di questi elementi, picture è un wrapper invisibile: uno span magico che semplicemente invia un srcset alla sua img.

Possiamo metterla anche in questi termini: img non è un fallback. Stiamo applicando il progressive enhancement a img inserendola in un elemento picture.

In pratica, questo significa che qualunque stile vogliamo applicare all’immagine che verrà visualizzata dovrà essere impostato su img, non su picture. picture { width: 100% } non fa alcunché. picture > img { width: 100% } fa quello che volete.

Ecco la nostra pagina di coperte pazze con applicato quel pattern. Tenendo a mente che il nostro scopo per l’utilizzo di picture era di fornire agli utenti su piccolo schermo più pixel (più significativi), vediamo come cambia la performance:

Un altro grafico che mostra il peso della pagina rispetto alla larghezza della finestra.

Niente male! Stiamo mandando pochi byte in più agli schermi 1x piccoli, ma per qualche complicata ragione avendo a che fare con le dimensioni delle nostre immagini sorgente, abbiamo in realtà esteso il range di dimensioni di schermo che vedo un risparmio a 2x. Il risparmio sulla pagina al primo passaggio si era fermato a 480px sugli schermi 2x, ma dopo il nostro secondo passaggio, continua fino a 700px.

La nostra pagina adesso si carica più rapidamente e appare migliore sui device più piccoli. E non abbiamo ancora finito!

Terzo step: formati multipli con source type#section5

I 25 anni di storia del web sono stati dominati da due formati bitmap: JPEG e GIF. C’è voluta una decade dolorosa per PNG prima di potersi unire al club esclusivo. Nuovi formati come WebP and JPEG XR stanno bussando alla porta, promettendo agli sviluppatori una miglior compressione e offrendo caratteristiche utili come gli alpha channels e modalità lossless. Ma a causa della tristemente singola src di img, la loro adozione è stata lenta: gli sviluppatori hanno bisogno di un supporto quasi universale per un formato prima di poterlo utilizzare. Non più ora. picture rende semplice offrire più formati seguendo lo stesso pattern source type stabilito da audio e video:

<picture>
	<source type="image/svg+xml" srcset="logo.svg" />
	<img src="logo.png" alt="RadWolf, Inc." />
</picture>

Se il browser supporta un type di source, manderà il srcset di quella source all’img.

Questo è un esempio chiaro, ma quando sovrapponiamo i cambiamenti di source type sulla nostra pagina esistente delle coperte pazze, ad esempio per aggiungere il supporto per WebP, le cose si fanno spinose (e ripetitive):

<picture>
	<!-- 16:9 crop -->
	<source
		type="image/webp"
		media="(min-width: 36em)"
		srcset="quilt_2/detail/large.webp  1920w,
		        quilt_2/detail/medium.webp  960w,
		        quilt_2/detail/small.webp   480w" />
	<source
		media="(min-width: 36em)"
		srcset="quilt_2/detail/large.jpg  1920w,
		        quilt_2/detail/medium.jpg  960w,
		        quilt_2/detail/small.jpg   480w" />
	<!-- square crop -->
	<source
		type="image/webp"
		srcset="quilt_2/square/large.webp   822w,
		        quilt_2/square/medium.webp  640w,
		        quilt_2/square/small.webp   320w" />
	<source
		srcset="quilt_2/square/large.jpg   822w,
		        quilt_2/square/medium.jpg  640w,
		        quilt_2/square/small.jpg   320w" />
	<img
		src="quilt_2/detail/medium.jpg"
		alt="Detail of the above quilt, highlighting the
                     embroidery and exotic stitchwork." />
</picture>

È davvero molto codice per un’unica immagine e stiamo inoltre gestendo un gran numero di files: 12! Tre risoluzioni, due formati e due crop per immagine sono davvero molti. Tutto quello che abbiamo guadagnato in performance e funzionalità ha avuto un costo pagato dalla complessità anticipata e dalla sostenibilità futura.

L’automazione è vostra amica: se la vostra pagina conterrà enormi blocchi di codice che fanno riferimento a numerose versioni alternative di un’immagine, sarà meglio che evitiate di scriverla a mano.

Si deve anche sapere quando troppo è troppo. Nel nostro esempio, ho usato tutti gli strumenti presenti nella specifica. Questa cosa non è quasi mai prudente. Si possono avere guadagni incredibili usando una qualsiasi delle nuove feature da sola e dovreste studiarvi bene le complessità derivanti dalla somma di ciascuna prima di estrarre le tenaglie e buttarvi nella riparazione dello scarico del lavandino.

Detto questo, diamo un’occhiata a quello che può fare WebP per le nostre coperte.

Il nostro terzo e ultimo grafico di peso della pagina contro larghezza della finestra.

Abbiamo un ulteriore 25-35% di risparmio in più oltre a quello che abbiamo già ottenuto, non solo nella parte bassa, ma su tutto il piano. Sicuramente non è da sottovalutare! La mia metodologia in questo caso non è per nulla rigorosa: la performance del vostro WebP potrebbe cambiare, ma il punto è: nuovi formati che possono dare benefici significativi rispetto allo status quo attuale costituito da JPEG/GIF/PNG e altri che continueranno ad arrivare. picture e source type abbassano la barriera in ingresso, spianando per sempre la strada per l’innovazione nei formati di immagine.

size the day#section6

Questo è il segreto della perfezione:

When raw wood is carved, it becomes a tool;

[…]

The perfect carpenter leaves no wood to be carved.

27. Perfection, Tao Te Ching

Per anni, abbiamo saputo cosa rallentava le nostre pagine responsive: le immagini, quelle grandi, create soprattutto per gli schermi molto grandi, che abbiamo inviato a tutti. È da un po’ che sappiamo come sistemare questo problema, inviando sorgenti diversi a client diversi. Il nuovo markup ci permette di fare proprio questo. srcset ci permette di offrire più versioni di un’immagine ai browser, che con un po’ di aiuto da sizes, sceglie la sorgente più appropriata da caricare tra quelle offerte. picture e source ci permettono di intervenire e di esercitare un po’ più di controllo, assicurandoci che verranno scelte determinate sorgenti in base o alle media query o al supporto del tipo di file.

Insieme, queste feature ci permetto di creare un markup adattabile, flessibile e responsive per le immagini. Ci permettono di mandare a ciascuno dei nostri utenti una sorgente su misura per il suo device, permettendo immensi guadagni in termini di performance. Armati di un eccellente polyfill e di un occhio verso il futuro, gli sviluppatori dovrebbero cominciare ad usare questo markup ora!

Illustrazioni: {carlok}

Nessun commento

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Altro da ALA

Webwaste

In questo estratto da World Wide Waste, Gerry McGovern esamina l'impatto ambientale di siti web pieni zeppi di asset inutili. Digital is physical. Sembra economico e gratis ma non lo è: ci costa la Terra.
Industry