La véritable complexité du JavaScript moderne provient des outils, et non du langage lui-même.
Cet article explique comment des fonctionnalités essentielles de JavaScript telles que async/await et le chaining optionnel simplifient le code, tandis qu’un excès d’outils et de dépendances crée une complexité inutile.
Il y a un moment étrange dans le développement en JavaScript lorsque l’on se rend compte qu’on en sait trop.
Pas trop de JavaScript lui-même.
Trop d’informations sur l’écosystème entourant JavaScript.
components.
Puis vous ouvrez un petit fichier JavaScript et vous réalisez que la logique réelle se résume à ceci :
const users = await fetch("/api/users").then(res => res.json());
C’est tout.
Le langage en soi est souvent étonnamment simple.
Ce qui rend le JavaScript moderne intimidant, c’est tout ce qui s’y superpose.
Voir les bases de code JavaScript passer d’un petit nombre de fichiers à des systèmes contenant des centaines de dépendances montre que nous confondons la sophistication avec une véritable complexité.
Travailler avec le JavaScript moderne ne signifie pas adopter toutes les nouvelles fonctionnalités qu’il propose.
Cela signifie reconnaître quels problèmes le langage a déjà résolus à votre place.
Le langage a silencieusement mûri
Autrefois, le JavaScript avait la réputation d’être un langage étrange, conçu à la hâte, qui a pourtant fini par alimenter tout le web.
Une partie de cette réputation était justifiée.
Les premières versions présentaient de réels problèmes. Les APIs des navigateurs ne fonctionnaient pas de manière cohérente selon les environnements. Comprendre le code asynchrone était véritablement difficile. Le moteur de exécution ne disposait pas de modules intégrés, ce qui obligeait les gros projets à recourir à des patterns souvent plus complexes que le langage lui-même.
Le JavaScript d’aujourd’hui est une toute autre histoire.
Voici ce qui est désormais disponible :
- des variables à portée de bloc grâce à
constetlet - une syntaxe de fonction concise via les fonctions flèche
- le déstructuring pour extraire des valeurs à partir d’objets et de tableaux
- l’interpolation de chaînes avec les littéraux de template
- des paramètres par défaut pour éviter les vérifications manuelles de fallback
- la syntaxe spread pour copier et fusionner des données
- un système de modules natif
- les classes, réservées aux cas où elles sont vraiment nécessaires
async et awaitMap et Set dédiéesAucune des fonctionnalités listées ne rend le langage difficile à utiliser.
Le véritable problème est que les développeurs ont tendance à mémoriser ces éléments en tant qu’amas complexe de syntaxe, au lieu de les considérer comme un ensemble d’outils relativement compact pour exprimer des concepts de programmation courants.
Dès que les idées fondamentales deviennent claires, la syntaxe cesse de paraître intimidante.
Vous n’avez probablement pas besoin de la moitié de ces architectures
L’un des moyens les plus rapides de compliquer inutilement un projet JavaScript est de se lancer dans des décisions architecturales avant même d’avoir compris le problème.
Imaginez un tableau de bord : il doit charger des données, permettre à l’utilisateur de les modifier et enregistrer le résultat.
Une première approche sensée pourrait ressembler à ceci :
async function loadUsers() {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to load users");
} return response.json();
}
Ensuite, l’appel est aussi simple que :
const users = await loadUsers();
On peut développer une quantité surprenante de logiciels fonctionnels en se basant uniquement sur ce modèle.
Pourtant, les habitudes modernes nous poussent à poser immédiatement une série de questions au lieu de cela :
Faut-il opter immédiatement pour un framework ?
Et qu’en est-il d’une couche permettant de récupérer des données ?
Avez-vous besoin d’une bibliothèque dédiée uniquement pour gérer l’état ?
Faut-il mettre en place un mécanisme de mise en cache devant vos requêtes ?
Une bibliothèque de validation de schéma est-elle nécessaire ?
Comment les dossiers et les fichiers doivent-ils être organisés ?
Quel modèle vos composants doivent-ils suivre ?
Quel outil de construction doit assembler tout cela ?
Quelle approche le serveur doit-il utiliser pour traiter les requêtes ?
Ces questions peuvent s’avérer importantes à terme.
Elles ne sont cependant que rarement les bonnes questions pour commencer.
La première question doit toujours être :
Qu’est-ce que cette application nécessite réellement ?
Cette distinction est importante car chaque abstraction a un coût.
Chacun d’eux ajoute un concept que quelqu’un d’autre devra apprendre avant de pouvoir manipuler le code en toute sécurité.
Si une fonction ordinaire suffit à accomplir la tâche, vous n’avez probablement pas besoin d’une classe.
Si un module suffit à accomplir la tâche, vous n’avez probablement pas besoin d’un framework.
Si un objet ordinaire peut gérer votre état, vous n’avez probablement pas besoin d’un système de gestion d’état.
Si fetch() répond à vos besoins, une autre couche de récupération de données ne fera probablement qu’ajouter une distance supplémentaire entre vous et le réseau.
Rien de tout cela ne signifie que les frameworks sont intrinsèquement une mauvaise idée.
Cela signifie qu’un framework doit résoudre un problème que vous avez réellement, et non un problème que vous pensiez avoir.
async et await ont éliminé une grande partie de la charge mentale
Tous les gros mises à jour du JavaScript moderne ne sont pas spectaculaires.
async et await sont un excellent exemple d’amélioration discrète mais significative.
Revenons à l’ancienne approche basée sur les callbacks :
getUser(id, function (user) {
getOrders(user, function (orders) {
processOrders(orders, function (result) {
console.log(result);
});
});
});
Le problème n’était pas seulement que ce code était peu agréable à regarder.
Les callbacks profondément imbriqués rendaient vraiment difficile de suivre le flux d’exécution.
Les promesses ont amélioré la situation :
getUser(id)
.then(user => getOrders(user))
.then(orders => processOrders(orders))
.then(result => console.log(result));
Puis async et await ont encore plus rapproché le code asynchrone d’une logique apparemment séquentielle et simple :
const user = await getUser(id);
const orders = await getOrders(user);
const result = await processOrders(orders);
console.log(result);
C’est le genre de fonctionnalité qui disparaît dans l’ombre une fois qu’elle devient habituelle.
C’est en fait un signe d’un bon design.
Les fonctionnalités de langage bien conçues ont tendance à se fondre dans l’usage quotidien plutôt qu’à attirer l’attention sur elles-mêmes.
L’erreur consiste à penser que, parce que le comportement asynchrone est intrinsèquement complexe, la syntaxe qui le gère doit l’être également.
La plupart du temps, ce n’est pas le cas.
Le déstructuration n’est pas un test de personnalité
Certains développeurs utilisent excessivement la déstructuration au point que le code ordinaire devient plus difficile à interpréter.
Prenons ceci :
const { name, email, address: { city } } = user;
Cela a sa place.
Mais d’autres fois, ceci est plus lisible :
const name = user.name;
const email = user.email;
const city = user.address.city;
Il n’y a pas de points bonus pour entasser plus de syntaxe en une seule ligne.
La déstructuration est utile lorsqu’elle rend la relation entre le code et les données sous-jacentes plus claire.
Par exemple :
function createUser({ name, email }) {
// ...
}
C’est expressif.
Cela indique au lecteur exactement quelle forme d’objet la fonction attend.
Mais une fois que le déstructuring devient suffisamment imbriqué pour que le lecteur doive reconstruire mentalement la structure d’objet originale, cette fonctionnalité cesse de servir à quelque chose et commence plutôt à nuire.
Le JavaScript moderne reste simple lorsque l’on considère la syntaxe comme un moyen de communiquer, et non comme une décoration.
La chaînage optionnel a résolu un vrai problème
Le chaînage optionnel en est un bon exemple.
Au moment où il n’existait pas, on voyait souvent quelque chose comme :
if (
user &&
user.profile &&
user.profile.settings &&
user.profile.settings.theme
) {
return user.profile.settings.theme;
}
Aujourd’hui, on peut écrire :
return user?.profile?.settings?.theme;
Ce n’est pas seulement plus court.
Cela exprime directement l’intention.
Le développeur dit en fait :
Ce chemin pourrait ne pas exister. S’il n’existe pas, sortir en toute sécurité.
La même logique s’applique au coalescing nullish :
const pageSize = options.pageSize ?? 20;
qui est souvent plus précis que :
const pageSize = options.pageSize || 20;
car || traite les valeurs fausses comme 0 ou "" comme si elles étaient absentes, tandis que ?? ne réagit qu’aux valeurs null ou undefined.
De telles fonctionnalités valent la peine d’être apprises, car elles éliminent des complexités accidentelles plutôt que d’ajouter des ornements stylistiques.
Voici un filtre utile à appliquer à toute nouvelle syntaxe :
Cela rend-il le code plus facile à comprendre ?
Si la réponse est oui, apprenez-la.
Si cela ne fait que donner au code un aspect impressionnant, procédez avec prudence.
JavaScript moderne ne signifie pas forcément de la gymnastique en programmation fonctionnelle
Il existe une autre source courante de complexités évitables : l’idée que le JavaScript moderne doit être écrit dans un style fortement fonctionnel.
Vous rencontrerez souvent des cas comme celui-ci :
const result = users
.filter(user => user.active)
.map(user => ({
...user,
displayName: `${user.firstName} ${user.lastName}`
}))
.reduce((groups, user) => {
const key = user.department;
groups[key] ??= [];
groups[key].push(user);
return groups;
}, {});
Rien de mal à cela en soi.
Mais il est facile d’oublier qu’une boucle simple reste un JavaScript parfaitement valide.
Cette version peut être tout aussi raisonnable :
const result = {};
for (const user of users) {
if (!user.active) continue; const department = user.department; if (!result[department]) {
result[department] = [];
} result[department].push({
...user,
displayName: `${user.firstName} ${user.lastName}`
});
}
La version avec boucle occupe plus de lignes.
Elle peut aussi être plus simple à parcourir lors du débogage.
Un code professionnel n’est pas jugé sur le nombre de lignes qu’il utilise.
Il est évalué selon l’effort cognitif inutile qu’il exige de celui qui le lit ensuite.
Parfois, map, filter et reduce réduisent cet effort.
Parfois, une boucle fait mieux le travail.
Savoir quand utiliser tel ou tel outil est plus important que de mémoriser chaque méthode d’array disponible.
Le modèle d’objets de JavaScript devient plus simple une fois que l’on cesse de lutter contre lui
Une grande partie de la confusion entourant JavaScript provient de tentatives pour l’adapter au modèle mental d’un langage différent.
Les objets JavaScript peuvent simplement conserver des données :
const user = {
name: "Sarah",
age: 31
};
Ils peuvent également conserver des fonctions :
const user = {
name: "Sarah",
greet() {
return `Hello, ${this.name}`;
}
};
On peut définir des classes :
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}`;
}
}
Mais les classes ne sont pas obligatoires dans toutes les situations.
Un objet simple associé à une fonction de fabrication est souvent tout aussi clair :
function createUser(name) {
return {
name,
greet() {
return `Hello, ${name}`;
}
};
}
Aucun de ces styles n’est supérieur dans tous les cas.
La véritable compétence consiste à reconnaître ce dont un morceau de code donné a réellement besoin.
Les classes s’avèrent utiles lorsqu’on travaille avec une véritable identité d’objet, un comportement, une gestion du cycle de vie, l’héritage ou l’encapsulation.
Les objets simples conviennent généralement aux données simples.
Les fonctions conviennent généralement aux transformations.
Les modules conviennent généralement aux frontières entre les différentes parties d’un système.
Vous n’êtes pas obligé de transformer chaque concept en classe simplement parce que le langage les prend en charge.
Le système de modules compte parmi les fonctionnalités les plus puissantes de JavaScript
Les modules natifs résolvent discrètement un problème qui générait autrefois beaucoup de complexité inutile.
Considérez ce schéma :
// math.js
export function add(a, b) {
return a + b;
}
Accompagné de :
// app.js
import { add } from "./math.js";
console.log(add(2, 3));
Cette configuration crée une frontière de dépendance claire. Le module importateur ne récupère que ce dont il a réellement besoin. La dépendance elle-même reste visible dans le code. Vous pouvez tester la fonction séparément. Et rien de tout cela ne nécessite d’entasser tout le contenu dans un espace de noms global étendu.
Ce qui rend cela significatif, c’est que l’on n’a pas besoin d’un framework pour obtenir de la modularité. Le langage lui-même permet de tracer des lignes claires entre les différentes parties du système. Un projet peut être organisé comme suit :
src/
users.js
orders.js
payments.js
validation.js
config.js
app.js
sans forcer chaque fichier à illustrer un modèle de conception.
Un module bien structuré rend généralement évidentes trois choses rien qu’en le regardant : quelle fonctionnalité lui appartient, quelles parties de cette fonctionnalité il met à disposition de l’extérieur, et quels autres fragments de code il utilise pour fonctionner.
Dès que ces éléments deviennent clairs, le projet est déjà en bon état.
Tout n’a pas besoin d’être abstrait
Ce pourrait être le point le plus important ici.
Il est courant que les développeurs introduisent l’abstraction dès qu’ils détectent du code répétitif. Mais la répétition en soi n’est pas nécessairement un problème qui mérite d’être résolu.
Disons que vous avez :
const user = await fetch("/api/users/42");
const order = await fetch("/api/orders/18");
À un moment donné, vous pourriez remarquer que la logique de traitement des réponses se répète constamment. Cette observation pourrait vous amener à écrire :
async function getJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
} return response.json();
}
Cela vous permet de simplifier en :
const user = await getJson("/api/users/42");
const order = await getJson("/api/orders/18");
Cette abstraction a sa raison d’être : elle remplit clairement une fonction précise.
Comparez cela à la création d’une usine de requêtes générique, d’un motif de stratégie de requête, d’une interface de répertoire, d’un adaptateur de transport, d’un enveloppe de réponse et d’une couche de service complète avant même d’avoir effectué une deuxième appel API. Dans ce cas, vous n’avez pas réellement réduit la complexité ; vous l’avez simplement déplacée au sein de l’architecture elle-même.
Ce motif apparaît constamment dans les bases de code JavaScript. Un développeur remarque deux lignes similaires et commence immédiatement à concevoir pour un avenir hypothétique. De bonnes pratiques d’ingénierie préconisent souvent l’approche inverse.
Attendez que la répétition devienne suffisamment évidente pour que l’abstraction appropriée se révèle d’elle-même.
Les abstractions introduites trop tôt sont particulièrement difficiles à supprimer par la suite, principalement parce qu’à un moment donné personne ne peut affirmer avec certitude si elles accomplissent encore une tâche utile.
Le navigateur fait déjà beaucoup pour vous
JavaScript moderne peut sembler plus compliqué que nécessaire, en partie parce que les développeurs oublient à quel point la plateforme elle-même peut être utile.
Vous pouvez manipuler directement le DOM. Vous pouvez ajouter des écouteurs d’événements. Vous pouvez envoyer des requêtes réseau. Vous disposez de Web Storage. Vous pouvez créer des Web Workers. Vous pouvez ouvrir des connexions WebSocket. Vous pouvez gérer l’historique du navigateur. Vous pouvez travailler avec des formulaires natifs.
Tout cela est disponible sans avoir besoin d’un outil spécifique pour chaque petite tâche. Par exemple :
const button = document.querySelector("#save");
button.addEventListener("click", async () => {
const response = await fetch("/api/save", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ message: "Hello" })
}); if (!response.ok) {
throw new Error("Save failed");
}
});
C’est une interaction pleinement fonctionnelle dans une application réelle, sans aucun framework impliqué.
Rien de tout cela n’est un argument contre les frameworks — ils résolvent des problèmes réels. Mais la plateforme sous-jacente mérite plus de reconnaissance qu’elle n’en reçoit souvent. Une fois que vous comprenez ce que le navigateur et le langage vous offrent déjà gratuitement, vous êtes en meilleure position pour décider de ce qu’un framework devrait réellement gérer.
Les dépendances ont un coût
Installer un paquet peut vous faire gagner du temps réel. Il peut aussi vous lier à des engagements à long terme pour quelque chose qui aurait pu être écrit en dix lignes de code par vous-même.
Prenons un petit outil comme :
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
Il est très probable que vous n’ayez pas besoin d’un package externe pour cela. Cependant, des éléments tels que l’authentification, la cryptographie, les calculs de dates complexes, les clients de base de données, le traitement d’images et des logiques de validation avancées relèvent d’une catégorie complètement différente.
La leçon ici n’est pas « éviter les dépendances à tout prix ». Il s’agit plutôt de savoir quel est le coût réel de chacune d’elles.
Chaque dépendance entraîne :
- une maintenance continue
- des implications en matière de sécurité
- des travaux d’upgrade de version
- de la documentation à lire
- un arbre de dépendances transitives
- des effets sur votre processus de compilation
- une abstraction supplémentaire à apprendre
Les meilleurs développeurs JavaScript ne sont pas nécessairement ceux qui ont mémorisé le plus grand nombre de packages npm. Ce sont ceux qui savent déterminer quand un package n’est pas vraiment nécessaire.
TypeScript ajoute une complexité perçue, mais c’est un sujet à part
Dans les projets ayant de gros volumes de code, TypeScript peut améliorer significativement l’expérience de développement au quotidien. En même temps, il ajoute une couche supplémentaire à le langage.
On finit par écrire des choses comme :
type User = {
id: number;
name: string;
email: string;
};
En plus de :
function getUser(id: number): Promise<User> {
// ...
}
Cela a une réelle valeur. Il est cependant important de distinguer la complexité provenant du langage de celle issue de l’installation des outils utilisés.
Le JavaScript en lui-même ne devient pas plus difficile simplement parce qu’un projet y ajoute un vérificateur de types, un linter, un agrégateur de modules, un framework UI, un ensemble de tests automatisés et plusieurs plugins de compilation. Ce qui devient plus complexe, c’est votre environnement de développement, et cette sophistication peut certainement se révéler bénéfique.
Mais un ensemble d’outils sophistiqué ne doit pas vous amener à conclure que le langage qui le sous-tend est intrinsèquement compliqué. Souvent, il est en réalité assez simple.
La véritable compétence réside dans le choix de l’abstraction la plus concise
Un schéma récurrent se retrouve dans tout ce qui a été abordé jusqu’à présent.
Rédiger du bon JavaScript a moins à voir avec la maîtrise de chaque fonctionnalité disponible et plus avec le choix de l’outil le plus léger capable d’accomplir la tâche.
Nécessité de transformer le contenu d’un tableau ? Utilisez map.
Nécessité de filtrer un ensemble de valeurs ? Utilisez filter.
Nécessité d’organiser des opérations asynchrones ? Utilisez async et await.
Besoin d’un morceau de logique réutilisable avec des limites claires ? Écrivez une fonction ou un module.
Besoin d’une collection dont les clés ne sont pas limitées aux chaînes de caractères ? Examinez Map.
Avez-vous besoin de garantir l’unicité ? Regardez Set.
Avez-vous besoin d’accéder en toute sécurité à une propriété qui pourrait manquer ? Utilisez la chaînage optionnel.
Avez-vous besoin d’une valeur par défaut uniquement lorsque la valeur est null ou undefined ? Utilisez ??.
Avez-vous besoin d’un conteneur de données simple ? Un objet fera l’affaire.
Avez-vous besoin d’un comportement plus riche et d’une identité liée à vos données ? Une classe pourrait convenir.
Avez-vous besoin de structurer une application vraiment volumineuse ? C’est alors qu’un framework mérite d’être envisagé.
Faites attention à cette séquence.
Vous commencez par le langage lui-même.
Puis vous examinez ce que la plateforme offre déjà.
Puis des abstractions conçues spécifiquement pour un but précis.
Puis des bibliothèques externes.
Puis des frameworks.
Inverser cet ordre est la source de beaucoup de complexités inutiles.
La simplicité n’est pas l’absence d’ingénierie
Il existe une idée répandue mais trompeuse concernant la signification réelle d’un code « simple ».
Certaines personnes assimilent la simplicité à un manque d’expérience.
Cette équation n’est pas valable.
Prenons l’exemple de quelqu’un qui débute dans ce domaine et écrit quelque chose comme ceci :
function process(data) {
// everything
}
À première vue, cela semble minimaliste, mais il est en réalité très difficile de le maintenir une fois qu’il s’agrandit.
Un développeur plus expérimenté diviserait plutôt cette même logique en cinq petits modules, chacun ayant des limites clairement définies.
Le résultat comporte plus d’éléments en mouvement, mais il est en fait beaucoup plus facile à comprendre.
C’est un type de simplicité différent, plus significatif.
L’objectif n’est pas de réduire le nombre de fichiers, la liste des dépendances ou le nombre de lignes.
L’objectif est d’éliminer la complexité qui n’a pas sa place.
Cette distinction devient de plus en plus importante à mesure que la base de code s’agrandit.
Un script ne comptant que vingt lignes peut encore être déroutant.
Une application de deux mille lignes peut, étonnamment, être facile à suivre.
Le facteur décisif n’est généralement pas la quantité brute de code.
C’est plutôt si ce code rend le modèle sous-jacent évident pour le lecteur.
Cessez de considérer chaque nouvelle fonctionnalité comme une connaissance obligatoire
JavaScript évolue rapidement, et ce rythme crée son propre type de stress.
Un nouveau framework apparaît.
Un nouvel environnement de exécution commence à gagner en popularité.
Un nouveau modèle de rendu devient la tendance.
Nouvelle syntaxe disponible.
Il est facile de se sentir constamment en retard.
Il est probable que ce ne soit pas le cas.
Vous n’êtes pas obligé d’apprendre chaque nouvelle technique JavaScript dès que quelqu’un en écrit un article.
Investissez plutôt dans les fondamentaux.
Faites-vous à la programmation asynchrone.
Faites-vous à la notion de portée.
Faites-vous à les fonctions fermées.
Faites-vous aux objets.
Faites-vous aux modules.
Faites-vous au cycle d’événements.
Faites-vous aux promesses.
Faites-vous à la gestion des erreurs.
Faites-vous aux API mises à disposition par votre navigateur ou votre environnement de exécution.
Faites-vous à la traçabilité du flux des données au sein d’une application.
Ne commencez à utiliser de nouveaux outils qu’alors, et uniquement lorsque vous êtes confronté à un problème réel qui les exige.
Cette approche permet d’acquérir des connaissances durables.
La maîtrise d’un framework spécifique peut devenir obsolète.
Un modèle mental solide du fonctionnement général des programmes, lui, ne le devient pas.
Le meilleur code JavaScript a souvent l’air ennuyeux
Les développeurs finissent généralement par se rendre compte de cela.
Un code facile à maintenir ne impressionne rarement personne dès la première lecture.
Il n’empile pas cinq abstractions les unes sur les autres.
Il n’exige pas de diagramme pour que la première fonction devienne compréhensible.
Il n’utilise pas toutes les fonctionnalités ajoutées par le langage dans sa dernière version.
Souvent, il a simplement l’air ordinaire.
Ce n’est pas une critique, c’est un atout.
Lorsque vous ouvrez le fichier, l’intention est immédiatement claire :
import { getUser } from "./users.js";
export async function loadProfile(id) {
const user = await getUser(id); if (!user) {
throw new Error("User not found");
} return {
name: user.name,
email: user.email
};
}
Il n’y a presque rien à déchiffrer.
La fonction récupère un utilisateur.
Elle vérifie si cette recherche a réussi.
Elle renvoie les données réellement nécessaires.
Rien ici ne cherche à prouver à quel point son auteur est intelligent.
Cela évite simplement de faire accomplir au lecteur un travail inutile, et c’est une caractéristique bien plus précieuse dans le code.
Le JavaScript moderne est plus simple que nos projets
JavaScript présente ses propres défauts réels. Tous les langages en ont.
Mais une grande partie de la complexité que l’on attribue au « JavaScript moderne » ne provient pas réellement du langage lui-même.
Elle provient des couches ajoutées par-dessus.
Des frameworks.
Des systèmes de construction.
Des dépendances.
Des conventions.
Des abstractions.
Des outils.
Des choix architecturaux.
Certaines de ces couches sont véritablement justifiées.
D’autres ne le sont pas.
La bonne question n’est pas de savoir si un outil est considéré comme moderne.
La bonne question est de savoir s’il réduit réellement la complexité pour le problème spécifique qui se pose.
Le meilleur JavaScript n’est pas celui qui utilise le plus d’outils.
C’est plutôt le code dans lequel chaque outil inclus a véritablement mérité sa place.