Verwandte Produkte und Cross-Sell-Carousels im Hyvä-Theme bauen
AI generated
Hyvä
phtml
Hyvä Theme · Produktempfehlungen
Verwandte Produkte und Cross-Sell-Carousels in Hyvä
Alpine-basierte Carousels ohne schwere JS-Bibliotheken, mit Lazy-Loading und GraphQL

Ein Carousel für verwandte Produkte ist schnell mit einer schweren JS-Bibliothek eingebunden und genauso schnell zum Performance-Problem geworden. Wer Hyvä-typisch bleibt, baut das Carousel mit Alpine und einer minimalen Swiper-Integration, lädt Bilder konsequent lazy und stellt sich ehrlich die Frage, ob ein statisches Grid nicht sogar die bessere Wahl wäre.

14 Min. Lesezeit Swiper Alpine Carousel Lazy-Loading GraphQL Empfehlungen Core Web Vitals

1. Warum Carousels im Hyvä-Kontext mit Vorsicht zu genießen sind

Hyvä steht für ein bewusst schlankes Frontend ohne unnötiges JavaScript-Gewicht, ein klassisches Carousel mit Autoplay, Pfeilen, Punkten und Touch-Gesten steht dazu erst einmal in einem gewissen Spannungsverhältnis. Viele Carousel-Bibliotheken bringen mehrere Hundert Kilobyte an JavaScript mit, oft inklusive eigener CSS-Engine, was dem Grundgedanken eines performanten Themes direkt entgegenläuft.

Trotzdem gibt es legitime Anwendungsfälle: Bei vielen verwandten Produkten oder Cross-Sell-Vorschlägen auf schmalen Viewports ist ein horizontales Scroll-Carousel oft die platzsparendste Darstellung, deutlich besser als ein zusammengequetschtes Grid mit winzigen Produktkarten. Der Schlüssel liegt darin, ein Carousel so schlank wie möglich zu implementieren, statt eine vollwertige Bibliothek mit Funktionen einzubinden, die im Produktkontext ohnehin nie genutzt werden.

2. Swiper minimal statt vollständig einbinden

Swiper ist unter den Carousel-Bibliotheken vergleichsweise schlank und modular aufgebaut, wichtig ist aber, nur die tatsächlich benötigten Module zu importieren, statt das komplette Bundle mit Effekten, Zoom und Video-Unterstützung zu laden. Für ein reines Produkt-Carousel reichen üblicherweise die Basis-Navigation und optional Pagination, alles andere bläht das JS-Bundle unnötig auf.

Die Einbindung erfolgt am saubersten als eigenständiges Modul über den bestehenden Hyvä-Webpack- beziehungsweise Vite-Build-Prozess, nicht als globales Skript über einen externen CDN-Link, da Letzteres die Content Security Policy verkompliziert und die Kontrolle über die ausgelieferte Version verliert. Alpine übernimmt dabei ausschließlich das Zeigen und Verstecken der Navigation sowie den aktuellen Zustand, während Swiper selbst die eigentliche Scroll- und Touch-Logik regelt.


// web/js/product-carousel.js
import Swiper from 'swiper';
import { Navigation } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/navigation';

export default function productCarousel(config) {
  return {
    swiper: null,

    init() {
      this.swiper = new Swiper(this.$refs.carousel, {
        modules: [Navigation],
        slidesPerView: 1.3,
        spaceBetween: 16,
        navigation: {
          nextEl: this.$refs.next,
          prevEl: this.$refs.prev,
        },
        breakpoints: {
          640: { slidesPerView: 2.3 },
          1024: { slidesPerView: 4 },
        },
      });
    },
  };
}

3. Das Alpine-Markup für ein Produkt-Carousel

Das phtml-Template selbst bleibt bewusst nah an Swipers erwarteter DOM-Struktur, damit keine zusätzliche Übersetzungsschicht zwischen Magento-Produktdaten und der Bibliothek nötig ist. Die Produktkarten innerhalb der Slides sind identisch zu denen im normalen Produktgrid, im Idealfall über dasselbe partial eingebunden, damit Compare-Button, Wishlist-Button und Preisdarstellung nicht doppelt gepflegt werden müssen.

Die Navigation-Pfeile werden nur angezeigt, wenn tatsächlich mehr Produkte vorhanden sind, als in den sichtbaren Bereich passen, sonst wirken sie wie kaputte UI-Elemente ohne Funktion. Diese Prüfung lässt sich einfach über die Anzahl der geladenen Produkte im Vergleich zur konfigurierten slidesPerView-Einstellung im Alpine-Zustand abbilden.


<div
  x-data="productCarousel()"
  x-init="init()"
  class="relative"
>
  <div class="swiper" x-ref="carousel">
    <div class="swiper-wrapper">
      <template x-for="product in products" :key="product.uid">
        <div class="swiper-slide">
          <!-- Wiederverwendete Produktkarte -->
          <div class="product-item">
            <img
              :src="product.thumbnail"
              :alt="product.name"
              loading="lazy"
              width="240"
              height="240"
              class="w-full h-auto"
            >
            <span x-text="product.name"></span>
          </div>
        </div>
      </template>
    </div>
  </div>
  <button x-ref="prev" x-show="products.length > 4" aria-label="Vorherige Produkte">‹</button>
  <button x-ref="next" x-show="products.length > 4" aria-label="Weitere Produkte">›</button>
</div>

4. Lazy-Loading der Carousel-Bilder korrekt umsetzen

Ein häufiger Fehler bei Carousel-Bildern ist, das native loading lazy Attribut unreflektiert auf alle Slides zu setzen, auch auf die ersten ein bis zwei sichtbaren Bilder. Der Browser lädt zwar auch bei loading lazy Bilder im initialen Viewport recht schnell nach, verzögert das Rendering aber messbar gegenüber einem eager geladenen Bild, was gerade bei above-the-fold-Carousels den Largest Contentful Paint verschlechtern kann.

Die robuste Lösung ist, die ersten ein bis zwei Slides eager oder ganz ohne loading-Attribut zu laden und erst ab dem dritten Slide auf lazy umzustellen, kombiniert mit expliziten width- und height-Attributen, um Layout-Shifts beim Nachladen zu vermeiden. Swiper selbst bringt zusätzlich eigene Lazy-Loading-Mechanismen mit, die aber mit dem nativen Browser-Lazy-Loading kollidieren können, weshalb sich in der Praxis die native Lösung ohne zusätzliches Swiper-Modul meist einfacher pflegen lässt.

5. GraphQL-Query für Produktempfehlungen

Für verwandte Produkte und Cross-Sell-Vorschläge liefert die products-Query mit den verschachtelten Feldern related_products beziehungsweise crosssell_products bereits alles Nötige, ohne einen zusätzlichen REST-Call zu benötigen. Wichtig ist, in der Query nur die tatsächlich im Carousel dargestellten Felder abzufragen, also Name, Bild, Preis und URL, statt versehentlich die vollständige Produktbeschreibung oder alle Attribute mitzuladen.

Bei sehr großen Kategorien mit dynamisch berechneten Produktempfehlungen, etwa über ein Recommendation-Modul eines Drittanbieters, lohnt sich zusätzlich ein serverseitiger Cache der Empfehlungsliste pro Produkt, da die Berechnung selbst rechenintensiver sein kann als eine einfache Attributzuordnung. Ein zu kurz gecachtes Ergebnis führt sonst dazu, dass jeder Produktseitenaufruf die komplette Empfehlungslogik erneut durchläuft.


query CrossSellProducts($sku: String!) {
  products(filter: { sku: { eq: $sku } }) {
    items {
      crosssell_products {
        uid
        name
        url_key
        small_image {
          url
        }
        price_range {
          minimum_price {
            final_price {
              value
              currency
            }
          }
        }
      }
    }
  }
}

Ein statisches Grid ohne JavaScript-Abhängigkeit ist in praktisch jeder Metrik günstiger als ein Carousel: keine zusätzliche JS-Bibliothek, keine Layout-Berechnung durch die Carousel-Engine, keine Touch-Event-Listener, die im Hintergrund aktiv bleiben. Für bis zu vier bis sechs Produktempfehlungen ist ein responsives CSS-Grid mit Umbruch auf kleineren Viewports deshalb häufig die bessere technische Entscheidung, auch wenn ein Carousel optisch moderner wirkt.

Ab etwa acht bis zehn Empfehlungen kippt die Abwägung tendenziell zugunsten eines Carousels, weil ein Grid mit derart vielen Produkten entweder sehr hoch wird oder auf Mobilgeräten unangenehm klein gerendert werden muss. Die pragmatische Faustregel lautet: Erst prüfen, ob ein Grid mit sinnvoller Spaltenanzahl auf allen Breakpoints noch funktioniert, und nur dann zum Carousel greifen, wenn diese Prüfung tatsächlich scheitert.

7. Mehrere Carousels auf einer Seite: Performance-Budget im Blick behalten

Auf einer einzelnen Produktseite tauchen schnell zwei oder drei Carousels gleichzeitig auf: verwandte Produkte, Cross-Sell im unteren Bereich und zusätzlich zuletzt angesehene Produkte. Jede dieser Instanzen initialisiert eine eigene Swiper-Instanz mit eigenen Event-Listenern, was sich bei unbedachter Umsetzung schnell summiert und die Interaktionsfähigkeit der Seite kurz nach dem Laden spürbar verzögert.

Der pragmatische Ansatz ist, Carousels erst dann zu initialisieren, wenn sie tatsächlich in den sichtbaren Bereich scrollen, statt alle sofort beim Seitenaufbau zu starten. Ein IntersectionObserver, in Alpine über x-intersect oder eine schlanke eigene Direktive eingebunden, verzögert die Swiper-Instanziierung bis zum tatsächlichen Bedarf und hält damit die initiale JavaScript-Ausführungszeit der Seite niedrig, selbst wenn mehrere Carousels auf derselben Seite existieren.


// Carousel erst initialisieren, wenn es in den Viewport scrollt
function lazyCarousel() {
  return {
    initialized: false,

    observeAndInit() {
      const observer = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && !this.initialized) {
          this.initialized = true;
          this.initSwiper();
          observer.disconnect();
        }
      }, { rootMargin: '200px' });

      observer.observe(this.$el);
    },
  };
}

8. Barrierefreiheit und Tastaturbedienung im Carousel

Ein Carousel, das nur per Maus oder Touch bedienbar ist, schließt Tastaturnutzer faktisch aus. Swiper unterstützt Tastaturnavigation über ein eigenes Keyboard-Modul, das aktiviert werden muss, zusätzlich sollten die Navigation-Buttons echte button-Elemente mit aussagekräftigem aria-label sein, statt reiner div-Klicks ohne semantische Bedeutung.

Für Screenreader-Nutzer empfiehlt sich zudem ein aria-live-Bereich, der bei Slide-Wechsel die aktuelle Position ansagt, etwa Produkt drei von acht, sowie ein role='region' mit aria-label auf dem Carousel-Container selbst, damit die Struktur für unterstützende Technologien klar erkennbar ist und nicht als beliebige Liste ohne Kontext interpretiert wird.

9. Typische Fehler bei Cross-Sell-Carousels

Der häufigste Fehler ist das ungeprüfte Einbinden einer vollständigen Carousel-Bibliothek mit allen Modulen, obwohl im Produktkontext nur Basis-Navigation gebraucht wird. Das führt zu einem unnötig aufgeblähten JS-Bundle, das direkt auf die Core-Web-Vitals-Metriken einzahlt, insbesondere auf Total Blocking Time bei langsameren Mobilgeräten.

Ein zweiter, oft übersehener Punkt ist das Fehlen jeglicher Fallback-Darstellung, wenn JavaScript aus irgendeinem Grund nicht lädt, etwa durch einen CSP-Fehler oder ein blockiertes Skript. Ohne Fallback sehen Kunden dann eine unstyled, horizontal aneinandergereihte Liste ohne jede Struktur, statt zumindest ein einfaches, funktionierendes Grid als Basis-Erlebnis zu erhalten.

Kriterium Statisches Grid Alpine + Swiper Carousel Empfehlung
JS-Bundle-Größe Keine zusätzliche Last Zusätzliches, aber schlankes Modul Nur bei echtem Platzbedarf
Anzahl Empfehlungen Gut bis 4-6 Produkte Sinnvoll ab 8-10 Produkten Faustregel je nach Produktanzahl
Tastaturbedienung Nativ durch Browser Erfordert Keyboard-Modul Modul aktivieren
Layout-Stabilität Kein CLS-Risiko Risiko bei fehlendem Width/Height Immer explizite Bildmaße setzen
Fallback ohne JS Funktioniert immer Erfordert bewusste Fallback-Strategie Grid als Basis-Markup verwenden

Mironsoft

Hyvä-Theme-Entwicklung und Luma-Migration

Noch auf Luma unterwegs oder ein Hyvä-Theme, das nicht rund läuft?

Wir entwickeln Hyvä-Themes für Magento von Grund auf oder migrieren bestehende Luma-Shops sauber, mit Tailwind CSS, Alpine.js und ohne unnötiges JavaScript-Gepäck.

Luma-zu-Hyvä-Migration

Bestehenden Shop strukturiert und ohne Funktionsverlust auf Hyvä umstellen.

Custom-Theme-Entwicklung

Individuelles Hyvä-Theme nach Design-Vorgaben von Grund auf umsetzen.

Performance-Optimierung

Core Web Vitals und Ladezeiten im Hyvä-Frontend gezielt verbessern.

10. Zusammenfassung

Cross-Sell-Carousels in Hyvä: Das Wichtigste auf einen Blick

Schlank bleiben

Nur benötigte Swiper-Module importieren statt der vollständigen Bibliothek mit allen Effekten.

Erst Grid, dann Carousel

Bis vier bis sechs Empfehlungen ist ein statisches Grid meist die technisch bessere Wahl.

Lazy-Loading gezielt einsetzen

Die ersten ein bis zwei Slides eager laden, erst danach lazy, um LCP nicht zu verschlechtern.

Barrierefreiheit nicht vergessen

Keyboard-Modul, aria-label und aria-live-Ansagen gehören zu jedem produktiven Carousel.

11. FAQ: Cross-Sell-Carousels in Hyvä: Das Wichtigste auf einen Blick

1Passt ein klassisches JS-Carousel überhaupt zur Hyvä-Philosophie?
Nur mit Einschränkungen: Hyvä steht für minimales JavaScript, deshalb sollte ein Carousel so schlank wie möglich implementiert werden, mit nur den tatsächlich benötigten Modulen. Für wenige Produkte ist ein statisches Grid ohnehin oft die bessere Alternative.
2Warum sollte ich nicht die vollständige Swiper-Bibliothek einbinden?
Weil das vollständige Bundle Module für Effekte, Zoom und Video-Unterstützung enthält, die im Produktkontext praktisch nie gebraucht werden. Ein selektiver Import nur der Navigation- und gegebenenfalls Pagination-Module hält das JS-Bundle deutlich schlanker.
3Ab wie vielen Produktempfehlungen lohnt sich ein Carousel gegenüber einem Grid?
Als Faustregel ab etwa acht bis zehn Empfehlungen, da ein Grid mit so vielen Produkten entweder sehr hoch wird oder auf Mobilgeräten zu klein gerendert werden muss. Darunter ist ein statisches, responsives Grid meist die technisch günstigere Lösung.
4Sollten alle Carousel-Bilder mit loading lazy versehen werden?
Nein, die ersten ein bis zwei sichtbaren Slides sollten eager oder ganz ohne loading-Attribut geladen werden. Erst ab dem dritten Slide macht lazy Sinn, sonst verzögert sich das Rendering der initial sichtbaren Bilder unnötig.
5Woher kommen die Daten für verwandte Produkte und Cross-Sell im Carousel?
Über die products-GraphQL-Query mit den verschachtelten Feldern related_products beziehungsweise crosssell_products. Wichtig ist, nur die für das Carousel tatsächlich benötigten Felder abzufragen, um die Query schlank zu halten.
6Wie stelle ich Tastaturbedienbarkeit im Carousel sicher?
Über Swipers eigenes Keyboard-Modul, das explizit aktiviert werden muss, plus echte button-Elemente mit aria-label für die Navigation statt reiner div-Klicks. Ohne diese Maßnahmen bleibt das Carousel für Tastaturnutzer faktisch unbedienbar.
7Was passiert mit dem Carousel, wenn JavaScript nicht lädt?
Ohne bewusste Fallback-Strategie sehen Kunden eine unstyled, horizontal aneinandergereihte Liste ohne Struktur. Ein Grid als Basis-Markup, das erst durch Alpine und Swiper in ein Carousel verwandelt wird, verhindert dieses Problem zuverlässig.
8Kollidiert Swipers eigenes Lazy-Loading mit dem nativen Browser-Lazy-Loading?
Es kann, weshalb sich in der Praxis meist die native loading lazy Lösung ohne zusätzliches Swiper-Lazy-Modul einfacher pflegen lässt. Beide Mechanismen gleichzeitig zu aktivieren führt zu unnötiger Komplexität ohne echten Zusatznutzen.
9Warum sollte ich die Navigation-Pfeile nicht immer anzeigen?
Weil sie bei weniger Produkten, als in den sichtbaren Bereich passen, wie kaputte UI-Elemente ohne Funktion wirken. Eine Prüfung der Produktanzahl gegen die konfigurierte slidesPerView-Einstellung blendet die Pfeile in diesem Fall automatisch aus.
10Wie vermeide ich Layout-Shifts beim Nachladen der Carousel-Bilder?
Durch explizite width- und height-Attribute auf jedem Bild, damit der Browser den benötigten Platz bereits vor dem eigentlichen Laden reserviert. Ohne diese Angaben verschiebt sich der restliche Seiteninhalt sichtbar, sobald die Bilder nachgeladen werden.