L'effetto sfocatura è un ottimo modo per reindirizzare l'attenzione di un utente. Se alcuni elementi visivi appaiono sfocati, mentre altri rimangono a fuoco, l'attenzione dell'utente viene attirata in modo naturale. Gli utenti ignorano i contenuti sfocati e si concentrano su quelli che possono leggere. Un esempio è un elenco di icone che mostrano i dettagli dei singoli elementi quando si passa il mouse sopra. Durante questo periodo, le scelte rimanenti potrebbero essere sfocate per reindirizzare l'utente alle informazioni appena visualizzate.
TL;DR
L'animazione di una sfocatura non è un'opzione molto praticabile perché è molto lenta. Al contrario, precompila una serie di versioni sempre più sfocate e applica una transizione graduale tra di esse. Il mio collega Yi Gu ha scritto una libreria che si occupa di tutto per te. Dai un'occhiata alla nostra demo.
Tuttavia, questa tecnica può essere piuttosto spiacevole se applicata senza un periodo di transizione. Animare un'immagine sfocata, passando da una versione non sfocata a una sfocata, sembra una scelta ragionevole, ma se hai mai provato a farlo sul web, probabilmente hai notato che le animazioni sono tutt'altro che fluide, come mostra questa demo se non hai un computer potente. Possiamo fare di meglio?
Il problema
Al momento, non è possibile animare un'effetto sfocatura in modo efficiente. Tuttavia, possiamo trovare una soluzione alternativa che sembra abbastanza buona, ma che tecnicamente non è un'apposita sfocatura animata. Per iniziare, capiamo perché la sfocatura animata è lenta. Esistono due tecniche per sfocare gli elementi sul web: la proprietà CSS filter
e i filtri SVG. Grazie al maggiore supporto e alla facilità d'uso, in genere vengono utilizzati i filtri CSS. Purtroppo, se devi supportare Internet Explorer, non hai altra scelta che utilizzare i filtri SVG, poiché IE 10 e 11 li supportano, ma non i filtri CSS. La buona notizia è che la nostra soluzione alternativa per animare un effetto sfocatura funziona con entrambe le tecniche. Proviamo a trovare il collo di bottiglia esaminando DevTools.
Se attivi "Sfarfallio della pittura" in DevTools, non vedrai alcun sfarfallio. Sembra che non ci siano ridisegni. E questo è tecnicamente corretto, poiché "ridipingere" si riferisce al fatto che la CPU deve ridipingere la trama di un elemento promosso. Ogni volta che un elemento viene sia promosso che sfocato, la sfocatura viene applicata dalla GPU utilizzando uno shader.
Sia i filtri SVG che i filtri CSS utilizzano i filtri di convoluzione per applicare una sfocatura. I filtri di convolvezione sono piuttosto costosi perché per ogni pixel di output è necessario prendere in considerazione un numero di pixel di input. Maggiore è l'immagine o maggiore è il raggio di sfocatura, maggiore è il costo dell'effetto.
Ed è qui che sta il problema: stiamo eseguendo un'operazione GPU piuttosto dispendiosa ogni frame, superando il budget di 16 ms per frame e ottenendo quindi un framerate ben al di sotto dei 60 fps.
Nella tana del bianconiglio
Cosa possiamo fare per risolvere il problema? Possiamo usare giochi di prestigio. Anziché animare il valore effettivo della sfocatura (il raggio della sfocatura), precomponiamo un paio di copie sfocate in cui il valore della sfocatura aumenta in modo esponenziale, quindi effettuiamo la transizione tra le due usando opacity
.
La transizione in dissolvenza è una serie di dissolvenze in entrata e in uscita dell'opacità sovrapposte. Ad esempio, se abbiamo quattro fasi di sfocatura, svaniamo la prima fase e allo stesso tempo facciamo apparire la seconda. Quando la seconda fase raggiunge l'opacità del 100% e la prima raggiunge lo 0%, la seconda fase viene attenuata mentre viene attenuata la terza fase. Al termine, svaniamo la terza fase e facciamo apparire la quarta e ultima versione. In questo scenario, ogni fase richiederebbe ¼ della durata totale desiderata. Visivamente, è molto simile a una sfocatura reale e animata.
Nei nostri esperimenti, l'aumento esponenziale del raggio di sfocatura per ogni fase ha prodotto i risultati visivi migliori. Esempio: se abbiamo quattro fasi di sfocatura, applichiamo
filter: blur(2^n)
a ogni fase, ovvero fase 0: 1px, fase 1: 2px, fase 2: 4px
e fase 3: 8px. Se forziamo ciascuna di queste copie sfocate sul proprio livello
(chiamata "promozione") utilizzando will-change: transform
, la modifica dell'opacità su questi
elementi dovrebbe essere super veloce. In teoria, questo ci consentirebbe di caricare in anticipo il lavoro costoso di sfocatura. A quanto pare, la logica è sbagliata. Se esegui questa demo, noterai che la frequenza dei fotogrammi è ancora inferiore a 60 fps e che la sfocatura è addirittura peggiore di prima.
Un rapido sguardo a DevTools rivela che la GPU è ancora estremamente impegnata e allunga ogni frame a circa 90 ms. Ma perché? Non stiamo più modificando il valore della sfocatura, ma solo l'opacità. Che cosa succede? Il problema sta ancora una volta nella natura dell'effetto sfocatura: come spiegato in precedenza, se l'elemento è promosso e sfocato, l'effetto viene applicato dalla GPU. Pertanto, anche se non stiamo più animando il valore di sfocatura, la texture stessa non è ancora sfocata e deve essere sfocata di nuovo a ogni frame dalla GPU. Il motivo per cui la frequenza frame è persino peggiore di prima deriva dal fatto che, rispetto all'implementazione ingenua, la GPU ha effettivamente più lavoro rispetto a prima, poiché la maggior parte del tempo sono visibili due texture che devono essere sfocate in modo indipendente.
La soluzione che abbiamo trovato non è molto bella, ma rende l'animazione incredibilmente veloce. Torniamo a non promuovere l'elemento da sfocare, ma promuoviamo un wrapper principale. Se un elemento è sia sfocato che in evidenza, l'effetto viene applicato dalla GPU. Questo è il motivo per cui la nostra demo è stata lenta. Se l'elemento è sfocato, ma non è stato promosso, lo sfocamento viene rasterizzato nella trama principale più vicina. Nel nostro caso, si tratta dell'elemento wrapper principale promosso. L'immagine sfocata ora è la texture dell'elemento principale e può essere riutilizzata per tutti i fotogrammi futuri. Questo funziona solo perché sappiamo che gli elementi sfocati non sono animati e memorizzarli nella cache è effettivamente utile. Ecco una demo che implementa questa tecnica. Chissà cosa ne pensa Moto G4 di questo approccio? Spoiler: pensa di essere fantastico:
Ora abbiamo molto margine sulla GPU e una fluidità a 60 fps. Ce l'abbiamo fatta!
Implementazione in produzione
Nella nostra demo, abbiamo duplicato una struttura DOM più volte per avere copie dei contenuti da sfocare con intensità diverse. Potresti chiederti come funzionerebbe in un ambiente di produzione, poiché potrebbe avere alcuni effetti collaterali indesiderati con gli stili CSS o addirittura con il codice JavaScript dell'autore. Hai ragione. Entra nel DOM ombra.
Sebbene la maggior parte delle persone pensi allo shadow DOM come a un modo per collegare elementi "interni" ai propri elementi personalizzati, è anche un elemento di isolamento e prestazioni. JavaScript e CSS non possono oltrepassare i confini dello Shadow DOM, il che ci consente di duplicare i contenuti senza interferire con gli stili o la logica di applicazione dello sviluppatore. Abbiamo già un elemento <div>
su cui eseguire la rasterizzazione di ogni copia e ora utilizziamo questi <div>
come host shadow. Creiamo un ShadowRoot
utilizzando attachShadow({mode: 'closed'})
e alleghiamo una copia dei contenuti al ShadowRoot
anziché al <div>
stesso. Dobbiamo assicurarci di copiare anche tutti gli stili in ShadowRoot
per garantire che le nostre copie abbiano lo stesso stile dell'originale.
Alcuni browser non supportano Shadow DOM 1.0 e, per questi, ricorriamo alla semplice duplicazione dei contenuti e speriamo che non si verifichino problemi. Potremmo utilizzare il polyfill DOM ombra con ShadyCSS, ma non lo abbiamo implementato nella nostra libreria.
Ecco fatto. Dopo aver esplorato la pipeline di rendering di Chrome, abbiamo capito come animare le sfocature in modo efficiente su tutti i browser.
Conclusione
Questo tipo di effetto non deve essere usato alla leggera. Poiché copiamo gli elementi DOM e li forziamo nel loro livello, possiamo spingere i limiti dei dispositivi di fascia bassa. Anche la copia di tutti gli stili in ogni ShadowRoot
rappresenta un potenziale rischio per le prestazioni, quindi devi decidere se preferisci modificare la logica e gli stili in modo che non siano interessati dalle copie nel LightDOM
o utilizzare la nostra tecnica ShadowDOM
. Tuttavia, a volte la nostra tecnica potrebbe essere un investimento utile. Dai un'occhiata al codice nel nostro repository GitHub, nonché alla demo e contattami su Twitter se hai domande.