Performant filtern mit Alpine.js, ganz ohne Bibliothek
Eine Live-Suche über tausende Einträge muss auf jeden Tastendruck reagieren, ohne die Seite auszubremsen. Mit x-model.debounce, einer effizienten Filterfunktion und Treffer-Highlighting entsteht mit Alpine.js eine schnelle, reaktive Suche, die auch bei sehr großen Listen flüssig bleibt.
Inhaltsverzeichnis
- 1. Warum Debounce bei einer Live-Suche unverzichtbar ist
- 2. Grundstruktur: Suchfeld und gefilterte Liste
- 3. x-model.debounce richtig konfigurieren
- 4. Filterlogik: computed Property statt Watcher Chaos
- 5. Fuzzy Matching: Tippfehler tolerieren
- 6. Treffer im Text hervorheben
- 7. Große Listen: virtuelles Scrollen kombinieren
- 8. Tastaturnavigation durch die Ergebnisliste
- 9. Debounce Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Debounce bei einer Live-Suche unverzichtbar ist
Eine Live-Suche soll dem Nutzer bei jedem Tastendruck sofort gefilterte Ergebnisse zeigen, doch genau darin liegt die technische Herausforderung. Ohne Drosselung löst jeder einzelne Tastendruck eine vollständige Neuberechnung der gefilterten Liste aus. Bei einer Liste mit wenigen Dutzend Einträgen fällt das nicht auf, bei mehreren tausend Einträgen führt es zu spürbaren Rucklern, gerade auf mobilen Geräten mit schwächerer CPU.
Debounce löst dieses Problem, indem die eigentliche Filterlogik erst ausgeführt wird, nachdem der Nutzer für eine kurze Zeitspanne aufgehört hat zu tippen. Alpine.js bringt diese Technik direkt als Modifikator mit: x-model.debounce.300ms verzögert die Aktualisierung der gebundenen Variable um 300 Millisekunden nach der letzten Eingabe. Für eine Live-Suche ist das der entscheidende Unterschied zwischen einer Oberfläche, die bei schnellem Tippen ruckelt, und einer, die butterweich bleibt.
Dieser Artikel baut eine vollständige Live-Suche mit Debounce auf: von der Grundstruktur über die richtige Debounce Konfiguration, effiziente Filterlogik, Fuzzy Matching für Tippfehlertoleranz, Treffer Highlighting bis zur Kombination mit virtuellem Scrollen für wirklich große Datenmengen.
2. Grundstruktur: Suchfeld und gefilterte Liste
Die Grundstruktur einer Live-Suche mit Alpine.js besteht aus einem Input Feld, das über x-model an eine query Variable gebunden ist, und einer x-for Schleife, die über eine gefilterte Version der Originalliste iteriert. Entscheidend ist, die vollständige, ungefilterte Datenliste getrennt von der aktuell angezeigten gefilterten Liste zu halten, damit bei einer Leerung des Suchfelds sofort wieder alle Einträge sichtbar sind, ohne die Originaldaten neu laden zu müssen.
Für eine Live-Suche über wirklich große Datenmengen sollten die zu durchsuchenden Daten idealerweise bereits beim Laden der Seite vollständig im Speicher vorliegen, nicht bei jedem Tastendruck per Ajax nachgeladen werden. Serverseitige Suche mit Ajax Anfragen pro Tastendruck erzeugt Netzwerk Overhead und Race Conditions, wenn Antworten in falscher Reihenfolge eintreffen. Clientseitiges Filtern mit Alpine.js ist bis zu einigen zehntausend Einträgen die deutlich robustere Lösung.
<div x-data="liveSearch()">
<input
type="text"
x-model.debounce.300ms="query"
placeholder="Suchen…"
class="w-full px-4 py-2 border border-slate-300 rounded-lg"
>
<ul class="mt-4 divide-y divide-slate-100">
<template x-for="item in filteredItems" :key="item.id">
<li class="py-2" x-text="item.name"></li>
</template>
<li x-show="filteredItems.length === 0" class="py-4 text-slate-500 text-sm">
Keine Treffer für "<span x-text="query"></span>"
</li>
</ul>
</div>
3. x-model.debounce richtig konfigurieren
Der debounce Modifikator von Alpine.js akzeptiert eine Zeitangabe direkt im Attribut, Standard ist 250 Millisekunden, wenn keine explizite Dauer angegeben wird. Für eine Live-Suche ist die richtige Wahl der Verzögerung eine Abwägung: zu kurz, und die Drosselung bringt kaum Vorteil bei schnellen Tippern. Zu lang, und die Suche fühlt sich träge an, weil Nutzer eine spürbare Verzögerung zwischen Tastendruck und Ergebnis wahrnehmen.
In der Praxis hat sich für eine Live-Suche ein Wert zwischen 200 und 350 Millisekunden bewährt, abhängig von der Größe der zu filternden Liste und der Komplexität der Filterlogik. Bei sehr großen Listen mit aufwendigem Fuzzy Matching lohnt sich ein höherer Wert um 400 Millisekunden, während eine einfache Substring Suche über wenige hundert Einträge auch mit 150 Millisekunden noch flüssig bleibt.
// x-model.debounce syntax variants for a live search input
// x-model.debounce="query" → default 250ms delay
// x-model.debounce.300ms="query" → explicit 300ms delay
// x-model.debounce.500ms="query" → longer delay for heavier filter logic
function liveSearch() {
return {
query: '',
allItems: [], // populated once on init, not re-fetched per keystroke
init() {
this.allItems = window.searchDataset || [];
},
get filteredItems() {
if (!this.query.trim()) return this.allItems;
const needle = this.query.toLowerCase();
return this.allItems.filter((item) => item.name.toLowerCase().includes(needle));
},
};
}
4. Filterlogik: computed Property statt Watcher Chaos
Ein häufiger Anfängerfehler bei einer Live-Suche mit Alpine.js ist, die gefilterte Liste über einen $watch auf die query Variable manuell zu befüllen und in einer separaten filteredItems Eigenschaft zu speichern. Das funktioniert, führt aber zu doppeltem Zustand und potenziellen Inkonsistenzen, etwa wenn allItems sich ändert, aber der Watcher nicht erneut feuert.
Die robustere Lösung nutzt einen JavaScript Getter, wie im vorherigen Codebeispiel gezeigt: get filteredItems() berechnet die gefilterte Liste bei jedem Zugriff frisch aus query und allItems, ganz ohne manuelle Synchronisierung. Alpine.js erkennt über sein reaktives Proxy System automatisch, wann sich eine der beiden Abhängigkeiten ändert, und aktualisiert die Ansicht entsprechend. Für eine Live-Suche ist dieser Ansatz nicht nur weniger fehleranfällig, sondern auch deutlich kürzer im Code.
function liveSearch() {
return {
query: '',
allItems: [],
selectedCategory: 'all',
init() {
this.allItems = window.searchDataset || [];
},
// Getter recomputes automatically whenever query, allItems, or
// selectedCategory change — no manual watcher synchronization needed
get filteredItems() {
let items = this.allItems;
if (this.selectedCategory !== 'all') {
items = items.filter((item) => item.category === this.selectedCategory);
}
const needle = this.query.trim().toLowerCase();
if (!needle) return items;
return items.filter((item) => item.name.toLowerCase().includes(needle));
},
};
}
5. Fuzzy Matching: Tippfehler tolerieren
Eine reine Substring Suche bei einer Live-Suche versagt, sobald Nutzer sich vertippen oder Wörter in anderer Reihenfolge eingeben. Fuzzy Matching toleriert kleine Abweichungen, indem es Zeichen in beliebiger Reihenfolge sucht oder eine Ähnlichkeitsdistanz zwischen Suchbegriff und Eintrag berechnet. Für die meisten Anwendungsfälle reicht ein einfacher Zeichen Sequenz Algorithmus: alle Zeichen der Suchanfrage müssen in derselben Reihenfolge im Zieltext vorkommen, aber nicht zwingend direkt aufeinanderfolgend.
Diese leichte Fuzzy Variante für eine Live-Suche lässt sich ohne externe Bibliothek in wenigen Zeilen JavaScript umsetzen und bringt bereits einen großen Komfortgewinn gegenüber exakter Substring Suche. Für komplexere Anforderungen mit Tippfehlertoleranz über die Levenshtein Distanz lohnt sich eine schlanke, fokussierte Bibliothek wie Fuse.js, die sich problemlos in eine Alpine.js Filterfunktion integrieren lässt, ohne die restliche Architektur zu verändern.
// Lightweight fuzzy matching: characters must appear in order, not necessarily adjacent
function fuzzyMatch(needle, haystack) {
needle = needle.toLowerCase();
haystack = haystack.toLowerCase();
let needleIndex = 0;
for (let i = 0; i < haystack.length && needleIndex < needle.length; i++) {
if (haystack[i] === needle[needleIndex]) {
needleIndex++;
}
}
return needleIndex === needle.length;
}
function liveSearch() {
return {
query: '',
allItems: [],
get filteredItems() {
const needle = this.query.trim();
if (!needle) return this.allItems;
return this.allItems.filter((item) => fuzzyMatch(needle, item.name));
},
};
}
6. Treffer im Text hervorheben
Nutzer einer Live-Suche erwarten, direkt zu sehen, welcher Teil eines Ergebnisses tatsächlich mit ihrer Eingabe übereinstimmt. Dieses Highlighting umschließt den passenden Textabschnitt mit einem <mark> Element, das über CSS eine Hervorhebungsfarbe erhält. Da Alpine.js über x-html rohes HTML rendern kann, lässt sich diese Hervorhebung direkt im Template umsetzen, solange die Eingabedaten vertrauenswürdig sind oder vor der Verwendung sauber escaped werden.
Für eine Live-Suche mit potenziell nutzergenerierten Daten ist es wichtig, vor dem Einfügen des <mark> Tags den Rest des Textes korrekt zu escapen, um Cross Site Scripting Angriffe zu verhindern. Eine einfache Hilfsfunktion übernimmt das Escaping der HTML Sonderzeichen, bevor die Markierung eingefügt wird, und macht das Highlighting damit auch bei Daten aus externen Quellen sicher.
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function highlightMatch(text, needle) {
if (!needle) return escapeHtml(text);
const escapedText = escapeHtml(text);
const escapedNeedle = escapeHtml(needle);
const regex = new RegExp(`(${escapedNeedle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return escapedText.replace(regex, '<mark class="bg-teal-200 text-teal-900 rounded px-0.5">$1</mark>');
}
<template x-for="item in filteredItems" :key="item.id">
<li class="py-2" x-html="highlightMatch(item.name, query)"></li>
</template>
7. Große Listen: virtuelles Scrollen kombinieren
Selbst eine effiziente Filterfunktion stößt an Grenzen, wenn die Live-Suche bei jedem Tastendruck mehrere tausend DOM Elemente neu rendern muss. Der Engpass liegt hier nicht in der Filterberechnung selbst, sondern im Rendern der gefilterten Ergebnisse. Die Lösung ist, nur die tatsächlich sichtbaren Zeilen zu rendern, unabhängig davon, wie viele Treffer die Filterung insgesamt liefert, ein Muster bekannt als virtuelles Scrollen.
Für eine Live-Suche mit virtuellem Scrollen wird zusätzlich zur gefilterten Liste eine zweite, abgeleitete Liste berechnet, die nur die Elemente im aktuellen Scrollfenster plus einen kleinen Puffer enthält. Diese Kombination aus Debounce für die Eingabe und virtuellem Scrollen für die Ausgabe erlaubt eine Live-Suche über zehntausende Einträge, die sich exakt so reaktiv anfühlt wie eine Suche über wenige Dutzend Einträge.
8. Tastaturnavigation durch die Ergebnisliste
Eine vollständige Live-Suche lässt sich nicht nur per Maus, sondern auch komplett über die Tastatur bedienen. Pfeil hoch und Pfeil runter bewegen einen Fokus Index durch die gefilterte Liste, Enter wählt den aktuell markierten Eintrag aus, und Escape leert das Suchfeld oder schließt die Ergebnisliste. Diese Interaktion orientiert sich am bekannten Verhalten von Autocomplete Comboboxen und erhöht die Zugänglichkeit der Live-Suche erheblich.
Wichtig ist, den Fokus Index bei jeder Änderung der Suchanfrage zurückzusetzen, da sich sonst der markierte Index auf ein anderes Element als erwartet bezieht, sobald sich die gefilterte Liste durch neue Eingabe verändert. Ein einfacher Reset auf 0 innerhalb des Debounce Callbacks stellt sicher, dass die Tastaturnavigation der Live-Suche immer mit dem ersten sichtbaren Treffer beginnt.
function liveSearch() {
return {
query: '',
allItems: [],
activeIndex: 0,
get filteredItems() {
const needle = this.query.trim().toLowerCase();
if (!needle) return this.allItems;
return this.allItems.filter((item) => item.name.toLowerCase().includes(needle));
},
onKeydown(event) {
const max = this.filteredItems.length - 1;
if (event.key === 'ArrowDown') {
event.preventDefault();
this.activeIndex = Math.min(max, this.activeIndex + 1);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
this.activeIndex = Math.max(0, this.activeIndex - 1);
} else if (event.key === 'Enter') {
this.selectItem(this.filteredItems[this.activeIndex]);
} else if (event.key === 'Escape') {
this.query = '';
}
},
selectItem(item) {
if (!item) return;
window.location.href = item.url;
},
};
}
9. Debounce Strategien im Vergleich
Für eine Live-Suche gibt es mehrere Strategien, um die Balance zwischen Reaktionsschnelligkeit und Performance zu finden.
| Strategie | Verzögerung | Reaktionsgefühl | Eignung |
|---|---|---|---|
| Kein Debounce | 0 ms | Sofort, aber ruckelt bei großen Listen | Nur bei sehr kleinen Listen |
| Kurzes Debounce | 150 bis 200 ms | Fast unmerklich verzögert | Einfache Filter, mittlere Listen |
| Mittleres Debounce | 300 ms | Guter Kompromiss | Standardfall für die meisten Live-Suchen |
| Langes Debounce | 400 bis 600 ms | Spürbar verzögert | Serverseitige Suche, teure Filterlogik |
Für die meisten clientseitigen Anwendungsfälle liefert ein mittleres Debounce um 300 Millisekunden bei einer Live-Suche das beste Ergebnis. Nur bei serverseitiger Suche mit Netzwerk Latenz oder sehr rechenintensivem Fuzzy Matching lohnt sich eine längere Verzögerung, um unnötige Zwischenberechnungen zu vermeiden.
Mironsoft
Alpine.js Komponenten für Hyvä, Magento und individuelle Frontends
Eine performante Live-Suche oder eine andere Alpine.js Komponente nötig?
Wir entwickeln maßgeschneiderte Alpine.js Komponenten, von Live-Suchen über Filterlisten bis zu komplexen Formularen, sauber integriert in euer bestehendes Hyvä oder Magento Frontend.
Konzeption
Datenmodell und Performance Anforderungen klären
Umsetzung
Debounce, Fuzzy Matching und virtuelles Scrollen aus einer Hand
Integration
Saubere Einbindung in bestehende Hyvä und Magento Frontends
10. Zusammenfassung
Eine performante Live-Suche über große Listen basiert auf drei Grundpfeilern: x-model.debounce verzögert die Filterberechnung bis nach der letzten Eingabe, eine computed Property statt manueller Watcher hält die Filterlogik synchron und wartbar, und Fuzzy Matching toleriert kleine Tippfehler ohne externe Bibliothek. Zusammen ergibt das eine Live-Suche, die sich sofort reaktiv anfühlt, ohne die Seite bei umfangreichen Datenmengen auszubremsen.
Bei wirklich großen Listen mit mehreren zehntausend Einträgen ergänzt virtuelles Scrollen die Debounce Strategie, indem nur die tatsächlich sichtbaren Ergebnisse gerendert werden. Zusammen mit Tastaturnavigation und sicherem Treffer Highlighting entsteht eine Live-Suche, die sowohl performant als auch vollständig zugänglich ist.
Live-Suche mit Alpine.js — Das Wichtigste auf einen Blick
Debounce
x-model.debounce.300ms verzögert Updates bis nach der letzten Eingabe, meist 300 ms optimal.
Filterlogik
JavaScript Getter statt manueller Watcher, automatisch reaktiv über Alpine Proxy System.
Fuzzy Matching
Zeichen Sequenz Prüfung toleriert Tippfehler ohne externe Bibliothek.
Skalierung
Virtuelles Scrollen kombiniert mit Debounce für zehntausende Einträge.