029 —Frontend
jQuery uitfaseren uit een legacy theme: een draaiboek
Oude themes laden jQuery vaak voor twee regels code: een hamburgermenu en een smooth scroll. Zo haal je het weg zonder zaterdag aan rollbacks te besteden.
Een bureau waar we mee samenwerken stuurde vorige week een header.php door. De site is uit 2017, gebouwd voor een regionaal transportbedrijf in Zuid-Nederland. Het theme laadt zes JavaScript-bestanden. Vier daarvan zijn jQuery-plugins (slick, fancybox, een sticky-header library, en eentje waarvan niemand zich nog kan herinneren wie hem heeft toegevoegd). De andere twee zijn snippets van elf regels die een mobiel menu togglen en de hero laten infaden. Wat de pagina daadwerkelijk doet: een hamburgerknop, een accordion in de footer, en een hero die infadet. Totale payload: 87 KB JavaScript, waarvan 32 KB jQuery zelf is, plus nog 18 KB aan plugins die jQuery meesleept.
Dit is de standaardvorm van een zeven jaar oud WordPress-theme. De interessante vraag is niet óf je jQuery moet uitfaseren. De interessante vraag is hoe je dat doet zonder je zaterdag kwijt te zijn aan rollbacks. Hieronder staat het draaiboek dat we gebruiken op een doorsnee legacy site, in de volgorde waarin we het toepassen. Het werkt net zo goed op een Drupal 7-theme, een Joomla 3-template, of welke andere PHP-gerenderde site dan ook die JavaScript heeft leren kennen via jQuery.
Inventariseer voor je iets aanraakt
Voordat je ook maar één wp_enqueue_script-aanroep verwijdert, breng eerst in kaart waar jQuery overal wordt aangeroepen. Twee greps dekken de meeste themes:
cd wp-content/themes/your-theme
grep -rn "jQuery" --include="*.js" --include="*.php" .
grep -rn '\$(' --include="*.js" .
grep -rn "wp_enqueue_script.*jquery" --include="*.php" ../..
De eerste twee greps laten zien wat het theme zelf met jQuery doet. De derde laat zien wie er nog meer op de site een dependency heeft gedeclareerd: plugins, het parent-theme, een child-theme, een page builder. Je krijgt jQuery niet volledig van de pagina af zolang iets anders het ook nog binnenhaalt, en dat is prima. De eerste ronde gaat over je eigen gebruik. jQuery die plugins binnenhalen is een aparte beslissing, voor later.
Catalogiseer elke aanroep. Een spreadsheet is genoeg. Wij doen het in drie kolommen: het bestand en regelnummer, de jQuery-functie die wordt gebruikt, en het equivalent in modern JavaScript. Na een half uur heb je zoiets als dit:
header.js:14·$('.menu-toggle').on('click', ...)·document.querySelector('.menu-toggle').addEventListener('click', ...)footer.js:8·$('.faq').slideToggle()· CSS-transition opgrid-template-rowsplus een class togglehero.js:22·$.ajax({...})·fetch(...)slider.js:1· slick-plugin · nu nog laten staan, vervangen in de tweede ronde
Het nut van die matrix is dat je vrijwel direct ziet dat 80 procent van de aanroepen events, selectors en class-toggling zijn. Daarvoor bestaan al ruim tien jaar native equivalenten. De resterende 20 procent is animatie, AJAX, en hier en daar een plugin. Voor elk daarvan neem je een aparte beslissing, geen herschrijving over de hele linie.
De vervangingsmatrix
Bij de meeste legacy themes zijn de vervangingen mechanisch. De lijst hieronder dekt de aanroepen die we zien in ongeveer negen van de tien audits.
Selectors en events
// jQuery
$('.menu-toggle').on('click', function () {
$(this).toggleClass('is-open');
$('.site-nav').slideToggle(200);
});
// Modern
document.querySelector('.menu-toggle').addEventListener('click', (e) => {
e.currentTarget.classList.toggle('is-open');
document.querySelector('.site-nav').classList.toggle('is-open');
});
De slideToggle wordt een CSS-transition op max-height of, op moderne browsers, interpolatie op grid-template-rows. Het gedrag is identiek, de bundle is weg, en de animatie loopt nu echt op de compositor in plaats van aangedreven door setInterval.
DOM-traversal
// jQuery
$('.card').each(function () {
$(this).find('.title').text($(this).data('label'));
});
// Modern
document.querySelectorAll('.card').forEach((card) => {
card.querySelector('.title').textContent = card.dataset.label;
});
Het denkmodel blijft hetzelfde. find wordt querySelector, each wordt forEach, .data('x') wordt .dataset.x. De meeste teams schrijven betere code in de tweede ronde, omdat de native API hen dwingt na te denken of ze één element of meerdere willen, in plaats van dat jQuery dat verschil stilletjes wegmoffelt.
AJAX
fetch is de voor de hand liggende vervanger, maar $.ajax heeft één gewoonte waar mensen op stuiten: het stuurt een X-Requested-With: XMLHttpRequest header mee, en WordPress' admin-ajax.php en diverse custom REST-endpoints controleren daarop. Zet die dus expliciet:
const res = await fetch('/wp-admin/admin-ajax.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
},
body: new URLSearchParams({ action: 'get_posts', cat: '4' }),
});
const data = await res.json();
Vrijwel elk legacy theme wikkelt zijn bootstrap ook in jQuery(document).ready(). Het native equivalent is document.addEventListener('DOMContentLoaded', fn), en in de meeste gevallen heb je het helemaal niet nodig: de script-tag aan het eind van <body> zetten, of hem op defer zetten, heeft hetzelfde effect.
Zet de wijziging achter een feature flag
Dit is het deel dat de meeste tutorials overslaan. Je deployt een jQuery-verwijdering niet in één keer. Je zet de nieuwe code live, laat de oude code staan, en switcht ertussen met een query parameter of een cookie. De kleinste versie hiervan in WordPress is een handvol regels in functions.php:
function pier_use_modern_js() {
return isset( $_GET['modernjs'] ) || isset( $_COOKIE['modernjs'] );
}
add_action( 'wp_enqueue_scripts', function () {
$dir = get_template_directory_uri() . '/assets/js';
if ( pier_use_modern_js() ) {
if ( ! is_admin_bar_showing() ) {
wp_dequeue_script( 'jquery' );
}
wp_enqueue_script( 'theme-modern', $dir . '/modern.js', [], '1.0', true );
} else {
wp_enqueue_script( 'theme-legacy', $dir . '/legacy.js', [ 'jquery' ], '1.0', true );
}
}, 100 );
Nu kun je de site bezoeken via ?modernjs=1 en zie je het nieuwe code-pad. Loop het menu, de accordions, het contactformulier, het zoekvak en de cookiebanner door. Als je tevreden bent, draai je de default om en houd je het cookie-pad open voor een rollback. Een week later gooi je de legacy-branch er helemaal uit.
Vang de regressies op die je niet zag aankomen
De zichtbare regressies (menu opent niet, formulier verstuurt niet) heb je binnen vijf minuten klikken te pakken. De onzichtbare zijn wat je zaterdag opslokt. Drie gewoontes helpen.
Diff de gerenderde HTML. Sla voor en na de cutover de gerenderde HTML van tien representatieve pagina's op met curl en draai diff. De meeste verschillen zullen nonces en timestamps zijn. Al het andere is een tweede blik waard.
Houd de console op productie in de gaten. Hang window.onerror aan een endpoint van één regel dat naar een logbestand schrijft. De eerste 48 uur na de cutover zie je daar elke $ is not defined uit een third-party widget langskomen, in de volgorde waarin het gebeurt, met URL en regelnummer erbij. Dat signaal is meer waard dan welke synthetische test ook.
Houd jQuery on demand laadbaar. Als een third-party plugin het nog nodig heeft, hoef je die strijd vandaag niet te voeren. Laat wp-includes/js/jquery/jquery.min.js gewoon geregistreerd staan, alleen niet door jouw theme enqueued. Plugins die een dependency declareren halen het alsnog binnen. Pagina's die dat niet doen, doen het niet. De browser cachet het één keer voor de paar pagina's die het nodig hebben, en de rest van de site is schoon.
De lastigere gevallen
Drie patronen verdienen aparte aandacht. Het eerste is jQuery UI: datepickers, sortables, dialogs. De native <input type="date"> dekt de meeste pickers, de Drag and Drop API dekt sortables, en het native <dialog>-element dekt modals inclusief focus trapping. Het tweede is jQuery Migrate, dat APIs afdekt die tussen jQuery 1.x en 3.x zijn verwijderd en zelf ook weg mag: heb je iets nodig wat het patcht, fix dan de code. Het derde zijn jQuery-afhankelijke page builders zoals oudere Visual Composer of pre-3.0 Elementor: die houden jQuery op je site in leven, wat het theme ook doet. Accepteer dat en ga door.
Voor de slider-plugins (slick, owl carousel, fancybox) is een één-op-één vervanger zelden nodig. CSS scroll-snap dekt een horizontale carousel af in ongeveer veertig regels. Het <dialog>-element dekt een lightbox af in twintig. We hebben slick-configuraties van vierhonderd regels meer dan eens vervangen door zestig regels CSS. De slider had zelden meer dan de helft nodig van wat jQuery ervoor deed.
Het doel van de oefening is geen nul jQuery op de pagina. Het doel is nul jQuery in code die jij beheert, zodat het de volgende keer dat je dit theme opent een normaal JavaScript-bestand is en geen archeologische opgraving.
Het kleinste dat je vandaag kunt doen
Open het theme. Draai de drie greps van bovenaan deze post. Plak de uitkomsten in een tekstbestand. Klaar, dat is de audit. De vervangingen zijn mechanisch zodra je de lijst voor je hebt liggen, en de feature-flag-steiger is twaalf regels PHP.
Toen we Pier bouwden, kwamen we dit patroon op klantsites steeds tegen: elk legacy theme heeft een net iets andere jQuery-footprint, en je wilt de audit, de live diff en de rollback op één plek. We hebben het uiteindelijk zo opgelost dat de version history van een bestand naast de lopende edit blijft staan, zodat een revert met één klik er altijd is, mocht de cutover je op een dinsdagochtend verrassen.
— Vragen —
Kan ik jQuery niet gewoon uit wp_enqueue_scripts halen en herladen?
Niet veilig. Plugins, de admin bar en page builders kunnen er nog steeds een dependency op declareren. Inventariseer eerst wie het nodig heeft, en dequeue daarna conditioneel voor anoniem verkeer aan de voorkant.
Is de besparing in bundle size het werk waard op een kleine site?
Op zichzelf is 32 KB marginaal. De echte winst is dat de volgende developer die het theme opent native JavaScript leest, in plaats van een plugin-stack uit 2014 die door iemand is geschreven die allang vertrokken is.
En jQuery Migrate?
Verwijder het. Het bestaat om APIs af te dekken die tussen jQuery 1.x en 3.x zijn verwijderd. Heeft jouw code die APIs niet nodig, dan is Migrate dode ballast; heeft het ze wel nodig, fix dan de code.