Die „Blur Up“-Technik zum Laden von Hintergrundbildern

Avatar of Emil Björklund
Emil Björklund am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Der folgende Beitrag ist ein Gastbeitrag von Emil Björklund. Filtereffekte in CSS gibt es schon eine Weile, und zusammen mit Dingen wie Mischmodi eröffnen sie neue Möglichkeiten, Elemente im Browser nachzubilden und zu manipulieren, die wir früher in Photoshop machen mussten. Hier untersucht Emil eine Leistungstechnik, die einen der vergesseneren Filtereffekte – die filter()-Funktion – nutzt und sie auch mit SVG nachbildet.

Das alles beginnt mit einem Artikel des Facebook-Engineering-Teams, wie sie Vorschaubilder für Titelbilder laden in ihren nativen Apps. Das Problem, mit dem sie konfrontiert waren, ist, dass diese „Titelbilder“ groß sind und oft eine Weile zum Laden brauchen, was dem Benutzer ein weniger als ideales Erlebnis hinterlässt, wenn der Hintergrund plötzlich von einer Volltonfarbe zu einem Bild wechselt.

Dies gilt insbesondere bei geringer Konnektivität oder mobilen Netzwerken, bei denen man oft auf eine leere graue Box starrt, während man auf den Download von Bildern wartet.

Idealerweise wäre das Bild in die ursprüngliche API-Antwort ihrer App beim Abrufen der Profildaten kodiert. Aber um in diese Anfrage zu passen, müsste das Bild auf 200 Bytes begrenzt sein. Problematisch, da Titelbilder über 100 Kilobytes groß sind.

Wie holt man also etwas Wertvolles aus 200 Bytes heraus und wie zeigt man dem Benutzer etwas an, bevor das Bild vollständig geladen ist?

Die (geniale) Lösung war, ein winziges Bild (ca. 40 Pixel breit) zurückzugeben und dieses winzige Bild dann hochzuskalieren und einen Gaußschen Weichzeichner anzuwenden. Dies zeigt sofort einen Hintergrund, der ästhetisch ansprechend aussieht und eine Vorschau darauf gibt, wie das Titelbild aussehen würde. Das eigentliche Titelbild könnte dann rechtzeitig im Hintergrund geladen und nahtlos eingeblendet werden. Klug gedacht!

Diese Technik hat ein paar coole Aspekte:

  1. Sie macht die wahrgenommene Ladezeit unglaublich schnell.
  2. Sie nutzt etwas, das traditionell teuer in Bezug auf die Leistung ist, um die Leistung zu verbessern.
  3. Sie ist im Web machbar.

Große Header-Hintergrundbilder (und ihre Leistungsprobleme) sind definitiv etwas, womit wir uns beim Erstellen für das Web auseinandersetzen können, also sind das nützliche Dinge. Wir versuchen vielleicht, den Download von großen Bildern zu vermeiden, aber manchmal gehen wir den Kompromiss ein, um eine bestimmte Stimmung zu erzielen. Das Beste, was wir in dieser Situation tun können, ist, die wahrgenommene Leistung zu optimieren, also können wir diese Technik genauso gut stehlen.

Ein funktionierendes Beispiel

Wir werden diese Header-Bildfunktion mit einem Ansatz vom Typ „kritische CSS“ nachbilden. Die allererste Anfrage lädt das winzige Bild in Inline-CSS, dann kommt das hochauflösende Hintergrundbild nach dem ersten Rendern.

Es wird ungefähr so aussehen, wenn es fertig geladen ist

In diesem Beispiel verwenden wir ein Hintergrundbild und betrachten es als Dekoration und nicht als Teil des Inhalts. Es gibt einige feinere Punkte zu diskutieren, wann diese Arten von Bildern als Inhalt angesehen werden (und somit als <img> kodiert werden) und wann sie Hintergrundbilder sind. Um clevere Größenmodi (wie die CSS-Werte cover und contain) nutzen zu können, sind Hintergrundbilder wahrscheinlich die gängigste Lösung für diese Art von Designs, aber neue Eigenschaften wie object-fit machen denselben Ansatz für Inhaltsbilder etwas einfacher. Seiten wie Medium verwenden bereits weichgezeichnete Inhaltsbilder, um Ladezeiten zu verbessern, aber die Nützlichkeit dieser Technik ist diskutierbar – bringen die weichgezeichneten Bilder etwas, wenn die Ladetechnik fehlschlägt? Wie auch immer: In diesem Artikel konzentrieren wir uns auf diese Technik, wie sie auf Hintergrundbilder angewendet wird.

Hier ist die Übersicht, wie es funktionieren wird:

  1. Inline ein winziges Vorschaubild (40×22 Pixel) als base64-kodiertes Hintergrundbild innerhalb eines <style>-Tags. Der Style-Tag enthält auch allgemeine Styling- und die Regeln für die Anwendung eines Gaußschen Weichzeichners auf das Hintergrundbild. Schließlich enthält er Stile für die größere Version des Header-Bildes, die auf einen anderen Klassennamen beschränkt sind.
  2. Holen Sie sich die URL zum großen Bild aus dem Inline-CSS und laden Sie es vorab mit JavaScript. Wenn das Skript aus irgendeinem Grund fehlschlägt, kein Schaden, kein Foul – das weichgezeichnete Hintergrundbild ist immer noch da und sieht ziemlich cool aus.
  3. Wenn das große Bild geladen ist, fügen Sie einen Klassennamen hinzu, der das CSS umschaltet, um das große Bild als Hintergrund zu verwenden und den Weichzeichner zu entfernen. Hoffentlich kann das Entfernen des Weichzeichners auch animiert werden.

Sie finden das endgültige Beispiel als Pen. Sie werden wahrscheinlich das weichgezeichnete Bild für einen Moment sehen, bevor das schärfere Bild geladen wird. Wenn nicht, versuchen Sie, die Seite mit einem leeren Cache neu zu laden.

Ein winziges, optimiertes Bild

Zunächst benötigen wir eine Vorschaubild-Version des Bildes. Facebook hat die Größe ihrer Bilder durch Kompressionszauberei auf 200 Bytes reduziert (z. B. Speichern der unveränderlichen JPEG-Header-Teile in der App), aber wir werden nicht ganz so extrem vorgehen. Mit einer Größe von 40 x 22 Pixeln kommt dieses spezielle Bild nach der Verarbeitung durch einige Bildoptimierungssoftware auf etwa 1000 Bytes.

Das vollständige JPEG-Bild ist etwa 120 KB bei 1500 × 823 Pixeln. Diese Dateigröße könnte wahrscheinlich deutlich niedriger sein, aber wir belassen sie so, da dies ein Proof of Concept ist. In einem realen Beispiel hätten Sie wahrscheinlich einige Größenvarianten des Bildes und würden je nach Ansichtsfenstergröße eine andere laden – vielleicht sogar ein anderes Format wie WebP.

Die filter-Funktion für Bilder

Als Nächstes möchten wir das winzige Bild so skalieren, dass es das Element abdeckt, aber wir wollen nicht, dass es pixelig und hässlich aussieht. Hier kommt die filter()-Funktion ins Spiel. Filter in CSS können etwas verwirrend erscheinen, da es effektiv drei Arten gibt: die filter-Eigenschaft, ihr vorgeschlagenes backdrop-filter-Gegenstück (in der Filter Effects Level 2-Spezifikation) und schließlich die filter()-Funktion für Bilder. Werfen wir zunächst einen Blick auf die Eigenschaft.

.myThing {
  filter: hue-rotate(45deg);
}

Ein oder mehrere Filter werden angewendet, wobei jeder auf dem Ergebnis des vorherigen basiert – sehr ähnlich einer Liste von Transformationen. Es gibt eine ganze Reihe vordefinierter Filter, die wir verwenden können: blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), sepia() und saturate().

Noch cooler ist, dass dies eine Spezifikation ist, die zwischen CSS und SVG geteilt wird. Nicht nur sind die vordefinierten Filter in Bezug auf SVG spezifiziert, wir können auch unsere eigenen Filter in SVG erstellen und sie aus CSS referenzieren.

.myThing {
  filter: url(myfilter.svg#myCustomFilter);
}

Dieselben Filtereffekte sind in backdrop-filter gültig und werden beim Compositing eines transparenten Elements mit seinem Hintergrund angewendet – vielleicht am nützlichsten für die Erstellung des „Milchglas“-Effekts.

Schließlich gibt es die filter()-Funktion für Bildwerte. Die Idee ist, dass überall dort, wo Sie ein Bild in CSS referenzieren können, Sie es auch durch eine Liste von Filtern leiten können. Für das winzige Header-Bild betten wir es als Base64 DataURI ein und führen es durch den blur()-Filter.

.post-header {
  background-image: filter(url(data:image/jpeg;base64,/9j/4AAQ ...[truncated] ...), blur(20px));
}

Das ist großartig, da dies genau das ist, wonach wir suchen, wenn wir die Technik aus der Facebook-App nachbilden! Allerdings gibt es schlechte Nachrichten in Bezug auf die Unterstützung. Die filter-Eigenschaft wird in den neuesten Versionen aller Browser außer IE unterstützt, aber keiner davon außer WebKit hat den filter()-Funktionsteil der Spezifikation implementiert.

Wenn ich hier WebKit sage, meine ich die WebKit-Nightly-Builds zum Zeitpunkt der Erstellung dieses Beitrags und nicht Safari. Die filter-Funktion für Bilder ist technisch in iOS9 als -webkit-filter() enthalten, aber das wurde soweit ich finden kann nirgends offiziell gemeldet, was etwas seltsam ist. Der Grund dafür ist wahrscheinlich ein furchtbarer Bug mit background-size: Das Originalbild wird nicht neu skaliert, aber die gekachelte Größe des gefilterten Ausgabebildes. Das bricht die Funktionalität von Hintergrundbildern ziemlich stark, besonders mit Weichzeichnung. Es wurde behoben, aber nicht rechtzeitig für die Safari 9-Version, also gehe ich davon aus, dass sie diese Funktion nicht ankündigen wollten.

Aber was machen wir mit der fehlenden/kaputten filter()-Funktionalität? Wir könnten entweder Browsern, die sie nicht unterstützen, einen Volltonhintergrund geben, bis das Bild geladen ist, obwohl das bedeutet, dass sie gar keinen Hintergrund erhalten, wenn JS fehlschlägt. Langweilig!

Nein, wir behalten die filter()-Funktion als zusätzliches Gewürz für die Animation des später eingeblendeten Bildes auf und emulieren stattdessen die Filterfunktion für das anfängliche Bild mit SVG.

Nachbildung des Weichzeichnerfilters mit SVG

Da die Spezifikation praktisch eine SVG-Entsprechung für den blur()-Filter bietet, können wir nachbilden, wie der Weichzeichnerfilter in SVG funktioniert, mit ein paar Anpassungen.

  • Die Ränder werden beim Anwenden des Gaußschen Weichzeichners etwas halbtransparent. Wir können dies beheben, indem wir einen sogenannten feComponentTransfer-Filter hinzufügen. Die Komponentenübertragung ermöglicht es Ihnen, jeden Farbkanal (einschließlich Alpha) einer Quellgrafik zu manipulieren. Diese spezielle Variante verwendet das feFuncA-Element, das jeden Wert zwischen 0 und 1 im Alpha-Kanal auf 1 abbildet, was bedeutet, dass es jede Alpha-Transparenz entfernt.
  • Das Attribut color-interpolation-filters auf dem <filter>-Element muss auf sRGB gesetzt werden. SVG-Filter verwenden standardmäßig den linearRGB-Farbraum, und CSS arbeitet in sRGB. Die meisten Browser scheinen die Farbkorrekturen richtig zu handhaben, aber Safari/WebKit macht die Farben ausgewaschen, es sei denn, dieser Wert ist gesetzt.
  • Die filterUnits ist auf userSpaceOnUse gesetzt, was vereinfacht ausgedrückt bedeutet, dass Koordinaten und Längen (wie die stdDeviation des Weichzeichners) Pixel auf dem Element abbilden, auf das wir den Weichzeichner anwenden.

Der resultierende SVG-Code sieht ungefähr so aus:

<filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
    <feGaussianBlur stdDeviation="20" edgeMode="duplicate" />
    <feComponentTransfer>
      <feFuncA type="discrete" tableValues="1 1" />
    </feComponentTransfer>
</filter>

Die filter-Eigenschaft verwendet ihre eigene url()-Funktion, in der wir entweder einen SVG-Filter referenzieren oder URI-kodieren können. Wie wenden wir also einen Filter auf etwas innerhalb eines background-image: url(...) an?

Nun, SVG-Dateien können auf andere Bilder verweisen, und wir können Filter auf diese Bilder innerhalb des SVG anwenden. Das Problem ist, dass SVG-Hintergrundbilder keine externen Ressourcen abrufen können. Aber wir können dies umgehen, indem wir das JPG innerhalb des SVG base64-kodieren. Dies wäre bei einem großen Bild nicht praktikabel, aber für unser winziges sollte es in Ordnung sein. Das SVG wird so aussehen:

<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     width="1500" height="823"
     viewBox="0 0 1500 823">
  <filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
    <feGaussianBlur stdDeviation="20 20" edgeMode="duplicate" />
    <feComponentTransfer>
      <feFuncA type="discrete" tableValues="1 1" />
    </feComponentTransfer>
  </filter>
  <image filter="url(#blur)"
         xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJ ...[truncated]..."
         x="0" y="0"
         height="100%" width="100%"/>
</svg>

Ein weiterer Nachteil (im Vergleich zur reinen Verwendung der filter()-Funktion mit einer Bitmap) ist, dass wir einige Größen manuell festlegen müssen, damit das SVG richtig mit der Hintergrundgröße zusammenarbeitet. Das SVG selbst hat eine viewBox, die eingestellt ist, um das Seitenverhältnis des Bildes nachzuahmen, und die Eigenschaften width und height sind auf dieselben Maße gesetzt, um sicherzustellen, dass es browserübergreifend funktioniert (z. B. IE verdirbt das Seitenverhältnis, wenn diese fehlen). Schließlich ist das <image>-Element so eingestellt, dass es die gesamte SVG-Leinwand abdeckt.

Jetzt können wir diese Datei als Hintergrund für den Post-Header verwenden, und es wird ungefähr so aussehen:

Als letzten Schritt können wir das SVG-Wrapper-Bild inline in das CSS einfügen, um eine zusätzliche Anfrage zu vermeiden. Inline-SVG muss URI-kodiert sein, ich benutze yoksel's SVG-Encoder dafür. Jetzt haben wir eine dataURI, die eine andere dataURI enthält. DataURInception!

Beim Kodieren des SVG erhalten wir Text zum Einfügen in die url(), aber es ist erwähnenswert, dass wir einige Metadaten voranstellen müssen, damit es angezeigt wird: data:image/svg+xml;charset=utf-8,. Das charset-Zeug ist wichtig: Es sorgt dafür, dass das kodierte SVG über Browser hinweg gut funktioniert.

.post-header {
  background-color: #567DA7;
  background-size: cover;
  background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg...);
}

Zu diesem Zeitpunkt beträgt die gesamte Seite, einschließlich des Bildes, 1 Anfrage und 5KB bei Verwendung von GZIP.

Abrufen der URL für das große Bild

Als Nächstes erstellen wir eine Regel für den erweiterten Header, in der wir das riesige Hintergrundbild einrichten.

.post-header-enhanced {
  background-image: url(largeimg.jpg);
}

Anstatt nur den Klassennamen umzuschalten, wodurch das große Bild geladen wird, möchten wir das große Bild vorab laden und dann den Klassennamen anwenden. Dies geschieht, damit wir den Wechsel später reibungslos animieren können, in der vernünftigen Annahme, dass das große Bild fertig geladen ist. Da wir die Bild-URL nicht sowohl im CSS als auch in JavaScript hart kodieren wollen, holen wir uns die URL mit JavaScript aus den Stilen. Da der Klassename noch nicht angewendet ist, können wir nicht einfach nach headerElement.style.backgroundImage usw. suchen – es kennt den Hintergrund noch nicht. Um dies zu lösen, verwenden wir das CSSOM – das CSS-Objektmodell und die schreibgeschützten JS-Eigenschaften, die es uns ermöglichen, die CSS-Regeln zu durchlaufen.

Der folgende Schnipsel findet den Klassennamen für den erweiterten Header, extrahiert dann die URL mit etwas Regex. Danach lädt er das Bild vorab und löst den hinzugefügten Klassennamen aus, sobald dies geschehen ist.

<script>
window.onload = function loadStuff() {
  var win, doc, img, header, enhancedClass;
  
  // Quit early if older browser (e.g. IE 8).
  if (!('addEventListener' in window)) {
    return;
  }
  
  win = window;
  doc = win.document;
  img = new Image();
  header = doc.querySelector('.post-header');
  enhancedClass = 'post-header-enhanced';

  // Rather convoluted, but parses out the first mention of a background
  // image url for the enhanced header, even if the style is not applied.
  var bigSrc = (function () {
    // Find all of the CssRule objects inside the inline stylesheet 
    var styles = doc.querySelector('style').sheet.cssRules;
    // Fetch the background-image declaration...
    var bgDecl = (function () {
      // ...via a self-executing function, where a loop is run
      var bgStyle, i, l = styles.length;
      for (i=0; i<l; i++) {
        // ...checking if the rule is the one targeting the
        // enhanced header.
        if (styles[i].selectorText &&
            styles[i].selectorText == '.'+enhancedClass) {
          // If so, set bgDecl to the entire background-image
          // value of that rule
          bgStyle = styles[i].style.backgroundImage;
          // ...and break the loop.
          break; 
        }
      }
      // ...and return that text.
      return bgStyle;
    }());
    // Finally, return a match for the URL inside the background-image
    // by using a fancy regex I Googled up, as long as the bgDecl 
    // variable is assigned at all.         
    return bgDecl && bgDecl.match(/(?:\(['|"]?)(.*?)(?:['|"]?\))/)[1];
  }());

  // Assign an onLoad handler to the dummy image *before* assigning the src
  img.onload = function () {
    header.className += ' ' +enhancedClass;
  };
  // Finally, trigger the whole preloading chain by giving the dummy
  // image its source.
  if (bigSrc) {
    img.src = bigSrc;
  }
};
</script>

Das Skript wird frühzeitig beendet, wenn addEventListener nicht unterstützt wird, was schön mit dem Rest der benötigten Unterstützung übereinstimmen sollte. Soweit ich das beurteilen kann, unterstützen alle einigermaßen modernen SVG-fähigen Browser den Rest des CSSOM und andere verwendete JavaScript-Funktionen.

Animation des Wechsels

Es ist ein bisschen enttäuschend, dass wir die filter()-Funktion nicht verwenden konnten, nachdem wir herausgefunden hatten, dass sie existiert und alles. Also fügen wir einen animierten Effekt hinzu, wenn wir das hochauflösende Bild einwechseln. Dies funktioniert derzeit nur in WebKit-Nightlies, und wir können die @supports-Regel sicher verwenden, um die Änderungen zu begrenzen. Hier ist ein animiertes GIF, das den Effekt in Aktion zeigt:

Beachten Sie, dass wir hierfür keine transition verwenden können: Die filter()-Funktion ist animierbar, aber nur für Werteänderungen in der Filterkette – wenn sich das Hintergrundbild ändert, haben wir Pech gehabt. Wir können jedoch eine Animation dafür verwenden, aber das bedeutet, dass wir die URL des Hintergrundbilds noch zweimal wiederholen müssen, als Start- und Endwerte. Ein kleiner Preis dafür.

Hier sind die CSS-Stile für die erweiterten Header-Stile für Browser, die die filter()-Funktion verstehen:

@supports (background-image: filter(url('i.jpg'), blur(1px))) {
  .post-header {
    transform: translateZ(0);
  }
  .post-header-enhanced {
    animation: sharpen .5s both;
  }
  @keyframes sharpen {
    from {
      background-image: filter(largeimg.jpg), blur(20px));
    }
    to {
      background-image: filter(largeimg.jpg), blur(0px));
    }
  }
}

Ein letztes Detail ist der translateZ(0)-Trick auf dem Header hier: Ohne ihn ist die Animation verrückt ruckelig. Ich habe versucht, ganz modern zu sein und will-change: background-image zu verwenden, aber das überzeugte den Browser nicht, eine Hardware-beschleunigte Ebene zu erstellen, also musste ich auf den alten Trick zurückgreifen, einen 3D-„Null-Transformations-Trick“ hinzuzufügen.

Schnelle, progressiv verbesserte Hintergrundbilder

Da haben wir es, eine Seite mit einem riesigen Hintergrundbild (wenn auch unscharf) lädt in 5 KB, lazy-loading das scharf aussehende Vollbildbild. Im Moment kann nur WebKit das schärfere Bild animieren, aber ich hoffe, dass andere Browser die filter()-Funktion bald implementieren werden. Ich bin sicher, dass wir sie noch für viele weitere unterhaltsame Techniken nutzen können.