Produkte nachladen ohne externe Bibliothek
Der Intersection Observer ist eine native Browser-API, die erkennt, wenn ein Element in den sichtbaren Bereich scrollt – ohne Scroll-Event-Listener, ohne Performance-Einbußen. In Kombination mit Alpine.js und der Magento-REST-API entsteht ein vollständiges Infinite-Scroll-System in weniger als 60 Zeilen.
Inhaltsverzeichnis
- 1. Infinite Scroll: Konzept und Browser-Support
- 2. Intersection Observer API erklärt
- 3. Alpine.js State-Design für Infinite Scroll
- 4. Observer in x-init registrieren und aufräumen
- 5. Produkte von Magento REST nachladen
- 6. Ladezustand, Fehler und Listenende anzeigen
- 7. URL-Seitenzahl reaktiv aktualisieren
- 8. SEO-Überlegungen bei Infinite Scroll
- 9. Infinite Scroll vs. Load-More-Button im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Infinite Scroll: Konzept und Browser-Support
Infinite Scroll ist das Muster, bei dem neue Inhalte automatisch nachgeladen werden, sobald der Nutzer das Ende der aktuell angezeigten Liste erreicht. Es ersetzt die klassische Paginierung durch ein kontinuierliches Scrollen – bekannt aus Social-Media-Feeds, aber auch in E-Commerce-Produktlisten weit verbreitet. Die Herausforderung liegt darin, das Nachladen genau dann auszulösen, wenn der Nutzer kurz vor dem Listenende ist – nicht zu früh (unnötige Requests), nicht zu spät (sichtbare Leerstelle).
Der Intersection Observer ist seit 2018 in allen modernen Browsern verfügbar und seit 2020 in Safari unterstützt. Er ist damit de facto der Standard-Mechanismus für Scroll-basiertes Verhalten. Das alte Muster – window.addEventListener('scroll', handler) mit getBoundingClientRect() – ist langsam, läuft auf dem Main Thread und blockiert bei intensiver Nutzung das Rendering. Der Intersection Observer dagegen läuft asynchron und ist vollständig nicht-blockierend.
Für Hyvä Themes ist Infinite Scroll ein häufig gefragtes Feature in Kategorielisten und Suchergebnisseiten. Hyvä bietet kein eingebautes Infinite-Scroll-System. Es muss entweder über ein Dritt-Modul oder – was in diesem Artikel gezeigt wird – als eigene Alpine.js-Komponente implementiert werden. Die Lösung integriert sich sauber mit dem bestehenden Hyvä-Produktraster und der Magento-REST-API.
2. Intersection Observer API erklärt
Ein IntersectionObserver beobachtet, ob ein Zielelement die Grenze eines anderen Elements (oder des Viewports) überschreitet. Der Konstruktor nimmt einen Callback und ein Options-Objekt. Der Callback wird aufgerufen, wenn sich der Intersection-Status des beobachteten Elements ändert. entry.isIntersecting ist true, wenn das Element sichtbar ist, false wenn es außerhalb des Viewports ist.
Die rootMargin-Option ist entscheidend für Infinite Scroll: Mit rootMargin: '200px' wird der Callback bereits ausgelöst, wenn das Element 200 Pixel vor dem unteren Viewport-Rand erscheint. So beginnt das Nachladen, bevor der Nutzer das tatsächliche Ende sieht – der Übergang wirkt nahtlos. Die threshold-Option steuert, wie viel Prozent des Elements sichtbar sein müssen, bevor der Callback feuert. Für Infinite Scroll reicht threshold: 0 – also sobald irgendein Pixel sichtbar wird.
// Alpine.js Infinite Scroll — core state and observer setup
document.addEventListener('alpine:init', () => {
Alpine.data('infiniteProductList', (config = {}) => ({
products: config.initial ?? [],
currentPage: config.startPage ?? 1,
totalPages: config.totalPages ?? 1,
loading: false,
error: null,
categoryId: config.categoryId,
pageSize: config.pageSize ?? 12,
// IntersectionObserver instance — stored for cleanup
_observer: null,
get hasMore() {
return this.currentPage < this.totalPages;
},
init() {
// Only set up observer if there are more pages to load
if (!this.hasMore) return;
this._observer = new IntersectionObserver(
(entries) => {
const sentinel = entries[0];
if (sentinel.isIntersecting && this.hasMore && !this.loading) {
this.loadNextPage();
}
},
{
rootMargin: '200px', // trigger 200px before visible
threshold: 0,
}
);
// Observe the sentinel element — a div at the bottom of the list
const sentinel = this.$el.querySelector('[data-sentinel]');
if (sentinel) this._observer.observe(sentinel);
},
destroy() {
// Clean up observer when component is removed from DOM
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
},
}));
});
3. Alpine.js State-Design für Infinite Scroll
Der State des Infinite-Scroll-Systems besteht aus wenigen, klar definierten Werten: products als Array aller bisher geladenen Produkte, currentPage als aktuelle Seite, totalPages als Gesamtseitenzahl (aus dem ersten Response bekannt), loading als Flag für den Ladezustand und error für Fehlermeldungen. Das Server-Rendering liefert die erste Seite der Produkte als PHP-generiertes JSON-Array, das als config.initial-Parameter übergeben wird.
Der berechnete Getter hasMore kapselt die Logik, ob weitere Seiten verfügbar sind. Alle Template-Teile (Ladeindikator, Listenende-Meldung, weiterer-laden-Hint) beziehen sich auf diesen einzelnen Getter. Wenn sich currentPage oder totalPages ändert, aktualisiert sich hasMore automatisch und alle davon abhängigen Template-Teile folgen. Das ist reaktive Programmierung in ihrer reinsten Form.
4. Observer in x-init registrieren und aufräumen
Der Intersection Observer wird in der init()-Methode registriert, die Alpine.js beim Mounten der Komponente automatisch aufruft. Das zu beobachtende Element ist ein Sentinel-Element – ein leeres <div data-sentinel></div> am Ende der Produktliste. Wenn dieses Element in den Viewport kommt, feuert der Observer-Callback und löst das Nachladen aus. Das Sentinel-Element wird aus dem DOM durch this.$el.querySelector('[data-sentinel]') selektiert.
Das Aufräumen des Observers ist ebenso wichtig wie das Erstellen. Alpine.js ruft destroy() auf, wenn die Komponente aus dem DOM entfernt wird. Dort wird this._observer.disconnect() aufgerufen. Ohne dieses Cleanup beobachtet der Observer weiterhin das Sentinel-Element, auch wenn die Komponente nicht mehr existiert – ein klassischer Memory Leak. Alpine's Lifecycle-Methode destroy() macht das Cleanup zu einem natürlichen Teil des Komponenten-Lebenszyklus.
// Load next page from Magento REST API
async loadNextPage() {
if (this.loading || !this.hasMore) return;
this.loading = true;
this.error = null;
try {
const nextPage = this.currentPage + 1;
const url = new URL('/rest/V1/products', window.location.origin);
url.searchParams.set('searchCriteria[filterGroups][0][filters][0][field]', 'category_id');
url.searchParams.set('searchCriteria[filterGroups][0][filters][0][value]', this.categoryId);
url.searchParams.set('searchCriteria[filterGroups][0][filters][0][conditionType]', 'eq');
url.searchParams.set('searchCriteria[pageSize]', this.pageSize);
url.searchParams.set('searchCriteria[currentPage]', nextPage);
url.searchParams.set('searchCriteria[sortOrders][0][field]', 'position');
url.searchParams.set('searchCriteria[sortOrders][0][direction]', 'ASC');
url.searchParams.set('fields', 'items[id,sku,name,price,custom_attributes],total_count');
const response = await fetch(url.toString(), {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
// Append new products to existing list
this.products.push(...data.items);
this.currentPage = nextPage;
this.totalPages = Math.ceil(data.total_count / this.pageSize);
// Update browser URL to reflect current page
this.updateUrl(nextPage);
} catch (err) {
this.error = 'Produkte konnten nicht geladen werden. Bitte versuche es erneut.';
console.error('[infiniteScroll] loadNextPage error:', err);
} finally {
this.loading = false;
}
},
5. Produkte von Magento REST nachladen
Die Magento REST API liefert Produktlisten über den Endpunkt /rest/V1/products mit SearchCriteria-Parametern. Die wichtigsten Parameter für eine Kategorieliste: filterGroups zum Filtern auf eine Kategorie-ID, pageSize für die Anzahl der Produkte pro Seite und currentPage für die gewünschte Seite. Der fields-Parameter begrenzt die Antwortgröße auf benötigte Felder – ohne ihn liefert Magento alle Produktattribute zurück, was die Antwort um ein Vielfaches aufbläht.
Die erste Seite kommt vom Server-Rendering – PHP lädt die ersten 12 Produkte und serialisiert sie als JSON ins Template. Alpine.js nimmt dieses Array als config.initial-Parameter entgegen und stellt es als initiales products-Array bereit. Jede weitere Seite wird über die REST-API nachgeladen und per this.products.push(...data.items) an das bestehende Array angehängt. Alpine.js propagiert die Änderung an alle x-for-Iterationen im Template, die neuen Produkt-Karten erscheinen sofort.
6. Ladezustand, Fehler und Listenende anzeigen
Der Ladezustand wird durch das loading-Flag gesteuert. Im Template erscheint ein Spinner oder ein Skeleton-Loader mit x-show="loading". Das Listenende wird durch x-show="!hasMore && !loading" signalisiert – eine Meldung wie "Alle 48 Produkte geladen" gibt dem Nutzer klares Feedback. Fehler werden mit x-show="error" und x-text="error" angezeigt, zusammen mit einem Retry-Button, der loadNextPage() erneut aufruft.
Der Skeleton-Loader ist im Hyvä-Kontext eine leere Produktkarten-Struktur mit Tailwind-Klassen wie animate-pulse bg-slate-200 rounded. Er gibt dem Nutzer visuelles Feedback, dass Inhalte geladen werden, ohne dass ein Spinner die Aufmerksamkeit bricht. Die Anzahl der Skeleton-Karten sollte mit pageSize übereinstimmen, damit der Übergang zwischen Skeleton und echten Karten nahtlos wirkt.
7. URL-Seitenzahl reaktiv aktualisieren
Wenn ein Nutzer auf Seite 3 einer Infinite-Scroll-Liste ist und die Seite neu lädt, sollte er wieder bei Seite 3 starten – nicht bei Seite 1. Die Lösung ist die History API: history.replaceState(null, '', `?p=${page}`) aktualisiert die URL ohne Seitenneuladen. So kann der Nutzer die aktuelle Position bookmarken oder teilen.
Beim initialen Laden liest Alpine.js den p-Parameter aus der URL und startet mit der entsprechenden Seite. Dazu prüft init() den URL-Parameter und setzt currentPage entsprechend. Alle Seiten von 1 bis zur aktuellen Seite müssen dann von der API nachgeladen werden – entweder sequenziell oder in einem einzigen Request mit erhöhtem pageSize-Wert (pageSize × p). Das ist die klassische Kompromiss-Frage: Genauigkeit vs. Request-Anzahl.
// URL management — keep browser URL in sync with current scroll position
updateUrl(page) {
if (!history.replaceState) return;
const url = new URL(window.location.href);
if (page > 1) {
url.searchParams.set('p', page);
} else {
url.searchParams.delete('p');
}
history.replaceState({ page }, '', url.toString());
},
// Read initial page from URL — call at start of init()
getInitialPage() {
const params = new URLSearchParams(window.location.search);
return parseInt(params.get('p') ?? '1', 10);
},
// Re-observe sentinel after DOM update (needed when sentinel was hidden)
async reObserveSentinel() {
await this.$nextTick();
if (this._observer && this.hasMore) {
const sentinel = this.$el.querySelector('[data-sentinel]');
if (sentinel) {
this._observer.unobserve(sentinel);
this._observer.observe(sentinel);
}
}
},
8. SEO-Überlegungen bei Infinite Scroll
Infinite Scroll hat aus SEO-Perspektive eine grundlegende Schwäche: Suchmaschinen-Crawler scrollen nicht. Sie sehen in der Regel nur die erste Seite. Das bedeutet: Produkte auf Seite 2 und folgende sind für Google nicht erreichbar, wenn sie ausschließlich über JavaScript nachgeladen werden. Google empfiehlt für Infinite-Scroll-Listen, <link rel="next">- und <link rel="prev">-Tags im <head> zu setzen und paginierte Varianten der Seiten zugänglich zu machen.
Die sauberste Lösung im Magento-Kontext ist eine hybride Strategie: Der Server rendert die erste Seite vollständig mit HTML, die Standard-Paginierungs-URLs (/category?p=2, /category?p=3) bleiben als eigenständige Seiten erreichbar und indexierbar. Alpine.js erweitert das Erlebnis für JavaScript-fähige Browser mit Infinite Scroll – aber als Progressive Enhancement, nicht als einziger Datenzugang. Diese Strategie kombiniert gute SEO mit exzellenter UX.
9. Infinite Scroll vs. Load-More-Button im Vergleich
Infinite Scroll und Load-More-Button lösen dasselbe technische Problem mit unterschiedlichen UX-Konsequenzen. Die Wahl hängt vom Kontext ab: Produktlisten, bei denen der Footer erreichbar sein muss, leiden unter Infinite Scroll. Entdeckungs-orientierte Listen profitieren davon.
| Kriterium | Infinite Scroll | Load-More-Button | Klassische Paginierung |
|---|---|---|---|
| Nutzerkontrolle | Gering – automatisch | Gut – explizite Aktion | Vollständig |
| SEO-Freundlichkeit | Schlecht ohne Fallback | Mittel | Sehr gut |
| Entdeckungs-UX | Sehr gut | Gut | Mittel |
| Footer-Erreichbarkeit | Problematisch | Kein Problem | Kein Problem |
| Alpine.js-Implementierung | Intersection Observer | @click + loadNextPage | Kein JS nötig |
In E-Commerce-Projekten hat sich ein Hybrid bewährt: Infinite Scroll für mobile Geräte (Touch-Scrolling ist natürlich), Load-More-Button für Desktop. Alpine.js ermöglicht das mit einem einfachen Conditional: Auf Mobile wird der Intersection Observer registriert, auf Desktop erscheint stattdessen ein Button, der loadNextPage() aufruft. Dieselbe Lade-Logik, unterschiedliche UX-Auslöser.
Mironsoft
Alpine.js, Hyvä Themes und Magento 2 Frontend-Entwicklung
Infinite Scroll für euren Magento-Hyvä-Shop?
Wir implementieren Infinite Scroll und Load-More-Systeme für Hyvä Themes – SEO-freundlich, performance-optimiert und vollständig in Alpine.js integriert.
Kategorielisten
Infinite Scroll mit Magento REST API, URL-State und SEO-Fallback
Suchergebnisse
Infinite Scroll für Elasticsearch-basierte Suche mit Filter-Integration
Mobile-First
Adaptive UX: Infinite Scroll auf Mobile, Load-More auf Desktop
10. Zusammenfassung
Infinite Scroll mit Alpine.js und dem Intersection Observer ist ein klar strukturierbares, performantes Feature, das ohne externe Bibliothek auskommt. Der Intersection Observer feuert den Nachladeaufruf asynchron und nicht-blockierend, sobald das Sentinel-Element in den Viewport kommt. Alpine.js hält den gesamten State – Produktliste, aktuelle Seite, Ladezustand, Fehler – reaktiv und propagiert Änderungen sofort ins Template. Die History API synchronisiert die Browser-URL mit der aktuellen Scrollposition.
SEO ist die wichtigste Einschränkung: Infinite Scroll muss als Progressive Enhancement über paginierte Fallback-URLs implementiert werden. Google-Crawler sehen nur die erste Seite. Die sauberste Lösung für Magento ist Server-Rendering der ersten Seite und REST-API-basiertes Nachladen für folgende Seiten, mit Standard-Paginierungs-URLs als indexierbare Alternativen.
Alpine.js Infinite Scroll — Das Wichtigste auf einen Blick
Intersection Observer
Asynchron, nicht-blockierend. rootMargin: '200px' für frühzeitiges Triggern. threshold: 0. Sentinel-Element am Listenende beobachten. disconnect() in destroy().
State-Design
products[], currentPage, totalPages, loading, error. hasMore-Getter als einzige Quelle für alle Template-Conditions. REST-Response per push() anhängen.
URL & SEO
history.replaceState für URL-Sync. Paginierungs-URLs (?p=2) als SEO-Fallback. Erste Seite server-gerendert. REST nur für Folgeseiten.
UX-Zustände
Skeleton-Loader (animate-pulse) statt Spinner. Listenende-Meldung wenn !hasMore. Retry-Button bei error. Adaptive UX: IO auf Mobile, Button auf Desktop.