Accueil / Articles / Vingt-cinq scénarios d’entretien en JavaScript issus de bogues en production

Vingt-cinq scénarios d’entretien en JavaScript issus de bogues en production

Les courses, le thrashing, les paiements idempotents, les fuites de données, l’hydratation, les caches WeakMap, les tests instables et les couches de récupération partagées — avec des réponses que les interviewers souhaitent vraiment entendre.

4513 mots

De l’IA

Vingt-cinq questions de JavaScript inspirées du monde professionnel

Les fiches d’entretien demandent encore quel est le résultat de typeof null, comment fonctionne l’élévation des variables, ou comment polyfiller bind. Ces questions sont faciles à noter — et faciles à tricher après un week-end de mémorisation. Puis le même candidat propose une boîte de recherche qui affiche des résultats pour une requête que l’utilisateur a déjà effacée.

Les équipes confrontées à un trafic réel ont changé de format. Au lieu de demander une présentation sur le cycle d’événements, elles affichent un écran d’analyse figé après un changement de plage de dates, transmettent le fragment de code en question et demandent ce qui ne va pas. Mêmes connaissances de base ; signal différent. L’une vérifie la mémorisation. L’autre vérifie si quelqu’un peut raisonner sur un système inconnu tout en étant observé.

Cette lacune se manifeste le plus rapidement dans trois domaines : le comportement asynchrone sur des réseaux réels (réponses hors ordre, promesses qui se résolvent mais ne sont plus pertinentes), la mémoire (rien ne plante avec un ordinateur portable laissé allumé pendant six minutes), et les pannes (un fournisseur renvoie un HTTP 200 avec un corps d’erreur en HTML, ce qui provoque une erreur avec response.json() à 2 heures du matin).

Ce qui suit est une liste de vingt-cinq scénarios tirés de bugs ayant réellement atteint l’environnement de production : tableaux de bord lents, requêtes dupliquées, augmentation de la mémoire lors des changements de route, résultats de recherche obsolètes, facturations doubles, hypothèses d’authentification erronées, tables de vingt mille lignes, et API fonctionnelles 96 % du temps. Chaque cas présente la situation, la réponse attendue par les intervieweurs, un bref exemple de code, une approche courante erronée, une étape probable suivante, ainsi que ce qui est mesuré.

Les exemples sont d’abord en JavaScript, avec des notes TypeScript uniquement lorsque les types modifient la réponse. Niveaux : débutant pour une préparation de niveau intermédiaire, intermédiaire pour la plupart des écrans avancés, avancé pour les conversations avec le personnel et les dirigeants.

Débutant — fondamentaux de la production

Ceux-ci distinguent les personnes qui ont déjà mis en production du code de celles qui n’ont terminé que des tutoriels. Rien d’obscur ; tout cela peut poser des problèmes dans les applications réelles.

Debounce vs throttle pour la recherche et le défilement

Situation. Un champ de recherche envoie une requête à chaque frappe. Il est nécessaire d’effectuer moins d’appels sans que la saisie ne paraisse bloquée. Les gestionnaires de défilement ailleurs nécessitent des mises à jour continues mais limitées.

Question. Quand utiliser debounce plutôt que throttle, et comment l’intégrer en toute sécurité dans React ?

Réponse. Le mécanisme de limitation des appels garantit une fréquence fixe — idéal pour le défilement et la modification de taille. Le mécanisme d’attente jusqu’à pause de frappe correspond à l’intention de recherche. Commencez par une valeur comprise entre un quart de seconde et trois cents millisecondes, puis ajustez-la en fonction des données mesurées. Associez ce mécanisme d’attente à une longueur minimale de requête ainsi qu’à l’annulation des demandes ; seul ce mécanisme n’arrange pas les réponses hors ordre.

function debounce(fn, wait = 250) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), wait);
  };
}

const search = debounce((q) => {
  if (q.length < 2) return;
  fetchResults(q);
});

Méthode incorrecte. Créer à chaque rendu un enveloppe avec ce mécanisme d’attente. Les temporiseurs se réinitialisent en fonction de nouvelles fonctions de clôture, ce qui empêche vraiment l’activation du mécanisme d’attente. Stabilisez la situation en utilisant useMemo/useRef et effacez les données lors du démontage.

Suivi. L’utilisateur tape rapidement, puis efface le champ. Une requête avec mécanisme d’attente est toujours envoyée avec la dernière valeur non vide. Comment interagissent l’annulation au démontage et à l’effacement ?

Mesuré. Le choix de l’outil est dicté par les objectifs d’expérience utilisateur, ainsi que par la facilité d’utilisation des fermetures qui restent valides quel que soit le rendu.

Vingt mille écouteurs d’événements

Situation. Une grille de données associe un événement onClick à chaque action de ligne. Avec 20 000 lignes, la page met plusieurs secondes à devenir interactive et la consommation mémoire augmente lors du pagination.

Question. Comment réorganiser le traitement des événements ?

Réponse. Utiliser la délégation d’événements. Un seul écouteur sur le conteneur lit l’élément cible au fur et à mesure que les événements remontent — une seule fonction en mémoire, sans réassociation lorsque les lignes changent, et cela fonctionne également pour les lignes ajoutées ultérieurement. Identifier la ligne et l’action à l’aide d’attributs de données et de la méthode closest, car les clics se produisent souvent sur une icône à l’intérieur du bouton plutôt que sur le bouton lui-même.

grid.addEventListener('click', (event) => {
  const button = event.target.closest('[data-action]');
  if (!button || !grid.contains(button)) return;

  const { action } = button.dataset;
  const rowId = button.closest('tr')?.dataset.rowId;
  handleAction(action, rowId);
});

Méthode erronée. On continue d’ajouter des milliers d’écouteurs de ligne en espérant qu’une boucle de nettoyage les supprimera. Le coût de configuration reste inchangé, et les références de fonctions incompatibles échouent silencieusement à être désactivées.

Suivi. Dans React 17+, où la bibliothèque enregistre-t-elle son écouteur racine, et comment les gestionnaires natifs délégués doivent-ils coexister avec le mécanisme de propagation des événements synthétiques ?

Évaluation pratique. Vérifier si le candidat considère le nombre d’écouteurs comme un véritable facteur de performance, et non simplement comme une question de style.

Une promesse qui s’est résolue n’est pas la même qu’une promesse qui reste valide.

Niveau intermédiaire — asynchrone, mémoire et performance

C’est ici que se décident la plupart des entretiens de niveau senior. Les questions ressemblent à des sessions de débogage, car c’est bien ce qu’il s’agit de faire.

Le tableau de bord qui se fige pendant deux secondes

Scénario. Changer la plage de dates fige l’onglet. Les clics n’ont aucun effet, les animations s’arrêtent, et l’indicateur de chargement ne tourne jamais. L’appel réseau lui-même prend 180 ms.

Question. Pourquoi l’indicateur de chargement ne s’anime-t-il pas, et où est passé ce temps ?

Réponse. Le thread principal gère en même temps le script, le layout, la mise à l’écran et les entrées utilisateur. Si l’on retient longtemps la pile d’appels, même un indicateur de chargement ne peut pas être affiché, car l’encadrement qui le montrerait ne s’exécute jamais. Les 180 ms correspondent au réseau ; le blocage provient du travail synchrone qui suit : l’analyse d’un gros en-tête JSON, suivie de l’exécution successive des fonctions map/filter sur des dizaines de milliers de lignes, chaque opération allouant un nouveau tableau.

Transférez les transformations lourdes dans un Web Worker, divisez le travail en parties et faites une pause entre elles, et demandez des données agrégées au backend lorsque c’est possible.

// Yield to the event loop between chunks so input and paint can run
async function processInChunks(items, fn, chunkSize = 500) {
  const out = [];
  for (let i = 0; i < items.length; i += chunkSize) {
    for (const item of items.slice(i, i + chunkSize)) out.push(fn(item));
    await new Promise((r) => setTimeout(r, 0));
  }
  return out;
}

Méthode incorrecte. Utiliser async/await au sein d’une boucle gourmande en ressources CPU tout en prétendant que le traitement est non bloquant. Aucun thread supplémentaire n’apparaît ; le travail synchrone bloque toujours l’onglet.

Suivi. Expliquer la différence entre les microtâches et les macrotâches dans ce cas de blocage, ainsi que le fait que l’utilisation de await Promise.resolve() ne supprime pas pour autant les périodes longues de traitement synchrone.

Mesuré. Comprendre la concurrence en JS comme une planification des tâches, et savoir quand des travailleurs sont nécessaires.

De l’IA

Résultats de recherche pour une requête déjà supprimée par l’utilisateur

Situation. En tapant « sam » puis en affinant la recherche à « samantha », des résultats pour « sam » apparaissent parfois après que des résultats pour « samantha » soient déjà affichés. C’est difficile à reproduire avec un lien rapide.

Question. Que se passe-t-il, et quelle est la correction appropriée ?

Réponse. Une course entre réponses hors ordre. Deux requêtes sont envoyées ; la plus lente correspond à la requête initiale ; celle qui se termine en dernier remporte la mise à jour d’état. Préférez les deux solutions de mitigation : annulez la requête précédente à l’aide de AbortController, et protégez l’écriture dans l’état afin que seule la réponse de l’entrée en cours puisse être enregistrée.

const controllerRef = useRef(null);

async function search(query) {
  controllerRef.current?.abort();
  const controller = new AbortController();
  controllerRef.current = controller;

  try {
    const res = await fetch(`/api/customers?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    });
    setResults(await res.json());
  } catch (err) {
    if (err.name !== 'AbortError') throw err;
  }
}

Méthode erronée. Allonger le délai de debounce jusqu’à une seconde complète avant de déclarer la victoire. Les courses deviennent plus rares, l’expérience utilisateur se détériore, et les réseaux lents continuent de reclasser les réponses.

Suivi. Dans quel cas un identifiant de requête en augmentation monotone serait-il plus efficace que AbortController pour éliminer les données de recherche obsolètes ?

Mesure. Nommer correctement ces courses et éviter d’inclure les signaux d’annulation dans les tableaux de bord d’erreurs.

Même requête, cinq fois

Scénario. Cinq composants consultent chacun /api/current-user au chargement. L’onglet Réseau affiche cinq appels identiques ; parfois, une réponse devient obsolète après une mise à jour de profil.

Question. Comment éliminer les demandes en cours sans réécrire toute la couche de données ?

Réponse. Mettez en cache la promise, et non le résultat. Identifiez chaque demande par une clé, renvoyez la même promise à tous les appels, et supprimez la clé une fois que la réponse est disponible afin de pouvoir réessayer en cas d’échec et récupérer de nouvelles données ultérieurement. Des bibliothèques comme TanStack Query et SWR ajoutent des règles d’invalidation et de détection de l’obsolété sur cette base.

const inFlight = new Map();

export function dedupedFetch(key, fetcher) {
  if (inFlight.has(key)) return inFlight.get(key);

  const promise = fetcher().finally(() => inFlight.delete(key));
  inFlight.set(key, promise);
  return promise;
}

// All five callers receive the same promise
const user = await dedupedFetch('/api/me', () => fetch('/api/me').then((r) => r.json()));

Méthode erronée. Conserver définitivement le payload traité dans une variable au niveau du module. Les appels GET redondants disparaissent, mais l’interface affiche alors les données de l’utilisateur d’hier jusqu’à ce que quelqu’un fasse un rechargement forcé.

Suivi. Si deux appels attachent des signaux d’annulation distincts à une promesse en cours d’exécution partagée, quelle politique d’annulation garantit la fiabilité des deux ?

Mesures à prendre. Partager judicieusement les promesses en cours d’exécution et planifier leur invalidation à l’avance.

Un fournisseur instable fait planter la page

Situation. Le profil, les informations de facturation et un widget de recommandations de tiers s’chargent en même temps. Le fournisseur est opérationnel à ~96 %. Lorsqu’il tombe en panne, toute la page affiche une erreur et les informations de facturation disparaissent.

Question. Comment la requête fetch devrait-elle être restructurée, et en quoi Promise.all et Promise.allSettled diffèrent-ils dans ce cas ?

Réponse. Promise.all rejette la promesse lorsqu’une des promesses d’entrée échoue, en ignorant celles qui ont réussi. Promise.allSettled, quant à lui, se résout toujours en indiquant l’état de chaque promesse — affichez ce qui a fonctionné et gérez les autres en conséquence. Gardez la facturation et le profil sur le chemin critique ; encadrez les recommandations dans un délai court afin qu’un fournisseur en phase d’exploration ne puisse pas bloquer le processus, même s’il renvoie ultérieurement un résultat positif.

const [profile, billing, recs] = await Promise.allSettled([
  getProfile(),
  getBilling(),
  withTimeout(getRecommendations(), 2000),
]);

if (profile.status === 'rejected' || billing.status === 'rejected') {
  return renderError();
}
render({
  profile: profile.value,
  billing: billing.value,
  recs: recs.status === 'fulfilled' ? recs.value : [],
});

Méthode incorrecte. Associer les échecs à null pour que Promise.all reste satisfait. La télémétrie perd alors la raison de l’échec, et tous les échecs semblent identiques.

Suivi. Choisissez entre Promise.any et Promise.race, en précisant ce qui arrive aux promesses qui ne remportent pas la victoire.

Mesuré. Maîtrise des combinateurs de promesses ainsi que compréhension du rayon d’impact potentiel.

Le paiement qui a été facturé deux fois

Situation. Le processus de paiement expire sur le passerelle. L’interface utilisateur tente à nouveau la transaction. Le client est donc facturé deux fois.

Question. Quelle stratégie de tentative est sûre pour un point d’entrée de paiement ?

Réponse. Les tentatives ne sont sûres que pour des opérations idempotentes. Une requête POST de création de facture n’est pas par défaut idempotente — il faut la rendre telle en utilisant une clé d’idempotence générée par le client que le serveur peut utiliser pour éviter les doublons. Ne tentez les opérations que en cas d’échecs de transmission ou de réponses 5xx/429 — jamais pour des réponses 4xx. Espacez les tentatives avec un retard exponentiel ainsi qu’un léger jitter afin d’éviter que le serveur ne soit submergé. Respectez les en-têtes Retry-After et fixez un plafond au nombre de tentatives.

async function postWithRetry(url, body, key, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status < 500 && res.status !== 429) throw new HttpError(res);

    const backoff = 2 ** i * 300 + Math.random() * 300; // jitter
    await new Promise((r) => setTimeout(r, backoff));
  }
  throw new Error('Payment could not be confirmed');
}

Méthode erronée. Tenter aveuglément tout trois fois selon un délai fixe. Les temps d’attente peuvent entraîner des facturations doubles, les boucles 4xx ne mènent jamais à rien, et les pannes s’aggravent jusqu’à provoquer une surcharge du système.

Suivi. Le navigateur a atteint son délai d’attente, mais le paiement a été finalisé du côté serveur — décrivez la procédure de récupération visible par l’utilisateur.

Mesuré. Conception idempotente en cas d’incertitude du côté client.

L’API qui bloque plutôt que de planter

Scénario. Un fournisseur cesse de répondre sans fermer les connexions. fetch ne se termine jamais ; les gestionnaires s’accumulent ; les indicateurs de chargement tournent pendant des minutes.

Question. Comment encadrer cela, et qu’y a-t-il au-delà d’un délai d’attente ?

Réponse. Les navigateurs ne fournissent aucune durée d’attente par défaut pour fetch — il faut en définir une. AbortSignal.timeout représente l’approche moderne ; AbortSignal.any combine la durée d’attente avec l’annulation par l’utilisateur. Ajoutez un mécanisme de protection : après des échecs successifs, cessez d’appeler pendant une période de refroidissement et servez immédiatement une solution de secours, afin de protéger à la fois l’application et la dépendance en cours de récupération.

const signal = AbortSignal.any([
  AbortSignal.timeout(3000),
  userController.signal,
]);

try {
  const res = await fetch(url, { signal });
  breaker.recordSuccess();
  return res.json();
} catch (err) {
  breaker.recordFailure();
  if (err.name === 'TimeoutError') return cachedFallback();
  throw err;
}

Méthode incorrecte. Utiliser un chronomètre en même temps que fetch et faire semblant que le perdant a arrêté. L’appel HTTP continue de fonctionner, bloque les sockets et peut encore modifier l’état une fois terminé.

Suivi. Définissez des durées d’attente tant au niveau du navigateur que du gateway API et des appels Node vers le même fournisseur, sans laisser de tâches orphelines.

Mesure. Méfiance par défaut envers les dépendances et annulation réelle, contre des tâches orphelines.

La mémoire augmente à chaque changement de route

Scénario. Un outil de support laissé ouvert toute la journée atteint 1,4 Go. Les captures d’heap montrent que les nœuds DOM détachés augmentent en nombre à chaque navigation entre tickets.

Question. Quelles en sont les causes habituelles, et comment les confirmer ?

Réponse. Les nœuds détachés restent actifs parce qu’un élément les référence encore : des écouteurs window/document jamais supprimés, des fonctions setInterval toujours en exécution, des objets IntersectionObserver/ResizeObserver jamais désconnectés, des écouteurs dans le stockage global, ainsi que des closures dans des caches à longue durée de vie qui conservent une référence au DOM. Pour confirmer, capturez une image de l’heap, naviguez, forcez la collecte des déchets, capturez à nouveau, puis examinez les références jusqu’à identifier clairement le détenteur.

useEffect(() => {
  const onResize = () => recalcLayout();
  const observer = new ResizeObserver(onResize);
  const id = setInterval(pollTicket, 5000);

  window.addEventListener('resize', onResize);
  observer.observe(panelRef.current);

  return () => {
    window.removeEventListener('resize', onResize);
    observer.disconnect();
    clearInterval(id);
  };
}, []);

Méthode erronée. Mettre à zéro les variables locales lors du nettoyage en espérant que la mémoire diminue. La faisabilité d’accès persiste grâce aux écouteurs en cours d’exécution et aux temporiseurs qui s’appuient sur les mêmes données.

Suivi. On peut qualifier un modèle de fuite où WeakMap résout proprement le problème, ainsi qu’une situation où les références faibles ne sont pas l’outil adapté.

Mesuré. Débogage pratique du heap et maîtrise de la faisabilité d’accès.

Le compteur qui affiche toujours zéro

Scénario. Un widget effectue des vérifications toutes les cinq secondes et ajoute des alertes. Il affiche toujours une seule alerte ; un journalage à l’intérieur de cet intervalle affiche indéfiniment l’état initial.

Question. Pourquoi l’intervalle voit-il un état obsolète, et comment le corriger ?

Réponse. L’effet a été exécuté une fois avec [], de sorte que la fonction de rappel conservait l’état de la première rendu. Les mises à jour créent de nouvelles valeurs ; l’ancienne fonction de rappel continue de faire référence à l’ancienne valeur. Préférez des mises à jour fonctionnelles afin que React fournisse la dernière valeur en file d’attente. Lorsque la fonction de rappel a besoin de données que l’updater ne peut pas exprimer, reflétez-les dans un ref à chaque rendu.

// Broken: `alerts` is frozen at the first render
useEffect(() => {
  const id = setInterval(() => setAlerts([...alerts, poll()]), 5000);
  return () => clearInterval(id);
}, []);

// Fixed: functional update, no stale capture
useEffect(() => {
  const id = setInterval(() => setAlerts((prev) => [...prev, poll()]), 5000);
  return () => clearInterval(id);
}, []);

Approche incorrecte. Indiquer alerts comme dépendance de l’effet. Les fonctions de rappel obsolètes disparaissent, mais l’intervalle se réinitialise à chaque ajout, ce qui fait échouer le rythme toutes les cinq secondes.

Suivi. Comment un hook personnalisé pourrait-il maintenir un rythme de vérification toutes les cinq secondes tout en lisant toujours l’état le plus récent ?

Mesuré. Fonctions de rappel qui « voyagent dans le temps » — une erreur classique à un niveau intermédiaire en React.

Vingt mille lignes dans le DOM

Scénario. Un tableau d’inventaire affiche chaque enregistrement API. La mise en page prend six secondes ; le filtrage entraîne des retards ; l’onglet consomme des centaines de mégaoctets.

Question. Comment rendre le tableau utilisable, et qu’est-ce qui doit être mesuré en premier ?

Réponse. Analysez d’abord le profil : script, style, mise en page et affichage. Pour des tableaux de cette taille, le nombre de nœuds DOM est généralement déterminant. Virtualisez la liste afin que seuls le champ de vision ainsi qu’un petit buffer d’overscan existent dans le DOM. Associez cela à des identifiants de ligne stables, une mémorisation rigoureuse, et un filtrage/sort côté serveur lorsque les ensembles de données côté client deviennent trop volumineux.

// Row identity matters as much as row count
{visibleRows.map((row) => (
  <Row key={row.id} data={row} />   // stable id, not the array index
))}

Méthode erronée. Envelopper toutes les lignes avec React.memo et s’arrêter là. Des propriétés inline fraîches annulent l’effet du memo, et la mise en page/affichage restent submergées par des dizaines de milliers de nœuds.

Suivi. Que se passe-t-il avec le ciblage et les entrées contrôlées lorsque les clés de ligne sont des indices d’array et qu’une ligne intermédiaire est supprimée ?

Mesurements. Habitudes de mesure en premier et limites réalistes de la mémorisation.

De l’IA

Désordre de mise en page dû aux boucles de filtrage/redimensionnement

Situation. Après le chargement des données, un tableau de bord redimensionne les conteneurs de graphiques. Les profils affichent de longs blocs de style/mise en page violets se répétant des centaines de fois dans une même fenêtre.

Question. Quel schéma en est la cause, et comment le corriger ?

Réponse. Problèmes de mise en page. L’accès aux API géométriques telles que les offsets de hauteur, les rectangles délimitants ou les positions de défilement force un nettoyage synchronisé des styles et de la mise en page afin que le moteur puisse fournir des résultats précis. Alterner ces lectures avec des écritures de styles à l’intérieur d’une boucle entraîne ce nettoyage à chaque itération. Collectez d’abord les mesures, modifiez ensuite les styles, et planifiez les écritures à l’aide de requestAnimationFrame. Pour la logique d’affichage/dissimulation, utilisez IntersectionObserver afin que la visibilité s’applique de manière asynchrone sans forcer une mise à jour de la mise en page.

// Broken: read, write, read, write
panels.forEach((p) => { p.style.height = p.offsetHeight * 1.2 + 'px'; });

// Fixed: batch reads, then batch writes
const heights = panels.map((p) => p.offsetHeight);
requestAnimationFrame(() => {
  panels.forEach((p, i) => { p.style.height = heights[i] * 1.2 + 'px'; });
});

Méthode incorrecte. Planifier chaque écriture dans sa propre frame d’animation tout en continuant à mélanger les lectures. Le coût est réparti sur plusieurs frames, ce qui prolonge les ralentissements.

Suivi. Quelles propriétés CSS restent gérées par le composant, et à quel moment will-change génère plus de coûts qu’il n’en économise ?

Mesuré. Les coûts du pipeline du navigateur sont présentés sous forme de séquence, et non comme une boîte noire.

En attente d’un calcul synchrone, la boucle d’événements n’est pas libérée.

Niveau avancé — sécurité, Node, tests et conception

Les décisions au niveau du personnel se concentrent moins sur la solution unique parfaite et plus sur les compromis, l’impact potentiel et la responsabilité associée.

Le champ du CMS qui a exécuté un script

Situation. Le service marketing stocke du texte enrichi dans un CMS sans interface. Une revue de sécurité révèle que <img onerror=...> est exécuté sur chaque page de produit.

Question. Comment cela a-t-il pu se produire, et comment le corriger dans l’ensemble de la stack ?

Réponse. La valeur est stockée dans innerHTML ou dans la fonction React dangerouslySetInnerHTML sans avoir été nettoyée. Par défaut, les caractères dangereux sont échappés ({value} dans React, textContent dans le DOM). Lorsqu’un HTML est nécessaire, il faut le nettoyer à l’aide d’une bibliothèque gérée qui utilise une liste d’éléments autorisés, comme DOMPurify — également sur le serveur, car le client ne constitue pas une frontière de confiance. Il convient d’ajouter une politique de sécurité du contenu afin qu’un payload non détecté ne puisse pas exécuter de script en ligne.

import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(cmsHtml, {
  ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'li'],
  ALLOWED_ATTR: ['href', 'title'],
});
<div dangerouslySetInnerHTML={{ __html: clean }} />

Méthode incorrecte. Suppression des balises <script> à l’aide de réguliers. Les attributs de gestion d’événements, les URLs javascript:, les vecteurs SVG ainsi que les encodages imbriqués échappent à ce traitement.

Suivi. Les outils d’analyse injectent des scripts en ligne et la politique CSP les bloque — il faut réintroduire ces balises sans activer l’option unsafe-inline.

Mesuré. Défenses multicouches contre XSS avec sanitisation au moment de l’affichage.

Le jeton dans localStorage

Situation. Après une attaque XSS, les équipes de sécurité notent les jetons de session dans localStorage, ajoutés par un intercepteur Axios. Elles cherchent une solution.

Question. Quels sont les compromis entre localStorage et les cookies pour les jetons d’authentification, et quelle est la recommandation ?

Réponse. Les scripts sur l’origine peuvent lire localStorage, ce qui permet à une seule attaque XSS d’exporter toute la session. Les cookies HttpOnly Secure avec SameSite=Lax restent invisibles pour JavaScript, mais les navigateurs les ajoutent automatiquement, de sorte que les requêtes modifiées nécessitent toujours des protections CSRF. Une approche courante consiste à utiliser un jeton d’accès à durée de vie courte en mémoire, un jeton de renouvellement dans un cookie HttpOnly, des tokens CSRF ou l’attribut SameSite pour les modifications, ainsi qu’un renouvellement périodique des jetons. Rien n’échappe intact à une attaque XSS — la prévention des attaques XSS reste primordiale.

// Server side, Express
res.cookie('refresh_token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  path: '/auth/refresh',
  maxAge: 1000 * 60 * 60 * 24 * 7,
});

Méthode erronée. Chiffrer les tokens dans localStorage avec une clé qui se trouve également dans le JavaScript de la page. Quiconque peut lire ce stockage peut aussi lire la clé — c’est purement symbolique.

Suivi de la question. Si les jetons d’accès ne restent que en mémoire, comment une nouvelle fenêtre ouverte peut-elle obtenir une session ?

Mesuré. Des choix de stockage des jetons basés sur la modélisation des menaces plutôt que sur des slogans.

L’endpoint Node qui ralentit tous les autres endpoints

Scénario. Un gestionnaire de rapports PDF d’Express, lorsqu’il est appelé, provoque des temps d’attente excessifs pour des vérifications de santé non liées, ce qui entraîne une augmentation du p99 dans l’ensemble du service.

Question. Pourquoi un endpoint affecte-t-il tous les autres, et comment y remédier ?

Réponse. Node exécute le JavaScript d’application sur un seul thread. Les tâches gourmandes en CPU bloquent la boucle d’événements, empêchant les autres requêtes, compteurs temporels ou appels de retour d’entrée/sortie. Transférez les tâches gourmandes en CPU vers worker_threads ou une file d’attente externe afin que le gestionnaire HTTP se contente d’enregistrer les requêtes et de répondre. Transmettez des charges utiles volumineuses sous forme de flux plutôt que de les buffer. Préférez les API cryptographiques asynchrones aux versions Sync ; évitez absolument d’utiliser readFileSync sur un chemin de requête.

import { Worker } from 'node:worker_threads';

app.post('/reports', async (req, res) => {
  const job = await queue.add('generate-report', req.body); // returns immediately
  res.status(202).json({ jobId: job.id, status: 'queued' });
});

// Or for in process CPU work
const worker = new Worker('./report-worker.js', { workerData: params });

Méthode erronée. On attend que le travail gourmand en CPU soit terminé en espérant que la boucle d’événements puisse fonctionner. L’approche de scaling-out multiplie les instances facturables qui restent toutes bloquées sur le même type de requête.

Suivi. Quels signaux en environnement de production révèlent un blocage de la boucle d’événements Node avant que les clients ne se plaignent ?

Mesuré. Séparation du travail gourmand en E/S de celui gourmand en CPU, avec des signaux en production correspondants.

Affichage incorrect du contenu au chargement (hydration)

Scénario. Une application Next.js affiche « Se connecter » sur le serveur, puis passe au nom de l’utilisateur après chargement. Avertissements de désaccord lors de l’hydratation ; parfois Le contenu texte ne correspond pas à l’HTML généré sur le serveur.

Question. Qu’est-ce qui provoque ces désaccords d’hydratation, et comment corriger ce problème ?

Réponse. Le serveur ne dispose pas des éléments d’état réservés au navigateur : les cookies lus par le client, localStorage, window.matchMedia, Date.now(). L’hydratation exige que la première rendu du client corresponde à l’arborescence serveur. En cas de différence, React élimine le markup serveur correspondant à cette sous-arborescence et émet une alerte. Pour afficher les données de session lors du rendu serveur, il faut lire les cookies sur le serveur et transmettre cette valeur dans l’arborescence. Pour les valeurs qui n’existent réellement que dans le navigateur, il convient d’afficher un placeholder stable et de le mettre à jour après le montage.

// Server component reads the session, so no mismatch
export default async function Layout({ children }) {
  const session = await getSession(cookies());
  return <AuthProvider value={session}>{children}</AuthProvider>;
}

Méthode incorrecte. Silencer les avertissements d’hydratation via un conteneur, ou dynamiser l’en-tête pour contourner le SSR. La différence persiste ; les avantages du SSR disparaissent.

Suivi de la question. Comment les timestamps relatifs peuvent-ils être affichés sur le serveur sans que l’hydratation ne contredise l’horloge du client ?

Mesuré. Corriger correctement le problème d’hydratation plutôt que de masquer les avertissements.

Le cache qui ne lâche jamais prise

Situation. Une bibliothèque de création de graphiques met en cache des tableaux graphiques identifiés par des éléments DOM. La mémoire augmente même après que les graphiques aient quitté la page. Les captures d’écran montrent des buffers conservés par un Map.

Question. Pourquoi les entrées ne sont-elles pas collectées, et comment WeakMap change-t-il le résultat ?

Réponse. Le collecteur de déchets accède aux éléments depuis les racines. Les entrées d’un Map ordinaire retiennent à la fois la clé et la valeur, de sorte qu’un nœud DOM détaché utilisé comme clé maintient son buffer de tableau graphique accessible. Un WeakMap retient les clés de manière faible — lorsque plus rien ne fait référence à l’élément, l’entrée peut disparaître avec lui. WeakMap n’est pas énumérable et n’a pas de taille ; cet équilibre rend sa faiblesse sûre.

// Leaks: the Map keeps removed nodes alive
const cache = new Map();

// Collectable: entry dies with the element
const cache = new WeakMap();
cache.set(chartEl, renderedCanvas);

Méthode incorrecte. Des nettoyages similaires à Cron qui suppriment les entrées de cache dont les nœuds ont quitté le document. Cela fonctionne tant que l’opération de nettoyage n’est pas oubliée ; WeakMap s’occupe de cette tâche.

Suivi. Quand est-ce que FinalizationRegistry est approprié, et pourquoi la correction ne doit-elle jamais dépendre du moment où il s’exécute ?

Mesuré. Le nettoyage automatique basé sur la reachabilité, sans prétendre que le temps de collecte est contrôlable.

Le test qui échoue une fois tous les vingt exécutions

Scénario. Un test de composant de recherche passe localement mais échoue à environ 5 % dans l’environnement CI lors de la recherche du texte « Samantha ». Quelqu’un a déjà ajouté waitFor(3000) et l’environnement CI réessaie.

Question. Comment rendre le test fiable, et que vous indique cette instabilité ?

Réponse. Les tests asynchrones instables testent généralement le temps d’exécution plutôt que le comportement. Utilisez des temporisateurs fictifs pour gérer l’effet de débouncing, substituez les requêtes HTTP par des simulations comme MSW afin d’obtenir des données stables, et effectuez des vérifications en attendant les mises à jour du DOM au lieu de dormir pendant un nombre fixe de millisecondes. Si un temps d’attente arbitraire est nécessaire pour que le test passe, cela indique souvent une véritable concurrence dans le composant — le test fonctionne correctement.

test('shows results for the final query', async () => {
  vi.useFakeTimers();
  render(<Search />);

await userEvent.type(screen.getByRole('searchbox'), 'samantha');
  await vi.advanceTimersByTimeAsync(300); // debounce window
  expect(await screen.findByText('Samantha')).toBeInTheDocument();
});

Méthode erronée. Des tentatives répétées dans l’environnement CI avec des délais d’attente de plus en plus longs. Le signal devient du bruit, et un ensemble de tests réessayé trois fois peut masquer une concurrence qui se manifestera plus tard en production.

Suivi. Comment un test pourrait-il forcer la réponse de recherche ancienne à arriver après la nouvelle ?

Mesuré. Interpréter l’instabilité comme un signal et rendre les tests asynchrones déterministes.

Sixty places that call fetch

Scénario. Une base de code vieille de quatre ans utilise fetch dans soixante composants. La gestion des erreurs est incohérente ; le renouvellement de l’authentification a été copié-collé onze fois ; personne ne connaît le nombre de délais d’expiration quotidiens.

Question. Comment concevoir une couche d’accès aux données partagée, et comment migrer sans interruption ?

Réponse. Rassembler les fonctionnalités partagées dans un seul client : URL de base et en-têtes, renouvellement d’authentification unique pour que toute série d’erreurs 401 ne provoque qu’un seul renouvellement, délais d’expiration, tentatives de réessai uniquement idempotentes, normalisation des erreurs avec types définis, ainsi que des points d’ancrage pour la télémétrie. Limiter l’interface afin que son adoption prime sur les contournements. Migrer de manière progressive — déployer le client, commencer par les chemins les plus fréquentés et ceux les plus sujets aux erreurs, interdire l’utilisation de fetch brut dans le nouveau code grâce à des outils de vérification, afin que la frontière ne bouge plus.

type ApiError =
  | { kind: 'network' }
  | { kind: 'timeout' }
  | { kind: 'http'; status: number; body: unknown }
  | { kind: 'parse' };

export async function apiRequest<T>(
  path: string,
  init: RequestInit & { timeoutMs?: number } = {},
): Promise<{ ok: true; data: T } | { ok: false; error: ApiError }> {
  // timeout, auth, retry, telemetry all live here
}

Méthode erronée. Proposer une migration complète vers un nouveau cadre, ou envelopper les réponses de manière si stricte que des cas particuliers aboutissent à l’utilisation directe de fetch. Dans les deux cas, on se retrouve avec deux couches de données concurrentes.

Suivi. Six mois plus tard, comment les règles de vérification et les revues de code empêchent-elles l’utilisation récurrente de fetch ?

Mesuré. Conception d’API à l’échelle de l’équipe et migration progressive sous pression de livraison.

Conclusion — se préparer sans mémoriser

La préparation basée sur des faits anecdotiques a ses limites : tables de contraintes mémorisées, tableaux de bord figés sans explication. La préparation basée sur des scénarios présente un autre mode d’échec : maîtriser le récit sans comprendre le mécanisme. Évitez les deux en travaillant à partir de code réel.

Reproduisez délibérément les pannes. Envoyez un champ de recherche qui rencontre des problèmes sous contrainte réseau, puis ouvrez les onglets Performance et Réseau jusqu’à ce que les effets de rafraîchissement différé deviennent évidents. Laissez un intervalle non défini, naviguez ailleurs, puis recherchez l’arbre séparé dans la section Mémoire. Les méthodes pratiques via les DevTools restent plus utiles que n’importe quel guide théorique.

Apprenez à mesurer correctement avant de chercher des réponses prêtes. Les panneaux Performance, Mémoire et Réseau de Chrome, ainsi que le React Profiler, transforment de nombreuses questions « avancées » en simples problèmes d’instrumentation.

Entraînez-vous à nommer oralement les compromis. Presque chaque scénario permet plusieurs solutions plausibles, chacune ayant des coûts différents. Les interviewers compétents remarquent si le choix a été fait délibérément.

Lisez des rapports d’incidents en production. Toute personne ayant déjà mis un produit sur le marché connaît au moins cinq scénarios de ce type. Ces expériences personnelles valent mieux que celles empruntées, car les analyses peuvent aller indéfiniment en profondeur.

Gardez une courte liste des pannes avec leurs causes et solutions — des notes, pas un portfolio. Lorsqu’on vous demande de décrire un bug difficile, une diagnosis précise vaut toujours mieux qu’un exposé général.

Que l’on soit débutant ou avancé, le schéma est toujours le même : identifier le mode de panne, proposer une solution adaptée au problème, et savoir ce qui pourrait faire paraître une mauvaise solution attrayante sous pression. C’est cette compétence que recherchent réellement les entretiens d’embauche — pas une table de contraintes parfaite, mais la capacité à maintenir l’honnêteté des systèmes lorsque les réseaux mentent, la mémoire diminue et que les fournisseurs manquent à leurs engagements.

Les équipes qui mènent des entretiens de cette manière ont également tendance à effectuer de meilleures analyses a posteriori : le même vocabulaire (courses, surcharge, idempotence, rayon d’impact) apparaît à la fois lors des processus de recrutement et lors des examens d’incidents. Étudier ces scénarios rapporte donc doublement — une fois dans la salle d’entretien, et à nouveau lorsque le tableau de bord se fige pendant deux secondes tandis que l’indicateur tourne sans bouger.

Les processus d’entretien ancrés dans des situations réelles de production révèlent également les compétences en communication. Raconter une situation de surcharge sans se noyer dans le jargon, dessiner rapidement un diagramme de séquence pour AbortController, ou expliquer pourquoi allSettled réduit le rayon d’impact montrent comment une personne se comportera dans un canal dédié aux incidents. Les réponses à des questions de culture générale montrent rarement cette capacité ; en revanche, les réponses à des scénarios le font presque toujours, c’est pourquoi ce format continue de se répandre dans les équipes de recrutement intermédiaires et senior.