Was von jQuery übrig bleibt, wenn der Browser mitzieht
querySelectorAll, classList, closest und fetch decken den größten Teil ab. Die eigentliche Arbeit steckt in den Plugins und in zwei Verhaltensunterschieden, die still neue Fehler erzeugen.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Der Umstieg bricht nicht an den Selektoren, sondern an zwei Verhaltensunterschieden
In einem gewachsenen Projekt liegt jQuery meist nicht mehr wegen seiner Selektoren im Bündel, sondern weil irgendwann eine Datentabelle, ein durchsuchbares Auswahlfeld oder ein Karussell dazukam. Der eigene Code besteht dann aus ein paar hundert Zeilen, die sich ohne Weiteres ersetzen ließen, und aus drei bis fünf fremden Erweiterungen, die es nicht tun. Wer die Bibliothek loswerden will, muss beide Teile getrennt betrachten, sonst bleibt der Umbau in der Mitte stehen.
Zwei Unterschiede erzeugen dabei zuverlässig neue Fehler. Erstens arbeitet jQuery auf einer Menge: $('#preis').text('0') läuft ohne jede Meldung durch, auch wenn es das Element auf dieser Unterseite gar nicht gibt. document.querySelector('#preis').textContent = '0' wirft an derselben Stelle einen TypeError und reißt alles mit, was in derselben Funktion danach kam. Zweitens weist fetch eine Antwort mit Status 404 oder 500 nicht ab, sondern liefert sie ganz normal aus. Wer die Fehlerbehandlung von $.ajax eins zu eins übernimmt, hat ab diesem Tag stille Ausfälle statt sichtbarer.
Beides fällt selten am Tag der Änderung auf, weil der Hauptweg ja funktioniert. Der TypeError trifft die Unterseite ohne Preisfeld, der übersehene Serverfehler trifft den Zweig, in dem etwas schiefgeht. Genau diese Wege prüft im Test niemand zuerst, und deshalb lohnt es sich, sie beim Umbau bewusst anzusteuern statt darauf zu hoffen, dass sie nebenbei auffallen.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Schritt für Schritt
- 1
Bestand zählen statt schätzen
Such im Quelltext nach jQuery-Aufrufen und sortier die Treffer nach Datei. Führ daneben eine zweite Liste mit allen fremden Erweiterungen, die jQuery voraussetzen. Die erste Liste ist Fleißarbeit, die zweite bestimmt den Zeitplan.
Geschafft, wenn: Zwei Listen liegen vor: Aufrufe je Datei und Plugins mit Abhängigkeit.
- 2
Mit den folgenlosen Aufrufen anfangen
Ersetz zuerst addClass, removeClass und text durch classList und textContent, die verhalten sich gleich. Bei attr und html sieh genauer hin: attr('checked') liest das Attribut und nicht den aktuellen Zustand des Elements, dafür gibt es die gleichnamige Eigenschaft, und html() entfernt vorher die von jQuery verwalteten Handler und Daten, während innerHTML sie einfach überschreibt.
Geschafft, wenn: Ein großer Teil der Treffer ist weg, und für attr und html steht eine kurze Liste der Stellen, die einzeln zu prüfen sind.
- 3
Auf fehlende Elemente vorbereiten
Geh die neuen querySelector-Aufrufe durch und entscheide je Stelle, ob ein fehlendes Element harmlos ist oder auffallen soll. Bau dafür entweder ein if oder eine kleine Hilfsfunktion, die wie jQuery bei null Treffern nichts tut.
Geschafft, wenn: Der Aufruf einer Unterseite ohne das erwartete Element wirft keinen TypeError mehr.
- 4
Events umstellen und die Delegation zuletzt
Zieh direkte Handler auf addEventListener um, danach die delegierten mit closest und der contains-Prüfung. Handler, die zu einem Modul gehören, bekommen ein gemeinsames signal aus einem AbortController.
Geschafft, wenn: Auch nach dem Neuzeichnen einer Liste reagieren die Schaltflächen darin.
- 5
Ajax auf fetch umbauen und den Fehlerpfad nachbauen
Setz die Prüfung auf response.ok, die ausdrückliche Umwandlung mit response.json und die Zeitgrenze über AbortSignal.timeout. Prüf auf der Serverseite, ob dort etwas an X-Requested-With hängt.
Geschafft, wenn: Ein absichtlich erzwungener Serverfehler landet im selben Zweig wie vorher.
- 6
Animationen an CSS abgeben
fadeIn, fadeOut und slideToggle ersetzt du durch eine Klasse mit CSS-Übergang oder durch element.animate. Der Gewinn liegt nicht darin, dass prefers-reduced-motion erst jetzt möglich wäre, das ginge mit jQuery.fx.off ebenfalls, sondern darin, dass die Medienabfrage einmal im Stylesheet steht statt in jeder einzelnen Aufrufstelle.
Geschafft, wenn: Wer Bewegung im Betriebssystem abbestellt hat, bekommt die Übergänge nicht mehr, und zwar an allen Stellen gleichzeitig.
- 7
Die Bibliothek abschalten, bevor du sie entfernst
Bau eine Testfassung, in der $ und jQuery nicht mehr global gesetzt sind, und klick die Anwendung einmal komplett durch. Danach nimmst du die Abhängigkeit aus dem Bündel und setzt die Linter-Regel gegen neue Verwendungen.
Geschafft, wenn: Ein vollständiger Durchlauf meldet in der Konsole kein fehlendes $ mehr.
Sechs jQuery-Aufrufe und ihr heutiges Gegenstück
- 01 $('.karte') wird zu document.querySelectorAll('.karte') und liefert eine NodeList.
- 02 addClass, removeClass und toggleClass laufen alle über element.classList.
- 03 Delegierte Klicks gehen über addEventListener und event.target.closest.
- 04 $.ajax wird zu fetch, aber fetch meldet bei 404 und 500 keinen Fehler.
- 05 fadeIn und slideUp ersetzt du durch CSS-Übergänge oder element.animate.
- 06 $(document).ready entfällt, sobald das script-Tag das Attribut defer trägt.
Was du danach ohne Nachschlagen ersetzen kannst
Der Umbau besteht aus wenigen Mustern, die sich immer wiederholen. Wenn du diese Muster einmal sauber gebaut hast, ist der Rest Fleißarbeit, die auch jemand anderes im Team übernehmen kann, ohne dass du jede Zeile nachlesen musst.
Selektoren mit ihrem Rückgabewert verstehen
Du weißt, dass querySelector null liefert und querySelectorAll eine statische NodeList mit forEach, aber ohne map und filter. Für Array-Methoden schiebst du Array.from davor, und den Fall ohne Treffer fängst du ab, statt ihn wie jQuery zu verschlucken.
Klassen, Attribute und Datenfelder trennen
classList übernimmt add, remove, toggle und contains. Bei .data() weißt du, dass jQuery die Werte in einem eigenen Speicher hält und dabei umwandelt, während dataset immer Zeichenketten liefert und tatsächlich im DOM steht.
Delegierte Events sauber nachbauen
Du hängst einen Handler an den Container, holst dir mit event.target.closest das eigentliche Ziel und prüfst mit container.contains, dass du nicht aus dem Container herausgeklettert bist. Damit überlebt der Handler auch neu gezeichnete Listen.
Handler wieder loswerden, ohne Namensräume
jQuery konnte Handler über einen Namensraum gebündelt entfernen. Nativ gibst du addEventListener ein signal aus einem AbortController mit und entfernst mit einem einzigen abort alle Handler eines Moduls auf einmal.
fetch mit dem Fehlerpfad bauen, den $.ajax mitbrachte
Du prüfst response.ok selbst, wandelst den Text bewusst mit await response.json() um und setzt eine Zeitgrenze über AbortSignal.timeout. Erst damit verhält sich der Aufruf so, wie der alte Code es erwartet hat.
Entscheiden, welche Plugins wirklich ersetzt werden
Für jede fremde Erweiterung fällt eine eigene Entscheidung: durch eine Lösung ohne Abhängigkeit ersetzen, in einem einzelnen Modul isolieren, oder bewusst stehen lassen. Ohne diese Liste hat der Umbau kein Ende.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Selektoren: die NodeList ist weder Array noch jQuery-Objekt
$('.karte') gibt dir ein jQuery-Objekt zurück, das sich wie eine Menge verhält: Jede Methode darauf läuft für alle Treffer gleichzeitig, und bei null Treffern passiert einfach nichts. document.querySelectorAll('.karte') liefert dagegen eine statische NodeList. Sie kennt forEach, aber weder map noch filter noch reduce. Für diese Methoden schreibst du Array.from(document.querySelectorAll('.karte')) oder nimmst den Spread-Operator. Für genau einen Treffer gibt es document.querySelector, das das erste passende Element oder null zurückgibt.
Aus diesem null entsteht der häufigste neue Fehler beim Umbau. Der optionale Verkettungsoperator hilft beim Lesen, aber nicht beim Schreiben: document.querySelector('#preis')?.textContent = '0' ist kein gültiges JavaScript, weil links vom Gleichheitszeichen kein optionaler Zugriff stehen darf. Du brauchst also entweder ein const-Zwischenergebnis mit anschließendem if, oder du ziehst dir eine kleine eigene Hilfsfunktion, die eine NodeList entgegennimmt und bei null Treffern nichts tut. Letzteres bildet das alte Verhalten am nächsten nach und macht den Umbau der Aufrufe zu reiner Suchen-und-Ersetzen-Arbeit.
Ein Detail, das nur beim Entfernen von Elementen auffällt: getElementsByClassName und getElementsByTagName liefern eine lebende Sammlung, die sich beim Ändern des DOM sofort mitverändert. Wer in einer klassischen for-Schleife darüber läuft und dabei Elemente entfernt, überspringt jedes zweite. querySelectorAll gibt eine Momentaufnahme zurück und hat dieses Problem nicht, weshalb es beim Umbau die richtige Wahl ist, auch wenn die alten Aufrufe schneller getippt sind.
Events: die Delegation ist die eigentliche Arbeit
Aus $el.on('click', fn) wird el.addEventListener('click', fn), das ist der einfache Teil. Interessant wird die Delegation. $(liste).on('click', '.loeschen', fn) hängt einen Handler an die Liste, der auch für später eingefügte Zeilen gilt. Nativ schreibst du den Handler an den Container und suchst dir das eigentliche Ziel: const knopf = event.target.closest('.loeschen'); if (!knopf || !liste.contains(knopf)) return;. Die zweite Prüfung ist kein Zierrat. closest klettert im Baum nach oben, bis es fündig wird, und findet dabei auch Treffer außerhalb deines Containers, sobald der Handler weiter oben hängt.
Beim Entfernen von Handlern fehlt eine Bequemlichkeit, die viele Projekte stark genutzt haben: Namensräume. $el.off('.meinModul') warf alle Handler eines Moduls in einer Zeile weg. Nativ brauchst du dafür entweder dieselbe Funktionsreferenz wie beim Registrieren, oder du gibst allen Handlern eines Moduls ein gemeinsames signal aus einem AbortController mit, also el.addEventListener('click', fn, { signal: controller.signal }). Ein einziges controller.abort() entfernt danach alle davon. Für Handler, die genau einmal laufen sollen, gibt es außerdem { once: true }, und für Scroll- und Touch-Handler { passive: true }.
Die unangenehmste Stelle im Übergang sind eigene Ereignisse. $el.trigger('meinEvent') erzeugt kein echtes DOM-Ereignis, sondern ruft die von jQuery verwalteten Handler auf. Ein natives addEventListener('meinEvent', ...) sieht davon nichts. Umgekehrt sehen jQuery-Handler native Ereignisse sehr wohl, weil jQuery sie ganz normal registriert. Diese Asymmetrie ist der Grund, warum in der Übergangsphase einzelne Handler scheinbar grundlos nicht mehr feuern. Nativ baust du das Ereignis mit new CustomEvent('meinEvent', { detail: nutzlast, bubbles: true }) und schickst es mit el.dispatchEvent los. Solange beide Welten nebeneinander laufen, lohnt es sich, jedes eigene Ereignis auf einmal umzustellen statt Sender und Empfänger getrennt.
Netzwerk: fetch ist kein Ersatz eins zu eins
$.ajax hat einen Aufruf abgewiesen, sobald der Server mit einem Fehlerstatus geantwortet hat. fetch tut das nicht. Ein 404 oder ein 500 ist für fetch eine ganz normale Antwort, das erfüllte Promise enthält dann eben ein Response-Objekt mit ok gleich false. Nur Netzwerkfehler, ein abgebrochener Aufruf oder eine blockierte Anfrage führen zu einer Abweisung. Wenn du den Fehlerzweig aus dem alten Code übernehmen willst, gehört deshalb ein if (!response.ok) throw new Error(...) an den Anfang jeder Auswertung, sonst versucht der nachfolgende Code, eine Fehlerseite als JSON zu lesen.
Ein zweiter Unterschied betrifft den Server und wird beim Umbau regelmäßig übersehen. jQuery setzt bei Anfragen an dieselbe Herkunft den Kopfzeileneintrag X-Requested-With mit dem Wert XMLHttpRequest. Manche Backends entscheiden genau daran, ob sie JSON oder eine vollständige HTML-Seite zurückgeben, und manche Sicherheitsprüfungen hängen ebenfalls daran. fetch schickt diese Kopfzeile nicht. Wenn nach dem Umbau plötzlich HTML statt Daten ankommt oder eine Prüfung anschlägt, ist das fast immer die Ursache. Du setzt den Eintrag entweder von Hand oder passt die Erkennung auf der Serverseite an, etwa auf den Accept-Wert.
Für die restlichen Einstellungen gibt es direkte Entsprechungen. Die Zeitgrenze aus $.ajax({ timeout: 8000 }) baust du mit signal: AbortSignal.timeout(8000) nach, denselben Weg nutzt du für ein manuelles Abbrechen über einen eigenen AbortController. Cookies schickt fetch bei gleicher Herkunft mit, für andere Herkünfte brauchst du credentials: 'include'. Und die Umwandlung passiert nicht mehr automatisch anhand eines dataType, sondern ausdrücklich über await response.json() oder await response.text().
Die Plugins bestimmen den Zeitplan, nicht der eigene Code
Bootstrap in Version 4 setzt jQuery voraus, Version 5 nicht mehr. Genau diese Art von Abhängigkeit entscheidet, ob dein Ausstieg eine Woche oder ein Quartal dauert. Bei Datentabellen, durchsuchbaren Auswahlfeldern, Karussells und Bildergalerien gibt es fast immer eine Alternative ohne Abhängigkeit, aber sie hat eine andere Programmierschnittstelle, eigene Marken im HTML und ein anderes Aussehen. Der Aufwand steckt also nicht im Austausch der Bibliothek, sondern in Markup, Styling und den Stellen, an denen euer Code die Erweiterung steuert.
In der Zwischenzeit hilft eine Isolierung. Lad jQuery nur noch dort nach, wo eine dieser Erweiterungen tatsächlich läuft, statt es global im Grundbündel zu halten. Mit jQuery.noConflict(true) gibst du $ und jQuery an ihre vorherigen Besitzer zurück und behältst die Referenz nur in dem Modul, das sie noch braucht. Der Rest der Anwendung ist damit früher frei, auch wenn eine einzelne Seite noch eine Weile mitzieht.
Damit der Bestand nicht nachwächst, gehört eine Regel in die Linter-Konfiguration. Mit no-restricted-globals auf $ und jQuery meldet der Linter jede neue Verwendung, während die bereits vorhandenen Dateien über eine Ausnahmeliste stillbleiben. Ohne diese Bremse fügt die nächste schnelle Fehlerbehebung in einer alten Datei wieder drei Aufrufe hinzu, weil dort ja ohnehin schon welche stehen.
Wann du die Bibliothek bewusst liegen lässt
Eine Anwendung, an der nur noch Fehler behoben werden und die keine neuen Funktionen mehr bekommt, gewinnt durch den Umbau wenig. Der Gewinn liegt nicht in erster Linie in der eingesparten Dateigröße, sondern darin, dass neue Leute im Team eine Sprache lesen, die sie ohnehin kennen, und dass eine Abhängigkeit weniger gepflegt und aktualisiert werden muss. Wo beides nicht zieht, ist der Umbau eine Änderung ohne Nutzen und mit Risiko.
Der wirklich schlechte Zustand ist der halbe Ausstieg ohne Enddatum. Dann laufen zwei Denkweisen nebeneinander, die Asymmetrie bei eigenen Ereignissen bleibt bestehen, und niemand weiß beim Lesen einer Datei mehr, welche Regeln gerade gelten. Wenn du anfängst, leg vorher fest, welche Plugins ersetzt werden und welche bleiben dürfen, und schreib beides auf. Ein bewusst stehen gelassenes Plugin mit jQuery daneben ist ein sauberer Zustand, ein vergessenes ist es nicht.
Dazu passende Kurse
Wenn du nicht nur nachlesen, sondern an eigenem Code alte Selektoraufrufe durch Bordmittel ersetzen willst, ist das der Stoff der JavaScript-Kurse.
Weil fadeIn und slideToggle am Ende in Stylesheets landen, lohnt sich parallel ein Blick auf Kurse rund um CSS-Übergänge und Layout .
Wie sicher bist du beim Thema wirklich?
Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.
Wo du genau das übst
Ein sehr gutes, praxisorientiertes und nachhaltiges Seminar. So sollte es immer sein. Vielen Dank.
Die Schulung ist gerade für Einsteiger oder Entwickler*innen mit eingestaubtem Basiswissen sehr hilfreich.
Ich bin sehr glücklich Franz als Trainer gehabt zu haben. Er hat offensichtlich unglaublich viel Wissen zu den Themen, und schafft es, dieses auch interaktiv und verständlich weiterzugeben.
Häufige Fragen
Ist jQuery inzwischen veraltet?
Muss ich alles auf einmal ersetzen?
Was wird aus eigenen Erweiterungen, die über $.fn angelegt wurden?
Brauche ich stattdessen ein Frontend-Framework?
Deine Ansprechpartner
Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Hilft dir, aus dem Webentwicklung-Programm den passenden Kurs für deinen Stand zu finden.
Norbert Jansen
Beratung & Inhouse
Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.
Vom Ersetzen einzelner Aufrufe zu einem Frontend ohne Altlast
In den JavaScript-Kursen bei cmt gehst du die heutigen Sprachmittel an eigenem Code durch und siehst dabei, an welchen Stellen sich das Verhalten wirklich unterscheidet.