Lazy Loading von Bildern in Svelte

Avatar of Donovan Hutchinson
Donovan Hutchinson am

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

Eine einfache Möglichkeit, die Geschwindigkeit einer Website zu verbessern, ist das Herunterladen von Bildern nur dann, wenn sie benötigt werden, also wenn sie in den Viewport gelangen. Diese „Lazy Loading“-Technik gibt es schon eine Weile und es gibt viele tolle Tutorials zur Implementierung.

Aber selbst mit all den verfügbaren Ressourcen kann die Implementierung von Lazy Loading je nach Projekt oder verwendetem Framework unterschiedlich aussehen. In diesem Artikel werde ich die Intersection Observer API zusammen mit dem onLoad-Ereignis verwenden, um Bilder mit dem Svelte JavaScript Framework lazy zu laden.

Schauen Sie sich Tristram Tollidays Einführung in Svelte an, wenn Sie neu im Framework sind.

Lassen Sie uns mit einem realen Beispiel arbeiten

Ich habe diesen Ansatz bei der Geschwindigkeitsprüfung einer Svelte- und Sapper-Anwendung entwickelt, an der ich arbeite, Shop Ireland. Eines unserer Ziele ist es, die Anwendung so schnell wie möglich zu machen. Wir erreichten einen Punkt, an dem die Homepage unter einer schlechten Performance litt, weil der Browser eine Menge Bilder herunterlud, die nicht einmal auf dem Bildschirm waren. Also haben wir uns natürlich dafür entschieden, sie stattdessen lazy zu laden.

Svelte ist bereits ziemlich schnell, da der gesamte Code im Voraus kompiliert wird. Aber sobald wir das Lazy Loading für Bilder hinzugefügt haben, begann die Leistung wirklich zu steigen.

Das ist es, woran wir gemeinsam arbeiten werden. Schnappen Sie sich gerne den finalen Code für diese Demo von GitHub und lesen Sie weiter, um eine Erklärung zu erhalten, wie es funktioniert.

Hier werden wir am Ende landen

Lassen Sie uns schnell Svelte starten

Sie haben vielleicht bereits eine Svelte-App, die Sie verwenden möchten, aber wenn nicht, lassen Sie uns ein neues Svelte-Projekt starten und lokal daran arbeiten. Von der Kommandozeile aus

npx degit sveltejs/template my-svelte-project
cd my-svelte-project
npm install
npm run dev

Sie sollten nun eine Anfänger-App unter https://:5000 laufen haben.

Hinzufügen des Komponentenordners

Die anfängliche Svelte-Demo hat eine App.svelte-Datei, aber noch keine Komponenten. Lassen Sie uns die Komponenten einrichten, die wir für diese Demo benötigen. Es gibt keinen Komponentenordner, also erstellen wir einen im src-Ordner. Innerhalb dieses Ordners erstellen wir einen Image-Ordner – hier werden unsere Komponenten für diese Demo gespeichert.

Wir werden unsere Komponenten zwei Dinge tun lassen. Erstens prüfen sie, wann ein Bild in den Viewport gelangt. Dann, wenn ein Bild eintritt, warten die Komponenten, bis die Bilddatei geladen ist, bevor sie angezeigt wird.

Die erste Komponente ist ein <IntersectionObserver>, das die zweite Komponente, einen <ImageLoader>, umschließt. Was mir an diesem Setup gefällt, ist, dass jede Komponente sich darauf konzentrieren kann, eine Sache zu tun, anstatt zu versuchen, eine Menge von Operationen in einer einzigen Komponente zu packen.

Beginnen wir mit der <IntersectionObserver>-Komponente.

Beobachten der Schnittmenge

Unsere erste Komponente wird eine funktionierende Implementierung der Intersection Observer API sein. Der Intersection Observer ist eine ziemlich komplexe Sache, aber im Grunde genommen beobachtet er ein Kind-Element und informiert uns, wenn es in die Bounding Box seines Elternelements eintritt. Bilder können Kindelemente eines Elternelements sein, und wir können eine Benachrichtigung erhalten, wenn sie in Sicht kommen.

Obwohl es definitiv eine gute Idee ist, sich mit den Feinheiten der Intersection Observer API vertraut zu machen – und Travis Almand hat eine ausgezeichnete Abhandlung darüber –, werden wir eine praktische Svelte-Komponente verwenden, die Rich Harris für svelte.dev erstellt hat.

Wir werden dies zuerst einrichten, bevor wir uns damit befassen, was genau es tut. Erstellen Sie eine neue IntersectionObserver.svelte-Datei und legen Sie sie in den Ordner src/components/Image. Hier definieren wir die Komponente mit dem folgenden Code

<script>
  import { onMount } from 'svelte';


  export let once = false;
  export let top = 0;
  export let bottom = 0;
  export let left = 0;
  export let right = 0;


  let intersecting = false;
  let container;


  onMount(() => {
    if (typeof IntersectionObserver !== 'undefined') {
      const rootMargin = `${bottom}px ${left}px ${top}px ${right}px`;


      const observer = new IntersectionObserver(entries => {
        intersecting = entries[0].isIntersecting;
        if (intersecting && once) {
          observer.unobserve(container);
        }
      }, {
        rootMargin
      });


      observer.observe(container);
      return () => observer.unobserve(container);
    }


    // The following is a fallback for older browsers
    function handler() {
      const bcr = container.getBoundingClientRect();


      intersecting = (
        (bcr.bottom + bottom) > 0 &&
        (bcr.right + right) > 0 &&
        (bcr.top - top) < window.innerHeight &&
        (bcr.left - left) < window.innerWidth
      );


      if (intersecting && once) {
        window.removeEventListener('scroll', handler);
      }
    }


    window.addEventListener('scroll', handler);
    return () => window.removeEventListener('scroll', handler);
  });
</script>


<style>
  div {
    width: 100%;
    height: 100%;
  }
</style>


<div bind:this={container}>
  <slot {intersecting}></slot>
</div>

Wir können diese Komponente als Wrapper um andere Komponenten verwenden, und sie ermittelt für uns, ob die umschlossene Komponente mit dem Viewport geschnitten wird.

Wenn Sie mit der Struktur von Svelte-Komponenten vertraut sind, werden Sie feststellen, dass sie einem Muster folgt, das mit Skripten beginnt, zu Stilen übergeht und mit Markup endet. Sie legt einige Optionen fest, die wir übergeben können, einschließlich einer once-Eigenschaft, zusammen mit numerischen Werten für die Abstände oben, rechts, unten und links vom Bildschirmrand, die den Punkt definieren, an dem die Schnittmenge beginnt.

Wir werden die Abstände ignorieren, aber stattdessen die once-Eigenschaft verwenden. Dies stellt sicher, dass die Bilder nur einmal geladen werden, wenn sie in den Viewport eintreten.

Die Hauptlogik der Komponente befindet sich im onMount-Abschnitt. Dies richtet unseren Observer ein, der verwendet wird, um unser Element zu überprüfen und zu bestimmen, ob es mit dem sichtbaren Bereich des Bildschirms „geschnitten“ wird.

Für ältere Browser wird außerdem ein Scroll-Ereignis angehängt, um zu prüfen, ob das Element beim Scrollen sichtbar ist, und dann wird dieser Listener entfernt, wenn wir festgestellt haben, dass es sichtbar ist und once auf true gesetzt ist.

Bilder laden

Lassen Sie uns unsere <IntersectionObserver>-Komponente verwenden, um Bilder bedingt zu laden, indem wir sie um eine <ImageLoader>-Komponente wickeln. Auch dies ist die Komponente, die eine Benachrichtigung von der <IntersectionOberserver> erhält, damit sie weiß, wann es Zeit ist, ein Bild zu laden.

Das bedeutet, wir benötigen eine neue Komponentendatei in components/Image. Nennen wir sie ImageLoader.svelte. Hier ist der Code, den wir darin haben wollen

<script>
  export let src
  export let alt


  import IntersectionObserver from './IntersectionObserver.svelte'
  import Image from './Image.svelte'
  
</script>


<IntersectionObserver once={true} let:intersecting={intersecting}>
  {#if intersecting}
    <Image {alt} {src} />
  {/if}
</IntersectionObserver>

Diese Komponente nimmt einige bildbezogene Props entgegen – src und alt –, die wir verwenden werden, um das tatsächliche Markup für ein Bild zu erstellen. Beachten Sie, dass wir im Skriptbereich zwei Komponenten importieren, einschließlich der <IntersectionObserver>, die wir gerade erstellt haben, und eine weitere namens <Image>, die wir aber noch erstellen werden.

Die <IntersectionObserver> wird eingesetzt, indem sie als Wrapper um die bald zu erstellende <Image>-Komponente dient. Schauen Sie sich die Eigenschaften an. Wir setzen once auf true, damit das Bild nur beim ersten Erscheinen geladen wird.

Dann nutzen wir Sveltes Slot-Props. Was sind das? Das behandeln wir als nächstes.

Weitergabe von Eigenschaftswerten über Slots

Umschließende Komponenten wie unsere <IntersectionObserver> sind nützlich, um Props an die von ihnen enthaltenen Kinder weiterzugeben. Svelte bietet uns dafür sogenannte **Slot-Props**.

In unserer <IntersectionObserver>-Komponente haben Sie vielleicht diese Zeile bemerkt

<slot {intersecting}></slot>

Dies gibt die intersecting-Prop an jede Komponente weiter, die wir ihr geben. In diesem Fall empfängt unsere <ImageLoader>-Komponente die Prop, wenn sie den Wrapper verwendet. Wir greifen über let:intersecting={intersecting} darauf zu, so:

<IntersectionObserver once={true} let:intersecting={intersecting}>

Wir können dann den intersecting-Wert verwenden, um zu bestimmen, wann es Zeit ist, eine <Image>-Komponente zu laden. In diesem Fall verwenden wir eine if-Bedingung, um zu prüfen, ob es soweit ist:

<IntersectionObserver once={true} let:intersecting={intersecting}>
  {#if intersecting}
    <Image {alt} {src} />
  {/if}
</IntersectionObserver> 

Wenn die Schnittmenge stattfindet, wird die <Image> geladen und erhält die alt- und src-Props. Sie können mehr über Slot-Props in diesem Svelte-Tutorial erfahren.

Wir haben nun den Code, um eine <Image>-Komponente anzuzeigen, wenn sie auf den Bildschirm gescrollt wird. Kommen wir nun endlich zum Erstellen der Komponente.

Bilder beim Laden anzeigen

Ja, Sie haben es erraten: Fügen wir eine Image.svelte-Datei zum Ordner components/Image für unsere <Image>-Komponente hinzu. Dies ist die Komponente, die unsere alt- und src-Props erhält und sie auf einem <img>-Element setzt.

Hier ist der Code der Komponente

<script>
  export let src
  export let alt


  import { onMount } from 'svelte'


  let loaded = false
  let thisImage


  onMount(() => {
    thisImage.onload = () => {
      loaded = true
    }
  }) 


</script>


<style>
  img {
    height: 200px;
    opacity: 0;
    transition: opacity 1200ms ease-out;
  }
  img.loaded {
    opacity: 1;
  }
</style>


<img {src} {alt} class:loaded bind:this={thisImage} />

Gleich zu Beginn erhalten wir die alt- und src-Props, bevor wir zwei neue Variablen definieren: loaded, um zu speichern, ob das Bild geladen wurde oder nicht, und thisImage, um eine Referenz auf das img-DOM-Element selbst zu speichern.

Wir verwenden auch eine hilfreiche Svelte-Methode namens onMount. Diese gibt uns eine Möglichkeit, Funktionen aufzurufen, nachdem eine Komponente im DOM gerendert wurde. In diesem Fall setzen wir einen Callback für thisImage.onload. In einfachen Worten bedeutet dies, dass er ausgeführt wird, wenn das Bild fertig geladen ist, und die Variable loaded auf den Wert true setzt.

Wir werden CSS verwenden, um das Bild anzuzeigen und es einzublenden. Lassen Sie uns bei Bildern eine opacity: 0 setzen, damit sie anfangs unsichtbar, aber technisch auf der Seite sind. Dann, wenn sie den Viewport schneiden und die <ImageLoader> die Erlaubnis zum Laden des Bildes erteilt, setzen wir die Deckkraft des Bildes auf voll. Wir können dies durch Setzen der transition-Eigenschaft auf dem Bild zu einem sanften Übergang machen. Die Demo setzt die Übergangszeit auf 1200 ms, aber Sie können sie nach Bedarf schneller oder langsamer einstellen.

Das führt uns zur allerletzten Zeile der Datei, dem Markup für ein <img>-Element.

<img {src} {alt} class:loaded bind:this={thisImage} />

Dies verwendet class:loaded, um bedingt eine .loaded-Klasse anzuwenden, wenn die Variable loaded auf true steht. Es verwendet auch die bind:this-Methode, um dieses DOM-Element mit der Variable thisImage zu verknüpfen.

Natives Lazy Loading

Obwohl die Unterstützung für natives Lazy Loading in Browsern kurz bevorsteht, wird es noch nicht von allen aktuellen stabilen Versionen unterstützt. Wir können die Unterstützung mit einem einfachen Fähigkeits-Check immer noch hinzufügen.

In unserer ImageLoader.svelte-Datei können wir die onMount-Funktion importieren und darin prüfen, ob unser Browser Lazy Loading unterstützt.

import { onMount } from 'svelte'

let nativeLoading = false
// Determine whether to bypass our intersecting check
onMount(() => {
  if ('loading' in HTMLImageElement.prototype) {
    nativeLoading = true
  }
})

Dann passen wir unsere if-Bedingung an, um diese nativeLoading-Boolean einzuschließen.

{#if intersecting || nativeLoading}
  <Image {alt} {src} />
{/if}

Zuletzt teilen wir in Image.svelte unserem Browser mit, dass er Lazy Loading verwenden soll, indem wir loading="lazy" zum <img>-Element hinzufügen.

<img {src} {alt} class:loaded bind:this={thisImage} loading="lazy" />

Dies ermöglicht modernen und zukünftigen Browsern, unseren Code zu umgehen und das Lazy Loading nativ zu übernehmen.

Lassen Sie uns alles zusammenfügen!

Okay, es ist Zeit, unsere Komponente tatsächlich zu verwenden. Öffnen Sie die App.svelte-Datei und fügen Sie den folgenden Code ein, um unsere Komponente zu importieren und zu verwenden

<script>
  import ImageLoader from './components/Image/ImageLoader.svelte';
</script>


<ImageLoader src="OUR_IMAGE_URL" alt="Our image"></ImageLoader>

Hier ist die Demo noch einmal

Und denken Sie daran, dass Sie den vollständigen Code für diese Demo auf GitHub herunterladen können. Wenn Sie dies auf einer Produktionswebsite sehen möchten, schauen Sie sich mein Projekt Shop Ireland an. Lazy Loading wird auf der Homepage, den Kategorieseiten und den Suchseiten verwendet, um die Geschwindigkeit zu verbessern. Ich hoffe, Sie finden es nützlich für Ihre eigenen Svelte-Projekte!