Je viens de lancer le projet µJS, et je vais vous en expliquer le genèse, l’utilité, et en quoi il se distingue des outils équivalents déjà existants.
Petit retour en arrière
En 2017, j’avais créé la startup Skriv, qui développait un logiciel de gestion de projets qui se voulait innovant par son approche basée sur le workflow.
À l’époque, j’avais voulu rendre l’interface la plus réactive possible. Côté back-end, j’utilisais le framework PHP Temma, qui a l’avantage d’être rapide à mettre en œuvre et performant. Mais je cherchais un moyen de pousser les choses un peu plus loin.
On m’a évidemment incité à utiliser un framework JavaScript pour développer toute l’interface graphique. Mais cela impliquait de prendre le temps de monter en compétence sur une technologie que je ne maîtrisais pas ; et à la création d’une startup, le temps est une denrée précieuse.
En plus, les choses bougeaient beaucoup à l’époque : Angular était le leader, et sa deuxième version (non rétrocompatible avec la version précédente) était sortie en 2016 ; React commençait à monter, mais nécessitait de comprendre tout un écosystème morcelé ; Vue n’était encore qu’un outsider.
J’ai donc développé une toute petite bibliothèque JavaScript, qui ajoutait une surcouche AJAX sur tous les liens. Ainsi, lors d’un clic sur un lien, plutôt que de laisser le navigateur aller chercher la page, puis l’afficher en réinterprétant tout le CSS et le JS, c’est le code JavaScript qui allait chercher la page en AJAX, en envoyant une information indiquant qu’il fallait envoyer uniquement le corps de page (et non pas la page entière avec son header et son footer) ; ensuite, le corps de la page affichée à l’écran était remplacé par celui récupéré.
L’opération de remplacement est instantanée une fois la page récupérée, et en l’absence d’interprétation CSS ou JS, le changement de page paraît extrêmement fluide.
En plus de cela, l’historique du navigateur était modifié pour gérer correctement le bouton de navigation arrière du browser.
Je gardais ainsi un développement côté serveur, utilisant les techniques auxquelles j’étais habitué, et qui pouvaient être optimisées (utilisation de cache par-dessus la base de données, ou mise en cache du HTML généré).
J’étais assez fier de ce développement, qui fonctionnait vraiment très bien. Lors des démonstrations de Skriv, plusieurs personnes m’ont demandé quel framework JavaScript j’utilisais, et étaient étonnées de ma réponse.
Par la suite, j’ai voulu extraire le code spécifique de Skriv pour en faire une bibliothèque facilement intégrable dans n’importe quel site web.
Début 2021, j’ai créé la bibliothèque Vik, qui propose des capacités supplémentaires dont je n’avais pas besoin dans Skriv, mais qui peuvent être utiles sur d’autres sites (événements JavaScript, différentes stratégies de mise à jour du contenu, la gestion d’un mode « fantôme » pour ne pas mettre à jour l’historique de navigation, etc.).
Vik fonctionnait bien, et je l’ai utilisée sur plusieurs sites.
Une idée pas si nouvelle que ça
Courant 2021, je suis tombé sur cet article de DHH (créateur de Ruby on Rails et CTO de Basecamp), qui m’a fait découvrir le projet Turbo, successeur de Turbolinks, qui faisait exactement la même chose que ce que j’avais développé pour Skriv et que Vik.
Je ne m’explique pas pourquoi Turbo n’était pas apparu sur mes radars, alors que ce projet existait depuis quelques années. J’imagine qu’au milieu des années 2010 j’étais tourné sur le PHP et assez peu sur les technos front-end, et par la suite je me suis focalisé sur la création de Skriv.
En tout cas, cette découverte était une confirmation : cette manière de faire n’était pas juste un hack tordu, mais bien une manière intelligente de rendre réactif un site web tout en gardant les techniques habituelles de génération côté serveur.
Et comme j’ai pu l’écrire par ailleurs, on voit un mouvement qui va vers une simplification du développement web, sans perdre pour autant en fonctionnalités. Dans cette mouvance, on peut remarquer la bibliothèque htmx, qui ajoute des capacités de chargement dynamique au HTML.
J’ai fait évoluer Vik au fil du temps, mais le projet avait toujours une limitation : il ne permettait de mettre à jour qu’une portion de page (ou la page complète) à la fois. Cette limitation était voulue au départ, pour garder un fonctionnement simple. Mais j’ai vu apparaître des cas d’usage pour lesquels il aurait été très utile de pouvoir mettre plusieurs zones à jour à partir d’un événement unique.
Il y avait trois autres fonctionnalités manquantes : le « debouncing », pour générer un événement lors de la frappe au clavier sans pour autant le faire à chaque lettre tapée ; la répétition, pour mettre à jour automatiquement une portion de page à intervalles réguliers ; et la connexion à un flux SSE pour mettre une page à jour à partir d’événements poussés par le serveur.
Pour pouvoir ajouter ces fonctionnalités, il fallait mener un refactoring important, car cela nécessitait de revoir une bonne partie de la philosophie initiale du projet.
Et voilà µJS
J’ai pris le temps de mener cette refonte, et j’en ai profité pour renommer le projet. Je viens donc de lancer la bibliothèque µJS (aussi appelée muJS quand on est restreint à l’ASCII) et le site mujs.org.
Le principe de base reste le même. Sur un site web “normal”, il suffit d’ajouter ces deux lignes :
<script src="https://unpkg.com/@digicreon/mujs@1.4.0/dist/mu.min.js"></script>
<script>mu.init();</script>Langage du code : HTML, XML (xml)
À partir de ce moment-là, lorsque l’utilisateur clique sur un lien, la page pointée est récupérée en AJAX, et son tag <body> remplace celui de la page courante.
Il est ensuite possible de configurer le fonctionnement par défaut, ou de mettre des paramètres sur chaque lien et chaque formulaire pour indiquer si l’historique de navigation doit être mis à jour ou non, ou pour définir la source et la destination (si on veut autre chose que les tags <body>).
Exemple mettant à jour autre chose que le tag <body>
Dans cet exemple, les liens de navigation remplacent l’élément qui a l’identifiant content par celui ayant aussi l’identifiant content dans le flux récupéré en AJAX :
<nav>
<a mu-target="#content" mu-source="#content" href="/">Accueil</a>
<a mu-target="#content" mu-source="#content" href="/contact">Contact</a>
</nav>
<main id="content">
Contenu de la page
</main>Langage du code : HTML, XML (xml)
Seul #content est remplacé. La barre de navigation reste en place ; les animations CSS ne sont pas interrompues ; s’il y a un lecteur audio ou vidéo dans la page, il continue de jouer.
Si tous vos liens doivent agir sur #content, il est plus simple de le définir dans la configuration :
<script>
mu.init({
target: "#content",
source: "#content"
});
</script>Langage du code : HTML, XML (xml)
Ainsi, plus besoin d’indiquer mu-target="#content" mu-source="#content" sur tous les liens.
Exemple de recherche en temps réel
Ici, une recherche instantanée fait une requête sur le serveur à chaque caractère tapé dans le champ de saisie. Un debounce de 300 millisecondes est là pour évite la surcharge serveur (si on tape plusieurs caractères d’affilée, il n’y aura qu’une seule requête à la fin).
<input type="text" name="q" mu-url="/search"
mu-target="#results" mu-source="#results"
mu-mode="update" mu-debounce="300">
<div id="results">
<!-- résultats de recherche -->
</div>Langage du code : HTML, XML (xml)
Pas besoin de JavaScript. Sur un tag <input>, µJS utilise le trigger change par défaut et envoie la valeur du champ en paramètre GET. Le serveur reçoit une requête /search?q=… et retourne un flux HTML contenant un <div id="results">.
Exemple de mise à jour multiple
Il est possible de mettre à jour plusieurs morceaux de la page en même temps. Il suffit d’envoyer une requête en mode patch, et que la réponse du serveur contienne des éléments avec des attributs mu-patch-target.
Voici un exemple de réponse serveur :
<div class="comment" id="comment-32"
mu-patch-target="#comments" mu-patch-mode="append">
<p>Super article !</p>
<time>3 mars 2026</time>
</div>
<form id="comment-form" action="/comments" method="post"
mu-patch-target="#comment-form">
<textarea name="body"></textarea>
<button type="submit">Envoyer</button>
</form>
<span mu-patch-target="#comment-count" mu-patch-mode="update">
14 commentaires
</span>Langage du code : HTML, XML (xml)
L’exemple se comprend assez bien : le serveur a retourné trois blocs.
- Le premier bloc est un commentaire, qui va être ajouté à l’intérieur de l’élément ayant l’identifiant
comments, en le plaçant en dernier (modeappend). - Le second bloc est un formulaire vide, et va remplacer le formulaire qui a servi à envoyer un commentaire, pour qu’il apparaisse vide. Ici, le mode
replaceest implicite (c’est le mode par défaut). - Le troisième bloc sert à afficher le nombre de commentaires. Le contenu de l’élément
comment-countde la page va être mis à jour avec le contenu du bloc (modeupdate).
Juste pour le fun, voilà comment faire l’équivalent avec la bibliothèque Turbo :
<turbo-stream action="append" target="comments">
<template>
<div class="comment" id="comment-32">
<p>Super article !</p>
<time>3 mars 2026</time>
</div>
</template>
</turbo-stream>
<turbo-stream action="replace" target="comment-form">
<template>
<form id="comment-form" action="/comments" method="post">
<textarea name="body"></textarea>
<button type="submit">Envoyer</button>
</form>
</template>
</turbo-stream>
<turbo-stream action="update" target="comment-count">
<template>
14 commentaires
</template>
</turbo-stream>Langage du code : HTML, XML (xml)
C’est beaucoup plus verbeux avec Turbo-Stream. Et surtout, avec µJS vous pouvez avoir exactement le même code HTML dans votre page d’origine et dans celle que vous récupérez en mode patch. Lors du chargement initial, les attributs mu-patch-target n’ont pas d’effet ; par contre, au moment de patcher la page, ces attributs sont utilisés.
Exemple de temps réel avec SSE
Voici un exemple de chat en temps réel, sans websocket et sans bibliothèque supplémentaire :
<div id="messages">
<!-- liste des messages -->
</div>
<div mu-url="/chat/stream" mu-method="sse"
mu-mode="patch" mu-trigger="load"></div>
<form action="/chat/send" method="post" mu-mode="patch">
<input type="text" name="message" autocomplete="off">
<button type="submit">Envoyer</button>
</form>Langage du code : HTML, XML (xml)
Le <div> avec mu-method="sse" ouvre une connexion EventSource au chargement de la page. Chaque message envoyé par le serveur en SSE est du HTML injecté via patch. Par exemple :
<div class="msg" mu-patch-target="#messages" mu-patch-mode="append">
<b>Alice</b> : Salut !
</div>Langage du code : HTML, XML (xml)
Pour faire une autre comparaison, avec htmx il faut charger une extension pour gérer les SSE, et toutes les mises à jour doivent se faire dans un container sse-connect (alors qu’avec µJS on utilise le système de patch classique, donc on peut cibler n’importe quel élément).
Voici l’équivalent de l’exemple précédent avec htmx :
<script src="htmx-ext/sse.js"></script>
<div id="messages">
<!-- liste des messages -->
</div>
<div hx-ext="sse" sse-connect="/chat/stream">
<div id="messages-listener" sse-swap="messages"
hx-target="#messages" hx-swap="beforeend"></div>
</div>
<form hx-post="/chat/send" hx-target="#messages"
hx-swap="beforeend" hx-select="#new-message">
<input type="text" name="message" autocomplete="off">
<button type="submit">Envoyer</button>
</form>Langage du code : HTML, XML (xml)
On peut voir qu’avec µJS le code est beaucoup plus simple, avec moins de tags et moins d’attributs spécifiques.
Mais aussi…
Il y a plein d’autres fonctionnalités à découvrir.
- Modes d’exécution : replace, update, prepend, append, before, after, remove, none, patch
- Méthodes HTTP : GET, POST, PUT, PATCH, DELETE
- Déclencheurs : click, submit, change, blur, focus, load
- Gestion de l’historique de navigation
- Scroll automatique au chargement de page et au retour à la page précédente
- Préchargement des pages au survol
- « DOM morphing » et « view transitions » pour améliorer le rendu du chargement
- Exécution ou non de code JavaScript embarqué
- Événements : init, before-fetch, before-render, after-render, fetch-error
- Interface programmatique : pour déclencher les traitements depuis du code JavaScript
Comparatif
Sur le site mujs.org, il y a une page qui compare µJS avec Turbo et htmx. Ces deux bibliothèque font plus de choses, mais j’ai essayé − comme d’habitude − de mettre le curseur au bon endroit en implémentant les fonctionnalités vraiment utiles.
Un des résultats, c’est que µJS ne pèse que 5 KO (minifié et gzipé), contre 16 KO pour htmlx et 25 KO pour Turbo. Si on ajoute le morphing DOM (pour faire en sorte que seuls les éléments modifiés soient mis à jour), qui est intégré dans Turbo, on en arrive à environ 8 KO pour Turbo, environ 20 KO pour htmx et toujours 25 KO pour Turbo.
Turbo est vraiment pensé pour le cas de base traité par µJS (la mise à jour de pages au cours de la navigation). Il ne supporte pas toutes les méthodes HTTP (uniquement GET et POST), ne peut pas être utilisé sur d’autres éléments que des liens et des formulaires, et n’a pas de mécanisme de polling ni de debouncing. Sans oublier que l’utilisation des Turbo Frames et des Turbo Streams est inutilement complexe par rapport à ce que ça pourrait être (cf. mon exemple ci-dessus).
htmx, de son côté, a pour but d’apporter au HTML des capacités avancées de navigation, en gérant des événements variés sur tous types d’éléments. Le cas d’usage évident est de rendre cliquable n’importe quelle <div>, pour déclencher un chargement dans n’importe quel élément. Elle est utilisée typiquement pour faire des applications web ; il lui manque donc des choses comme la gestion de l’historique de navigation.
À vous !
N’hésitez pas à aller faire un tour sur le site mujs.org. Il y a une page « Playground » qui permet de tester en voyant l’effet de µJS en temps réel.
Faites le test de simplement ajouter la bibliothèque sur votre site, et regardez ce qu’il se passe.
Faites-moi des retours 😉