Carnet technique
Entrée N° 001mis à jour le 8 min#javascript #jquery #performance

jQuery ou JavaScript vanilla en 2026 : pourquoi j'ai arrêté jQuery

jQuery 4 pèse encore 27 ko gzip. Pourquoi je l'ai abandonné, les équivalents natifs de 2026, un tableau jQuery → JavaScript et les cas où il reste utile.

En 2021, je travaillais en agence web. Surtout des sites vitrines, avec toujours la même panoplie côté JavaScript : un slider, un peu d’Ajax, du lazy loading. Les pages obtenaient entre 90 et 100 sur Dareboost, et entre 90 et 100 aussi sur PageSpeed Insights en version bureau. Sur mobile, en revanche, le score décrochait.

En décortiquant les rapports de PageSpeed, le coupable revenait à chaque fois : jQuery. Et quand jQuery UI était chargé en plus, le score mobile baissait encore. J’ai fini par retirer jQuery de mes projets et écrire le JavaScript directement. J’avais publié un article là-dessus à l’époque ; il était brouillon et, avec le recul, faux sur plusieurs points. Voici la version réécrite, mise à jour pour 2026.

À la fin, vous saurez ce que coûte réellement jQuery aujourd’hui, par quoi remplacer chacune de ses fonctions courantes, et dans quels cas le garder reste le choix raisonnable.

Ce qu’est jQuery, et ce qu’il n’est plus

jQuery est une bibliothèque JavaScript sortie en 2006. Son intérêt tenait en deux choses : une syntaxe courte pour manipuler le DOM, les événements et l’Ajax, et surtout une couche qui gommait les différences entre navigateurs. À l’époque d’Internet Explorer 6 à 8, ce second point valait des jours de travail.

Ce problème a disparu. querySelectorAll, classList, fetch, closest ou addEventListener fonctionnent de la même façon dans tous les navigateurs actuels. Ce qui reste de jQuery, c’est la syntaxe.

La bibliothèque n’est pas morte pour autant. jQuery 4.0.0 est sortie le 17 janvier 2026, première version majeure depuis presque dix ans. Elle abandonne Internet Explorer 10 et antérieurs (IE 11 reste pris en charge jusqu’à jQuery 5), retire une série de fonctions dépréciées (jQuery.trim, jQuery.isArray, jQuery.parseJSON, jQuery.type…), passe ses sources en modules ES et accepte les Trusted Types. Le guide de migration détaille le reste.

Ce que pèse jQuery en 2026

Mon ancien article annonçait « seulement 87,4 ko » pour jQuery 3.6.0 minifié. Le chiffre était juste, le « seulement » beaucoup moins, et surtout ce n’est pas ce qui transite sur le réseau : les serveurs compressent. J’ai refait la mesure sur les fichiers officiels de code.jquery.com :

curl -sO https://code.jquery.com/jquery-4.0.0.min.js
curl -sO https://code.jquery.com/jquery-4.0.0.slim.min.js
curl -sO https://code.jquery.com/jquery-3.7.1.min.js
for f in jquery-*.js; do
  printf '%s : %s octets minifié, %s octets gzip\n' \
    "$f" "$(wc -c < "$f")" "$(gzip -9c "$f" | wc -c)"
done

Résultat :

Fichier Minifié Minifié + gzip
jquery-3.7.1.min.js 87 533 octets 30 215 octets
jquery-4.0.0.min.js 78 748 octets 27 398 octets
jquery-4.0.0.slim.min.js (sans Ajax ni Deferred) 56 032 octets 19 375 octets

Le poids de jQuery UI se mesure de la même façon : le paquet complet 1.14.1 (jquery-ui.min.js) fait 252 957 octets minifié, environ 67 ko en gzip. Un build personnalisé est plus léger, mais c’est rarement ce qu’on trouve sur un site vitrine.

Environ 27 ko compressés, ce n’est pas énorme. Le problème est ailleurs : ce code doit être téléchargé, analysé et exécuté avant que votre propre script puisse s’en servir. Sur un téléphone d’entrée de gamme, c’est ce temps d’exécution qui pèse, et c’est lui que PageSpeed Insights mesure en mode mobile, avec un processeur volontairement bridé. Pour ne faire que trois addEventListener et un fetch, le rapport coût/service est mauvais.

Pour vérifier ce que votre page utilise vraiment, l’onglet Coverage des DevTools de Chrome (menu ⋮ › More tools › Coverage) affiche, fichier par fichier, la part de code jamais exécutée pendant le chargement. Sur une page vitrine, la barre rouge de jQuery est souvent éloquente.

Les 3 boutons, version 2021 et version 2026

L’exemple de l’article d’origine : trois boutons, et un clic affiche le numéro du bouton.

<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <title>Trois boutons</title>
  <script type="module" src="boutons.js"></script>
</head>
<body>
  <button class="btn" type="button">Bouton 1</button>
  <button class="btn" type="button">Bouton 2</button>
  <button class="btn" type="button">Bouton 3</button>
</body>
</html>

La version jQuery de l’époque utilisait $(document).ready(...). Cette écriture est dépréciée depuis jQuery 3.0 au profit de $(fonction) :

$(function () {
  $('.btn').on('click', function () {
    const index = $('.btn').index(this) + 1;
    alert('Vous avez cliqué sur le bouton ' + index);
  });
});

En 2021, ma version « JavaScript pur » faisait douze lignes : une fonction ready maison, puis [].forEach.call(...) pour parcourir les éléments. J’en concluais que le natif était deux fois plus long. En réalité, j’écrivais du JavaScript de 2012. Voici l’équivalent actuel, dans boutons.js :

document.querySelectorAll('.btn').forEach((bouton, i) => {
  bouton.addEventListener('click', () => {
    alert(`Vous avez cliqué sur le bouton ${i + 1}`);
  });
});

Cinq lignes, sans dépendance. Deux choses ont changé :

  • Plus besoin d’attendre le DOM. Un script chargé avec type="module" est différé par défaut : il s’exécute après l’analyse du HTML, comme un script classique marqué defer. La fonction ready n’a plus de raison d’être.
  • NodeList a sa propre méthode forEach. Le détour par [].forEach.call servait aux vieux navigateurs.

Mieux : la délégation d’événements

Attacher un écouteur par bouton fonctionne, mais les boutons ajoutés plus tard (par un appel Ajax, par exemple) n’en auraient pas. La délégation règle ça avec un seul écouteur sur un parent, et closest retrouve le bouton même si l’utilisateur a cliqué sur une icône à l’intérieur :

<div class="actions">
  <button class="btn" type="button" data-numero="1">Bouton 1</button>
  <button class="btn" type="button" data-numero="2">Bouton 2</button>
  <button class="btn" type="button" data-numero="3">Bouton 3</button>
</div>
document.querySelector('.actions').addEventListener('click', (event) => {
  const bouton = event.target.closest('.btn');
  if (!bouton) return;
  alert(`Vous avez cliqué sur le bouton ${bouton.dataset.numero}`);
});

C’est exactement ce que fait $('.actions').on('click', '.btn', ...) en jQuery, sans la bibliothèque. Le numéro passe par un attribut data-*, lu avec dataset, plutôt que par la position dans la page : on peut réordonner les boutons sans casser le script.

Tableau de correspondances jQuery → JavaScript natif

Les fonctions que j’utilisais le plus, et leur équivalent. Tout fonctionne dans les navigateurs actuels sans polyfill.

jQuery JavaScript natif
$(fn) / $(document).ready(fn) <script defer> ou <script type="module"> ; sinon document.addEventListener('DOMContentLoaded', fn)
$('.a') document.querySelectorAll('.a')
$('#id') document.getElementById('id') ou document.querySelector('#id')
$el.find('.b') el.querySelectorAll('.b')
$el.closest('.c') el.closest('.c')
$el.parent() el.parentElement
$els.each(fn) els.forEach(fn)
$el.on('click', fn) el.addEventListener('click', fn)
$el.on('click', '.btn', fn) un écouteur sur el + event.target.closest('.btn')
$el.one('click', fn) el.addEventListener('click', fn, { once: true })
$el.off('click', fn) el.removeEventListener('click', fn)
$el.trigger('maj') el.dispatchEvent(new CustomEvent('maj', { bubbles: true }))
$el.addClass('x') el.classList.add('x')
$el.removeClass('x') el.classList.remove('x')
$el.toggleClass('x') el.classList.toggle('x')
$el.hasClass('x') el.classList.contains('x')
$el.attr('href') el.getAttribute('href')
$el.attr('href', url) el.setAttribute('href', url)
$el.data('id') el.dataset.id (toujours une chaîne, pas de conversion)
$el.text() / $el.text(s) el.textContent
$el.html(s) el.innerHTML en écriture (jamais avec une donnée utilisateur)
$el.empty().append(noeud) el.replaceChildren(noeud)
$el.val() el.value
$el.css('color', 'red') el.style.color = 'red'
$el.hide() / $el.show() el.hidden = true / el.hidden = false
$el.append(noeud) el.append(noeud)
$el.remove() el.remove()
$el.fadeIn() transition CSS, ou el.animate(...)
$.ajax / $.get / $.getJSON fetch(url) puis response.json()
$.extend({}, a, b) { ...a, ...b } ou Object.assign({}, a, b)
$.extend(true, {}, a) structuredClone(a)
$.trim(s) (retiré de jQuery 4) s.trim()

Une différence de comportement à connaître : un sélecteur jQuery qui ne trouve rien renvoie une collection vide, et les méthodes appelées dessus ne font rien. document.querySelector renvoie null, et null.classList lève une erreur. L’opérateur ?. couvre les cas où l’absence est normale : document.querySelector('.bandeau')?.remove().

L’Ajax avec fetch

C’était le deuxième usage de jQuery sur mes sites vitrines. Un chargement de contenu en Ajax, avec fetch et async/await :

const liste = document.querySelector('.actualites');

async function chargerActualites(page) {
  const reponse = await fetch(`/api/actualites?page=${page}`, {
    headers: { Accept: 'application/json' },
  });
  if (!reponse.ok) {
    throw new Error(`HTTP ${reponse.status}`);
  }
  const articles = await reponse.json();
  for (const article of articles) {
    const li = document.createElement('li');
    li.textContent = article.titre;
    liste.append(li);
  }
}

chargerActualites(1).catch((erreur) => console.error(erreur));

Le piège classique : contrairement à $.ajax, fetch ne rejette pas la promesse sur une erreur HTTP. Une réponse 404 ou 500 est une réponse valide ; il faut tester reponse.ok soi-même. La promesse n’est rejetée qu’en cas d’échec réseau.

Pour envoyer un formulaire, FormData remplace $(form).serialize() :

const formulaire = document.querySelector('#contact');

formulaire.addEventListener('submit', async (event) => {
  event.preventDefault();
  const reponse = await fetch(formulaire.action, {
    method: 'POST',
    body: new FormData(formulaire),
  });
  formulaire.querySelector('.statut').textContent =
    reponse.ok ? 'Message envoyé.' : 'Échec de l’envoi.';
});

Le navigateur fixe lui-même l’en-tête Content-Type (en multipart/form-data) ; ne le forcez pas à la main.

Animations, slider et lazy loading sans bibliothèque

Les deux autres usages de mes sites vitrines, sliders et lazy loading, ont désormais une réponse native, souvent sans une ligne de JavaScript.

Les effets fadeIn / slideUp se font en CSS avec une transition sur une classe, que JavaScript se contente d’ajouter :

.panneau {
  opacity: 0;
  transition: opacity 200ms ease-out;
}
.panneau.visible {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .panneau { transition: none; }
}
document.querySelector('.panneau').classList.add('visible');

Quand l’animation doit être pilotée en JavaScript (enchaînement, attente de la fin), la Web Animations API fait le travail de .animate() de jQuery et renvoie une promesse :

const panneau = document.querySelector('.panneau');
const animation = panneau.animate(
  [{ opacity: 0 }, { opacity: 1 }],
  { duration: 200, easing: 'ease-out', fill: 'forwards' },
);
await animation.finished;

(await au niveau supérieur fonctionne dans un script type="module".)

Un slider simple se construit avec CSS Scroll Snap : le défilement tactile, l’inertie et l’arrêt sur chaque diapositive sont gérés par le navigateur.

.slider {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}
.slider > * {
  flex: 0 0 100%;
  scroll-snap-align: start;
}

Des boutons précédent/suivant se branchent en quelques lignes avec slider.scrollBy({ left: slider.clientWidth, behavior: 'smooth' }).

Le lazy loading des images est un attribut HTML, loading="lazy", pris en charge par tous les navigateurs actuels :

<img src="photo.webp" alt="Façade de la boutique" width="800" height="600" loading="lazy">

Ne le mettez pas sur l’image principale visible au chargement : elle serait retardée, ce qui dégrade justement le LCP mesuré par PageSpeed.

Côté jQuery UI, plusieurs widgets ont aussi un équivalent natif : <dialog> pour les fenêtres modales, <details> pour les accordéons, <input type="date"> pour le sélecteur de date.

Quand garder jQuery reste raisonnable

Retirer jQuery n’est pas une fin en soi. Je le garde, ou je ne m’en préoccupe pas, dans ces cas :

  • Un projet existant qui fonctionne. Réécrire des milliers de lignes de jQuery sans raison mesurable, c’est du risque sans bénéfice. Mieux vaut écrire le nouveau code en natif et migrer au fil des modifications.
  • WordPress. jQuery est livré avec WordPress et de nombreux thèmes et extensions en dépendent. Si la page le charge de toute façon, l’utiliser dans votre script ne coûte rien de plus. Le passage de WordPress à jQuery 4 est suivi par l’équipe du cœur ; c’est un bon moment pour vérifier ce que vos propres scripts utilisent.
  • Un plugin jQuery sans équivalent crédible. Si un plugin éprouvé fait exactement le travail, le remplacer par du code maison n’est pas forcément un progrès.

Pour une mise à jour vers jQuery 4, le plugin jQuery Migrate signale dans la console les appels à des fonctions retirées.

Pour un nouveau site vitrine, en revanche, je ne vois plus d’argument : tout ce que j’y faisais avec jQuery en 2021 tient en quelques lignes natives.

À retenir

  • jQuery 4.0.0 (janvier 2026) pèse environ 27 ko en gzip, 19 ko en version slim. Le coût réel est l’exécution sur mobile, pas le téléchargement.
  • type="module" ou defer remplacent $(document).ready. querySelectorAll(...).forEach et addEventListener remplacent $('.x').on(...).
  • La délégation (event.target.closest(...)) remplace .on('click', '.btn', fn) et couvre les éléments ajoutés plus tard.
  • fetch ne rejette pas sur un 404 ou un 500 : testez response.ok.
  • Transitions CSS, Web Animations API, scroll snap et loading="lazy" couvrent les animations, sliders et lazy loading.
  • Sur un projet legacy ou WordPress qui charge déjà jQuery, le garder est un choix raisonnable ; sur un site neuf, il n’apporte plus rien.
PartagerLinkedInBlueskyXRedditHacker News

Commentaires et réactions

Les commentaires sont hébergés par GitHub Discussions, via giscus. Rien n'est chargé avant ce clic.