Interface modale Cmd+K : listes de résultats, indices clavier et états vides (approche HTML/CSS d’abord)

Cet article vous a aidé ?

Si votre palette de recherche Cmd+K semble « correcte » sur un ordinateur rapide et « mystérieusement maudite » partout ailleurs, vous n’imaginez pas. Les modales de recherche sont une drôle d’intersection entre finition UI, règles d’accessibilité et latence en production : vous pouvez livrer une belle liste qui s’effondre dès que le jeu de données grandit ou que le focus clavier part en vrille.

Voici le guide de terrain pour construire une modale Cmd+K qui se comporte comme un bon ingénieur d’astreinte : prévisible sous tension, claire quand elle ne peut pas aider, et ne piégeant jamais l’utilisateur sans touche Échap.

Audience ingénieurs frontend, designers qui déploient, et toute personne ayant déjà dit « c’est juste une modale » puis regretté.

Périmètre motifs UI axés HTML/CSS pour listes de résultats, indices clavier et états vides, plus diagnostics opérationnels et modes de défaillance.

À quoi ressemble le « bon » dans une modale Cmd+K

Une modale de recherche Cmd+K n’est pas une fonctionnalité ludique. Elle devient la porte d’entrée cachée vers votre produit. Quand elle est bonne, les utilisateurs développent un réflexe et cessent de naviguer dans l’interface. Quand elle est mauvaise, ils perdent confiance — puis cessent de l’utiliser. Et une fois que les gens cessent d’utiliser la recherche, votre architecture d’information soigneusement organisée devient la seule issue, ce qui est… ambitieux.

Comportements non négociables

  • Rétroaction instantanée à chaque frappe. Si un chargement est nécessaire, affichez l’état de chargement. Ne figez pas l’UI.
  • Priorité clavier : / déplace la sélection ; Enter active ; Esc ferme ; Tab n’envoie pas le focus voler dans le vide.
  • Classement prévisible : la même requête produit les mêmes résultats en tête, sauf si les données sous-jacentes ont changé.
  • Sémantique accessible : les lecteurs d’écran doivent obtenir une histoire cohérente : « Recherche, 12 résultats, résultat sélectionné X. »
  • État vide clair qui indique à l’utilisateur quoi faire ensuite, pas ce qu’il a mal fait.

Comment ça casse en production

  • Fuites de focus : la modale s’ouvre mais le focus reste derrière. Les utilisateurs clavier tapent dans ce qui était précédemment focalisé.
  • Prison de défilement : la page derrière la modale défile encore ; la liste de résultats non ; quelqu’un ouvre un ticket « la recherche est cassée ».
  • Mensonges de latence : l’UI affiche « Aucun résultat » avant le retour réseau ; puis les résultats apparaissent. Les utilisateurs apprennent à l’ignorer.
  • Tempêtes d’événements : chaque frappe déclenche une recherche back-end ; votre API devient un keylogger involontaire facturable.
  • Incohérence entre indice et réalité : le pied de page dit Enter pour ouvrir, mais Enter soumet un formulaire et ferme la modale.

« L’espoir n’est pas une stratégie. »

— General Gordon R. Sullivan

Une vérité sèche : une modale de recherche est comme un pager. Elle est silencieuse quand tout va bien, et quand elle est nécessaire, elle est nécessaire maintenant. Construisez-la comme si vous deviez la déboguer à 2 h du matin, un œil ouvert.

Faits et contexte historique à réutiliser pour les décisions

Voici des éléments concrets d’histoire et d’évolution industrielle qui expliquent pourquoi les utilisateurs attendent que Cmd+K fonctionne d’une certaine manière. Ce ne sont pas des anecdotes. Ce sont des contraintes déguisées en faits amusants.

  1. Les palettes de commandes ne sont pas nées sur le web. Les éditeurs pour power users (notamment les IDE) ont normalisé le « taper pour rechercher des commandes » bien avant les navigateurs, donc les utilisateurs arrivent avec de fortes attentes sur le comportement clavier.
  2. Spotlight a popularisé « recherche comme lanceur ». La recherche au niveau système a appris aux gens que rechercher ne se limite pas à trouver des documents ; c’est un sélecteur d’action universel.
  3. Cmd+K est devenu une convention de facto sur le web moderne parce que c’est mémorisable, ne conflit pas avec « Rechercher dans la page » (Cmd+F), et les plateformes avaient besoin d’un raccourci commun.
  4. Les modèles WAI-ARIA pour combobox/listbox ont évolué lentement car le « typeahead + liste » accessible est étonnamment subtil ; beaucoup de premiers modèles ont cassé les lecteurs d’écran ou la navigation clavier.
  5. Les boîtes de dialogue modales ont une longue histoire de bugs de focus car le piégeage du focus n’est pas un primitif natif du navigateur ; on approximativement reconstitue un gestionnaire de fenêtres de bureau dans une page.
  6. Les attentes d’« instantanéité » suivent le progrès matériel. À mesure que les appareils sont devenus plus rapides, la tolérance à « attendre après chaque frappe » a disparu ; les normes UX se sont resserrées.
  7. Les CDN ont rendu la livraison d’actifs bon marché, pas la livraison d’état. Livrer un gros index au client peut sembler « rapide » en local, puis faire fondre les appareils bas de gamme à cause de la mémoire.
  8. Le mobile a changé la signification des indices clavier. Un pied de page plein de keycaps est du bruit sur les appareils tactiles ; les indices doivent s’adapter.

Usage décisionnel : traitez Cmd+K comme une fonction réflexe. Votre KPI principal n’est pas « temps d’implémentation », c’est « temps de réussite par requête sous contrainte ».

Anatomie : saisie, liste de résultats, pied de page indicatif et états

Une modale Cmd+K est une petite UI, mais elle a des sous-systèmes distincts. Si vous ne les nommez pas, vous les déboguerez comme un blob. Donnez-leur un nom. Ça aide.

Sous-système Mission Mode de défaillance Philosophie de correction
Déclencheur Ouvrir de manière fiable depuis n’importe où Conflits de raccourcis, bloqué dans des champs Respecter les conventions plateforme ; ne pas détourner les champs éditables
Coquille de la boîte Piéger le focus ; empêcher l’interaction en arrière-plan Fuite de focus, défilement en arrière-plan Utiliser des sémantiques correctes ; verrouiller le défilement
Champ de recherche Capturer la requête ; afficher le chargement ; effacer Bugs IME, mauvais débounce Gérer la composition ; ne pas sur-débouncer
Liste de résultats Afficher ; permettre la sélection ; activer Défilement saccadé, mauvaise sélection DOM simple, clés stables, surlignage prévisible
Pied de page d’indices Enseigner l’interaction ; montrer la portée Indices mensongers ou surchargés Montrer seulement ce qui est vrai ; s’adapter au dispositif
États Vide, erreur, chargement, hors ligne « Rien ne se passe » ambigu Communiquer systématiquement l’action suivante

La plupart des palettes sont 70 % comportement de la liste, 20 % sémantique de focus, 10 % tout le reste. La liste est l’endroit où les rêves UX vont mourir — car c’est là que se rencontrent latence, classement, accessibilité et impatience humaine.

Structure HTML/CSS d’abord (amélioration progressive)

HTML/CSS-first ne veut pas dire « pas de JavaScript ». Cela signifie que le balisage exprime l’intention, que les états sont visibles et que le JS améliore le comportement plutôt que de l’inventer à partir de zéro. En termes de fiabilité : vous voulez une dégradation élégante et des états observables.

Balisage de base : dialog + input + list + footer

Utilisez une vraie boîte de dialogue si vous le pouvez, mais traitez-la comme un primitif UI, pas un sortilège magique. Vous aurez toujours besoin de gestion du focus et de verrouillage du défilement autour. Votre HTML doit rester compréhensible si la logique de sélection échoue.

Démo : liste de résultats avec indices clavier

Runbook : latence API
Opérations · Mis à jour il y a 2 jours
Enter
Tableau de bord : taux de hit du cache
Observabilité · En direct
Enter
Service : billing-worker
Services · Dégradé précédemment
Enter

Ceci est « HTML-first » dans l’esprit : structure de liste lisible, sélection visible, et indices qui reflètent le comportement.

Démo : état vide qui propose une action suivante

Aucun résultat pour « kafak ».

  • Essayez kafka ou queue.
  • Utilisez des préfixes / comme /runbook pour restreindre la portée.
  • Si vous attendiez un service, il peut être masqué par les permissions.

Les états vides doivent réduire l’incertitude : « est-ce moi, le système, ou le contrôle d’accès ? »

L’état vide est une surface produit. Traitez-le comme tel.

Sémantiques d’accessibilité à ne pas improviser

Choisissez un modèle ARIA connu et suivez-le. Pour un « typeahead + liste de résultats », vous finirez généralement avec un modèle proche du combobox ou un modèle plus simple textbox + listbox avec active descendant. Le modèle dépend si les résultats sont des « suggestions » ou une liste distincte. Quoi que vous fassiez, assurez-vous que les lecteurs d’écran puissent annoncer :

  • Où se trouve le focus (champ de saisie vs liste)
  • Combien de résultats existent
  • Quel élément est sélectionné

Opinion : si votre application a déjà une infrastructure a11y mature, implémentez le modèle active-descendant complet. Sinon, restez simple mais correct : ne mettez pas en production une demi-combobox.

Une blague, courte et pratique : une modale sans gestion du focus est comme un centre de données sans portes — techniquement « ouvert », opérationnellement terrifiant.

Liste de résultats qui survit à la réalité

Les listes de résultats dans les palettes de commande ont tendance à être construites comme un fil d’actualité : beaucoup d’éléments imbriqués, icônes, métadonnées, tags, surlignage, boutons, menus hover. Puis quelqu’un demande pourquoi la navigation fléchée saccade. Parce que vous avez construit une cathédrale DOM et que vous lui demandez de se recalculer 60 fois par seconde.

Règles pour une liste qui reste rapide

  • Gardez le DOM de la ligne peu profond. Une ligne devrait être : titre, extrait optionnel, indice aligné à droite. Pas un mini-framework UI imbriqué.
  • Identité stable. Ne keyez pas les résultats par index. Utilisez un identifiant stable ou un chemin d’URL. Sinon la sélection saute quand les résultats changent.
  • La sélection est un état, pas un effet hover. Utilisez aria-selected et un style visible qui fonctionne sans hover.
  • Faites défiler la liste, pas la page. Donnez au conteneur de résultats une hauteur max et overflow:auto.
  • Ne surlignez pas en reconstruisant innerHTML. Utilisez une fonction de rendu qui sépare le texte en toute sécurité, ou pré-calculer les plages de surlignage. innerHTML est l’endroit où XSS et problèmes de performance se serrent la main.

Comportement clavier : choisissez un modèle

Il y a deux modèles courants. Choisissez explicitement :

  • Le focus reste dans l’entrée, et les flèches changent l’« active descendant » dans la liste. Cela garde la frappe stable et facilite IME/composition. C’est aussi plus complexe sur le plan accessibilité.
  • Le focus se déplace dans la liste au premier appui sur la flèche bas, et revient à l’entrée lors de la frappe. Sémantique plus simple, mais vous devez vous assurer que la frappe n’est pas absorbée et que la restauration du focus est correcte.

Du point de vue de la fiabilité, « le focus reste dans l’entrée » conduit généralement à moins de bugs du type « je ne peux plus taper ». C’est plus difficile à implémenter correctement, mais ça échoue moins catastrophiquement.

Classement + groupement sans confondre l’utilisateur

Le groupement est utile : éléments récents, meilleures correspondances, commandes vs documents. Mais il peut aussi rendre la navigation clavier cassée si la sélection saute par-dessus des en-têtes de groupe. Si vous montrez des en-têtes de groupe, rendez-les non sélectionnables et visuellement discrets.

De plus : gardez le classement cohérent. Si vous mélangez « récent » et « meilleur résultat », étiquetez-le. Les gens pardonneront un classement étrange s’il est expliqué. Ils ne pardonneront pas une liste qui change d’ordre en cours de frappe sans explication.

Indices clavier : les afficher sans crier

Les indices clavier sont de la documentation UI que vous livrez en production. Cela signifie qu’ils doivent être :

  • Vrais (ils correspondent au comportement réel)
  • Contextuels (n’affichez pas « Enter pour ouvrir » s’il n’y a rien de sélectionné)
  • Adaptatifs (n’imposez pas l’iconographie des touches sur les appareils tactiles ; n’affichez pas Cmd sur Windows)

Rendu des keycaps qui n’a pas l’air d’une note de rançon

Utilisez du CSS simple pour les keycaps. Évitez le SVG inline par touche. Vous alourdirez le DOM et rendrez le theming difficile. Gardez les composants keycap cohérents : bordure, fond, et légère ombre intérieure. C’est petit, mais ça donne à la palette un aspect « natif ».

Indices comme machine à états

Les indices doivent refléter l’état :

  • Inactif (pas encore de requête) : montrer des exemples (/ pour la portée, ou « Tapez pour chercher… »)
  • En recherche : afficher « Recherche… » et Esc pour fermer
  • Résultats disponibles : afficher navigation + touches d’action
  • Vide : montrer comment élargir la requête, et éventuellement un fallback « rechercher partout »
  • Erreur/hors ligne : montrer touche de retry ou fallback « Ouvrir dans la recherche du navigateur »

L’autre blague (et la dernière, détendez-vous) : si votre pied de page dit « Appuyez sur Esc pour fermer » et qu’Esc ne ferme pas, félicitations — vous avez inventé un test de régression de confiance.

États vides : le silence est un bug

Un état vide n’est pas « aucun résultat ». C’est une branche de l’histoire de l’utilisateur. Et dans les systèmes de production, chaque branche a besoin d’observabilité parce que c’est là que vivent la confusion.

Trois états vides dont vous avez besoin (pas un seul)

  • Aucun résultat : la requête a retourné zéro résultat. Fournissez des suggestions et expliquez la portée.
  • Pas encore indexé : les données existent mais ne sont pas recherchables. Dites-le. Fournissez un chemin de secours.
  • Accès restreint : l’utilisateur peut ne pas voir des éléments à cause des permissions. Reconnaissez-le sans divulguer d’informations sensibles.

Contenu d’état vide qui réduit les tickets

Les bons états vides répondent à trois questions :

  1. Le système m’a-t-il entendu ? Reprenez la requête (sanitizée si nécessaire).
  2. Où a-t-il cherché ? « Docs seulement » vs « Tout ».
  3. Et maintenant ? Suggestions, opérateurs de portée, ou moyen de demander l’accès.

Angle opérationnel : si vous ne pouvez pas différencier « aucun résultat » d’un « backend de recherche en timeout », vous passerez des semaines à courir après des rapports « la recherche est instable » qui sont en réalité une ambiguïté UX.

Quand « Aucun résultat » est un mensonge

Deux causes classiques :

  • Conditions de course : vous affichez l’état vide pour une réponse rapide d’une requête plus ancienne, puis l’écrasez avec des résultats de la requête la plus récente, ou l’inverse.
  • Debounce trop agressif : votre UI retarde les requêtes, mais votre état vide se déclenche immédiatement basé sur un filtrage local, donc il montre brièvement « Aucun résultat » à chaque frappe.

Corrigez cela avec le séquencement des requêtes (IDs monotones) et en liant les états au même cycle de vie : si vous débouncez les requêtes, débouncez aussi les transitions d’état vide.

Performance et fiabilité : les contraintes ennuyeuses

Cmd+K ressemble à une fonctionnalité frontend jusqu’à ce qu’elle mette à genoux un endpoint de recherche, et là, c’est une fonctionnalité SRE. Vous avez besoin de budgets et de contrôle de flux.

Budgets de latence : objectifs

  • Ouverture de la modale : sous 100 ms, incluant le placement du focus.
  • Premiers résultats après frappe : perçus sous 150–250 ms, idéalement avec des résultats locaux optimistes si disponible.
  • Navigation fléchée : doit sembler instantanée ; toute saccade est un bug.

Contrôle de flux : ne vous auto-DDOS pas

La frappe génère du trafic en rafale. Si vous appelez le backend à chaque frappe, vous devez utiliser :

  • Debounce (petit, comme 80–150 ms) et/ou throttle
  • Annulation (abort des requêtes en cours)
  • Mise en cache côté client pour les requêtes récentes
  • Limites de débit côté serveur qui renvoient une réponse conviviale, pas une crise 429

Observabilité : mesurer l’essentiel

Suivez :

  • Temps d’ouverture (keydown → champ focalisé)
  • Temps jusqu’aux premiers résultats (changement de requête → liste remplie)
  • Taux d’état vide, par portée et par famille d’agent utilisateur
  • Taux d’erreur et taux de timeout (et si l’UI a affiché « Aucun résultat » à la place)
  • Taux de requêtes backend par utilisateur (les pics signifient que le debounce ou le cache a cassé)

Astuce d’esprit SRE : traitez la modale comme un client qui produit de la charge. Ce n’est pas « l’UI ». C’est un générateur de trafic avec un clavier attaché.

Trois mini-récits d’entreprise issus du terrain

1) Incident causé par une hypothèse erronée : « Les résultats de recherche sont publics de toute façon »

Une entreprise de taille moyenne a construit une palette Cmd+K unifiée cherchant docs, tickets et tableaux de bord de services internes. L’équipe a supposé que si un élément apparaît dans la navigation, il est sûr de l’afficher dans les résultats. Cette hypothèse a tenu pour la doc. Elle a échoué spectaculairement pour les tickets et les métadonnées de service.

Ce qui a mal tourné n’était pas une fuite de contenu. C’était une fuite de métadonnées. La liste de résultats affichait titres et noms de projets pour des éléments que l’utilisateur ne pouvait pas ouvrir. « Accès refusé » apparaissait après sélection, ce que l’équipe jugeait acceptable. En pratique, le simple titre contenait des indices sensibles : noms d’incidents, noms de clients, noms de code d’acquisition. La palette est devenue un pipeline de commérages involontaire.

Le bug a survécu à la revue de code car tout le monde testait avec des comptes admin. Il a survécu à la préproduction car les données de staging étaient assainies. Il a survécu au lancement car « personne ne s’est plaint » jusqu’à ce qu’une personne le fasse, bruyamment, dans le genre de réunion que vous ne voulez pas fréquenter.

La correction n’a pas été seulement d’ajouter des vérifications de permissions. Ils ont changé le contrat UI : les résultats ne retournaient que les éléments que vous pouvez ouvrir, et pour les cas limites ils retournaient une entrée générique « Élément restreint » sans révéler de métadonnées identifiantes. Ils ont aussi ajouté un indice d’état vide : « Si vous attendiez quelque chose, vous n’y avez peut‑être pas accès. » Cette phrase seule a réduit les tickets « la recherche est cassée » et clarifié la posture sécurité.

2) Optimisation qui a échoué : pré-indexation côté client pour « rendre instantané »

Une autre équipe a décidé d’envoyer un index précomputé au navigateur pour que la palette Cmd+K fonctionne hors ligne et semble instantanée. Ça marchait dans les démos. L’index était compressé, livré via CDN, et mis en cache agressivement. L’UI semblait réactive sur un MacBook.

Puis ça a touché des appareils bas de gamme et des sessions navigateur longues. L’usage mémoire a monté. Le GC de l’app a augmenté. La palette elle-même était rapide, mais tout autour devenait plus lent. Les utilisateurs ne disaient pas « l’index est lourd ». Ils disaient « l’application est lente après le déjeuner ». Classique.

Pire, la stratégie de mise à jour de l’index était fragile. Parce que l’index était fortement mis en cache, les utilisateurs avaient des résultats périmés après des changements de rôle et de contenu. L’état vide est devenu trompeur : « Aucun résultat » pour quelque chose qui existait… hier.

La leçon pour le rollback : « hors ligne » n’est pas une fonctionnalité gratuite. L’équipe est passée à un hybride : petit cache local pour les éléments récents et épinglés, recherche serveur pour la longue traîne, et invalidation stricte du cache liée à la version d’autorisation. La palette est devenue un peu moins magique, mais l’application a cessé de consommer la RAM comme passe-temps.

3) Pratique ennuyeuse mais correcte qui a sauvé la mise : feature flag + canary + budget d’erreur

Une grande entreprise a déployé une nouvelle implémentation de palette de commandes sur plusieurs applications internes. L’équipe UI a fait les choses ennuyeuses mais justes : feature flag, cohortes canary, et un SLO clair : « taux d’erreur des requêtes de recherche < X, p95 latence < Y ». Pas d’héroïsme. Juste de la discipline.

Pendant le canary, les métriques ont montré un pic étrange : le taux d’erreur augmentait seulement pour une certaine région et seulement les lundis matins. L’UI semblait correcte. L’équipe aurait pu blâmer le backend et passer à autre chose. À la place, ils ont corrélé le pic avec un comportement clavier spécifique : des gens ouvraient la palette et collaient de longues chaînes (lots de tickets, notes de réunion) qui explosaient la complexité de requête en aval.

La correction n’était pas glamour : limites de longueur d’entrée avec message convivial, et gardes côté backend. Le canary a évité une panne plus large, et le feature flag a permis un désengagement rapide dans les apps affectées pendant que le correctif était appliqué.

Ce n’était pas un « joli succès UI ». C’était une petite victoire opérationnelle qui a évité le type de panne qui vous donne plus de travail d’observabilité que prévu pour le trimestre.

Tâches pratiques : commandes, sorties et décisions

Les bugs UI vivent souvent à la couture entre comportement frontend et réalité backend. Ces tâches sont ce que vous exécutez quand quelqu’un dit « Cmd+K est lent » ou « la recherche n’affiche rien » et que vous voulez arrêter de deviner. Chaque tâche inclut une commande, ce que la sortie signifie, et la décision que vous prenez.

1) Confirmer que le bundle UI n’a pas régressé en taille

cr0x@server:~$ ls -lh /srv/web/assets/search-palette.*.js
-rw-r--r-- 1 root root 412K Dec 18 09:12 /srv/web/assets/search-palette.8b2c1a.js

Ce que cela signifie : Le chunk de la palette pèse 412K sur le disque (pré-compression). Si avant il faisait 120K, vous avez probablement embarqué une bombe de dépendances.

Décision : Si le chunk a beaucoup grandi, auditez les imports (libs de fuzzy search, packs d’icônes), et mettez les fonctionnalités « rares » (surlignage, éléments récents) derrière des frontières asynchrones.

2) Vérifier l’efficacité gzip/brotli pour le chunk de la palette

cr0x@server:~$ gzip -c /srv/web/assets/search-palette.8b2c1a.js | wc -c
118902

Ce que cela signifie : La taille gzippée est ~119KB. C’est plausible. Si la taille gzip est proche de la taille brute, le fichier peut être mal compressé/minifié ou contenir beaucoup de données incompressibles.

Décision : Si la compression est inefficace, supprimez les datasets embarqués, gros blobs JSON ou assets base64 du bundle JS.

3) Valider que le serveur sert bien brotli quand attendu

cr0x@server:~$ curl -I -H 'Accept-Encoding: br' https://app.example.internal/assets/search-palette.8b2c1a.js
HTTP/2 200
content-type: application/javascript
content-encoding: br
cache-control: public, max-age=31536000, immutable

Ce que cela signifie : Brotli est activé. Si vous ne voyez pas content-encoding: br, votre UI « rapide » peut payer des coûts de téléchargement supplémentaires.

Décision : Corrigez la config CDN/origin avant de toucher le code UI. La bande passante est le goulot le plus stupide et le plus courant.

4) Détecter timeouts backend vs ambiguïté « Aucun résultat »

cr0x@server:~$ tail -n 20 /var/log/nginx/access.log | grep "/api/search" | tail -n 5
10.2.4.19 - - [28/Dec/2025:10:31:44 +0000] "GET /api/search?q=kafka HTTP/2.0" 200 4821 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:31:45 +0000] "GET /api/search?q=kafak HTTP/2.0" 504 164 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:31:45 +0000] "GET /api/search?q=kaf HTTP/2.0" 200 9912 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:31:46 +0000] "GET /api/search?q=kafak%20runbook HTTP/2.0" 504 164 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:31:47 +0000] "GET /api/search?q=kafka%20runbook HTTP/2.0" 200 10532 "-" "Mozilla/5.0"

Ce que cela signifie : Vous voyez des 504 pour certaines requêtes. Si l’UI traduit cela en « Aucun résultat », les utilisateurs penseront que l’indexation est cassée.

Décision : Mettez à jour l’UI pour afficher un état d’erreur sur les réponses non-200, et ajoutez des gardes backend pour la complexité des requêtes.

5) Mesurer rapidement la distribution de latence de l’API

cr0x@server:~$ awk '$7 ~ /\/api\/search/ {print $(NF-1)}' /var/log/nginx/access.log | tail -n 10
0.021
0.034
0.112
0.487
1.902
0.029
0.041
0.055
0.078
0.090

Ce que cela signifie : Ces nombres (si votre format de log inclut le temps de requête en avant‑dernier champ) montrent des réponses occasionnelles à plusieurs secondes. Une palette semblera « instable » même si la moyenne est correcte.

Décision : Si la queue de latence existe, poursuivez des améliorations p95/p99 : cache, limites de requête, ou résultats populaires précomputés.

6) Confirmer que la limitation de débit ne punit pas les rafales de frappe

cr0x@server:~$ grep -E " 429 " /var/log/nginx/access.log | grep "/api/search" | tail -n 5
10.2.4.19 - - [28/Dec/2025:10:32:02 +0000] "GET /api/search?q=kafka%20runb HTTP/2.0" 429 89 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:32:02 +0000] "GET /api/search?q=kafka%20runbo HTTP/2.0" 429 89 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:32:02 +0000] "GET /api/search?q=kafka%20runboo HTTP/2.0" 429 89 "-" "Mozilla/5.0"
10.2.4.19 - - [28/Dec/2025:10:32:02 +0000] "GET /api/search?q=kafka%20runbook HTTP/2.0" 200 10532 "-" "Mozilla/5.0"

Ce que cela signifie : Une rafale de requêtes est throttlée jusqu’à ce que la dernière passe. L’UI clignotera des états vides/erreur.

Décision : Debounce côté client, mais aussi ajuster les limites pour autoriser de courtes rafales par utilisateur. La limitation doit protéger l’infrastructure, pas punir la frappe.

7) Vérifier si les requêtes de recherche sont cacheables et mises en cache

cr0x@server:~$ curl -I "https://app.example.internal/api/search?q=runbook"
HTTP/2 200
content-type: application/json
cache-control: private, max-age=0
vary: authorization

Ce que cela signifie : Pas de cache. Parfois correct (résultats personnalisés), parfois coûteux (docs publics).

Décision : Si les résultats sont sûrs à mettre en cache par utilisateur, envisagez un cache court clé par contexte d’auth. Sinon, au moins ajoutez une mémorisation serveur pour les requêtes identiques sur une fenêtre temporelle.

8) Confirmer que le backend de recherche est sain (service systemd)

cr0x@server:~$ systemctl status search-api.service --no-pager
● search-api.service - Search API
     Loaded: loaded (/etc/systemd/system/search-api.service; enabled)
     Active: active (running) since Sun 2025-12-28 09:41:10 UTC; 52min ago
   Main PID: 2147 (search-api)
      Tasks: 24
     Memory: 612.4M
        CPU: 18min 22.118s

Ce que cela signifie : Le service est up ; la mémoire est significative. Si la mémoire monte continuellement, vous avez peut‑être une fuite de cache ou des jeux de résultats non bornés.

Décision : Si la mémoire est suspecte, inspectez le heap/métriques ; plafonnez les caches ; imposez des tailles de réponse max.

9) Identifier la saturation CPU corrélée aux rafales de frappe

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.1.0 (search01)  12/28/2025  _x86_64_  (8 CPU)

10:34:21 AM  CPU   %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
10:34:22 AM  all   78.12 0.00  9.34   0.17 0.00  0.42   0.00   0.00   0.00 11.95
10:34:23 AM  all   82.55 0.00 10.02   0.12 0.00  0.38   0.00   0.00   0.00  6.93
10:34:24 AM  all   80.10 0.00  9.77   0.09 0.00  0.40   0.00   0.00   0.00  9.64

Ce que cela signifie : Le CPU est fortement utilisé en espace utilisateur. Cohérent avec un classement coûteux, du fuzzy matching, ou du chargement d’index par requête.

Décision : Profilez la gestion des requêtes backend ; préchargez les index ; réduisez les allocations par requête ; limitez les algorithmes flous sur les courtes requêtes.

10) Déterminer si vous êtes lié par l’I/O (latence stockage)

cr0x@server:~$ iostat -xz 1 3
Linux 6.1.0 (search01)  12/28/2025  _x86_64_  (8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          71.20    0.00    8.90    9.80    0.00   10.10

Device            r/s     w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %util  await
nvme0n1         980.0   220.0  7840.0  1960.0     0.0     0.0   92.0   7.80

Ce que cela signifie : %util élevé et await significatif. Si votre backend de recherche touche le disque pour chaque requête (index sur disque, cache froid), vous aurez des spikes p95 perceptibles.

Décision : Gardez les index chauds en mémoire ; ajustez le page cache ; réduisez les lectures aléatoires ; ou migrez vers un moteur de recherche adapté à cette charge.

11) Vérifier les problèmes réseau entre l’UI et l’API

cr0x@server:~$ ss -s
Total: 1382
TCP:   921 (estab 612, closed 245, orphaned 3, synrecv 0, timewait 245/0), ports 0

Transport Total     IP        IPv6
RAW       0         0         0
UDP       42        36        6
TCP       676       498       178
INET      718       534       184
FRAG      0         0         0

Ce que cela signifie : Beaucoup de timewait peut indiquer de nombreuses connexions courtes. Si keepalive HTTP est mal configuré, chaque frappe peut ouvrir une nouvelle connexion.

Décision : Corrigez les paramètres keepalive au load balancer ou Nginx ; privilégiez HTTP/2 ; réduisez les surcoûts par requête.

12) Vérifier que l’UI n’appelle pas la recherche quand la modale est fermée

cr0x@server:~$ journalctl -u search-api.service --since "10 minutes ago" | grep "q=" | tail -n 8
Dec 28 10:28:01 search01 search-api[2147]: request_id=2b7b q=runbook user=anon status=200
Dec 28 10:28:02 search01 search-api[2147]: request_id=2b7c q=runboo user=anon status=200
Dec 28 10:28:02 search01 search-api[2147]: request_id=2b7d q=runbook user=anon status=200
Dec 28 10:28:03 search01 search-api[2147]: request_id=2b7e q= user=anon status=400
Dec 28 10:28:03 search01 search-api[2147]: request_id=2b7f q= user=anon status=400
Dec 28 10:28:04 search01 search-api[2147]: request_id=2b80 q= user=anon status=400
Dec 28 10:28:05 search01 search-api[2147]: request_id=2b81 q= user=anon status=400
Dec 28 10:28:06 search01 search-api[2147]: request_id=2b82 q= user=anon status=400

Ce que cela signifie : Des requêtes vides (q=) sont envoyées de manière répétée. C’est souvent un bug de cycle de vie UI : effacer l’entrée déclenche une requête même quand la modale se ferme.

Décision : Ajoutez une garde client : ne pas requêter pour une chaîne vide ; ne pas requêter si la modale est fermée ; annulez le travail en cours à la fermeture.

13) Inspecter les requêtes lentes SQL si la recherche frappe la base

cr0x@server:~$ sudo -u postgres psql -c "select now() - query_start as age, state, left(query,120) from pg_stat_activity where datname='searchdb' order by query_start asc limit 5;"
   age   | state  | left
---------+--------+--------------------------------------------------------------
 00:00:05| active | select id,title from docs where title ilike '%kafka runbook%' l
 00:00:02| active | select id,title from docs where title ilike '%kafka runb%' limi
 00:00:01| active | select id,title from docs where title ilike '%kafka run%' limit

Ce que cela signifie : Vous faites des ILIKE wildcard à chaque frappe. C’est un moyen prévisible de générer une facture DB plus grosse et de la tristesse.

Décision : Passez à un index full-text approprié, ou au moins contraignez les requêtes (recherche préfixe, index trigram), et débouncez plus agressivement.

14) Vérifier le ratio hits/misses du cache si Redis est utilisé

cr0x@server:~$ redis-cli info stats | egrep "keyspace_hits|keyspace_misses"
keyspace_hits:1829012
keyspace_misses:712334

Ce que cela signifie : Les misses sont élevées par rapport aux hits. Si votre cache n’aide pas, vous faites du travail en plus pour rien.

Décision : Reconsidérez les clés de cache (normalisez les requêtes), la TTL, et si le caching est par utilisateur vs global. Si la personnalisation tue le cache, ne cachez que le sous‑ensemble public.

Mode d’intervention rapide

Quand « Cmd+K est lent » apparaît dans le chat, vous avez deux tâches : arrêter l’hémorragie et identifier le goulot réel. Ne commencez pas par réécrire le classement. Commencez par prouver où va le temps.

Première étape : décider si c’est du jank UI ou de la latence backend

  • Symptômes UI jank : navigation fléchée saccadée, frappe lente, surlignage de sélection qui « téléporte », pics CPU côté client.
  • Symptômes latence backend : la frappe est fluide mais les résultats arrivent tard ; « chargement » persiste ; les retries aident ; les erreurs se corrèlent aux pics de trafic.

Vérification rapide : cherchez des 5xx/429 dans les logs d’accès pour /api/search et comparez aux rapports utilisateurs.

Deuxième étape : vérifier les patterns de requêtes (vous pourriez spammer)

  • Envoyez-vous des requêtes pour des requêtes vides ?
  • Envoyez-vous des requêtes alors que la modale est fermée ?
  • Annulez-vous les requêtes en vol ?
  • Y a-t-il du caching pour les requêtes partielles répétées (k, ka, kaf) ?

Troisième étape : isoler la couche lente

  • Si le backend est lent : vérifiez CPU (mpstat), I/O (iostat), activité DB (pg_stat_activity), taux de hit cache (Redis), puis logs pour pics de complexité de requête.
  • Si l’UI est lente : réduisez la complexité de rendu de la liste, évitez de rerendre toute la liste sur les changements de sélection, et assurez-vous que la logique de surlignage n’est pas O(n*m) par frappe.

Heuristique d’astreinte : si vous voyez des 429, corrigez d’abord le taux de requêtes client. Si vous voyez des 504, corrigez timeouts et gardes de requête. Si ni l’un ni l’autre, vous avez probablement du jank UI.

Erreurs courantes (symptômes → cause → correctif)

1) « La frappe semble retardée »

Symptôme : les caractères apparaissent en retard ; le curseur saccade.

Cause racine : travail synchrone à chaque frappe (rendu trop de nœuds DOM ; surlignage coûteux ; parsing JSON ; fuzzy matching sur le thread principal).

Correctif : gardez le DOM petit ; limitez le nombre de résultats affichés ; pré-calculer le surlignage ; déplacez les travaux lourds dans un web worker si côté client ; débouncez les appels réseau mais pas le rendu local de l’entrée.

2) « Les flèches arrêtent parfois de fonctionner »

Symptôme : la navigation fonctionne puis n’importe quand cesse ; le focus semble perdu.

Cause racine : le focus a bougé vers un élément non focalisable, ou la modale se ferme et se rouvre sans restaurer le focus ; des en-têtes de groupe deviennent accidentellement focalisables.

Correctif : gardez le focus dans l’entrée (modèle active-descendant) ou assurez que les items de liste sont constamment focalisables ; à l’ouverture, placez le focus de façon déterministe ; à la fermeture, restaurez le focus de déclenchement.

3) « Esc ferme, mais le défilement en arrière-plan est encore possible »

Symptôme : l’utilisateur peut faire défiler et la page derrière bouge ; la modale reste en place.

Cause racine : verrouillage du scroll non appliqué, ou mal appliqué pour iOS/conteneurs overflow.

Correctif : verrouillez le défilement avec une approche testée ; assurez-vous que la zone défilable est le conteneur de résultats. Vérifiez spécifiquement sur Safari mobile.

4) « Aucun résultat » clignote pendant que les résultats chargent

Symptôme : l’état vide apparaît brièvement, puis les résultats arrivent.

Cause racine : l’état vide est lié à « length du tableau de résultats === 0 » sans considérer l’état « loading » ; course entre requêtes.

Correctif : états explicites : idle, loading, loaded, error. Liez chaque vue rendue à un ID de requête.

5) « Les résultats semblent corrects mais ouvrent le mauvais élément »

Symptôme : le surlignage est sur l’item A ; Enter ouvre l’item B.

Cause racine : clés basées sur l’index du tableau ; la liste se réordonne ; l’indice de sélection pointe vers l’ordre ancien.

Correctif : stockez la sélection par ID de résultat stable ; mettez à jour la sélection quand les résultats changent ; évitez le mapping basé sur l’index pour l’activation.

6) « La recherche marchait hier ; aujourd’hui elle est vide pour certains utilisateurs »

Symptôme : un sous-ensemble d’utilisateurs voit un état vide pour des éléments connus.

Cause racine : mismatch recherche sensible aux permissions, index client périmé, ou cache qui ne varie pas par auth.

Correctif : assurez que les réponses de recherche respectent l’autorisation ; variez les caches par contexte d’auth ; versionnez les droits utilisateur et invalidez en conséquence.

7) « Cmd+K cesse de fonctionner dans les zones de texte »

Symptôme : le raccourci entre en conflit avec l’édition ou est bloqué.

Cause racine : le gestionnaire global de touches ignore le contexte de l’élément cible ou empêche le comportement par défaut de façon incorrecte.

Correctif : ne déclenchez pas dans les éléments éditables ; autorisez une désactivation ; gardez le traitement des touches limité et conservateur.

8) « Les utilisateurs de lecteurs d’écran ne savent pas ce qui est sélectionné »

Symptôme : la liste se met à jour mais aucune annonce ; la sélection est seulement indiquée visuellement.

Cause racine : rôles/relations ARIA manquants ; active descendant non câblé ; sélection indiquée uniquement visuellement.

Correctif : implémentez un modèle connu ; assurez les annonces ; testez avec de vrais lecteurs d’écran, pas seulement des audits.

Listes de contrôle / plan étape par étape

Plan de construction étape par étape (HTML/CSS d’abord)

  1. Définir le contrat : ce qui peut être recherché (docs, commandes, personnes), quelles métadonnées peuvent être affichées, et que se passe-t-il aux frontières de permission.
  2. Marquer la coquille de la boîte : inclure titre, champ, conteneur de résultats, et indices en pied de page. Rendre lisible sans comportement JS.
  3. Implémenter les règles de focus : à l’ouverture, focus sur l’entrée ; à la fermeture, restaurer le focus ; piéger le focus dans la modale ; assurer que Esc ferme toujours.
  4. Implémenter la navigation de liste : décider du modèle de focus (active descendant vs déplacement du focus). Rendre / déterministes.
  5. Ajouter les états : idle, loading, loaded, empty, error/offline. Rendre chaque état distinct visuellement et soigné dans la copie.
  6. Ajouter les indices clavier : n’afficher que les touches qui fonctionnent dans l’état courant. Adapter les touches-modificateurs selon la plateforme.
  7. Garde-fous performance : limiter le nombre de résultats affichés ; limiter le taux de requêtes ; annuler les requêtes en vol ; cacher les requêtes récentes en toute sécurité.
  8. Observabilité : logger les request IDs et durées ; instrumenter temps d’ouverture et temps jusqu’aux résultats ; suivre le taux d’état vide.
  9. Tests d’accessibilité : vérifier uniquement au clavier ; puis lecteur d’écran ; puis mode fort contraste ; puis réduction des animations.
  10. Plan de déploiement : déployer derrière feature flag ; canary ; surveiller les budgets d’erreur ; avancer ou reculer avec intention.

Checklist pré-lancement (choses qui cassent à l’échelle)

  • L’ouverture de la modale est sous 100 ms sur du matériel milieu de gamme.
  • La navigation des résultats n’a pas de thrash layout (pas de saut de scroll ; la sélection reste visible).
  • L’API peut gérer les rafales de frappe sans tempêtes 429.
  • Les timeouts produisent un état d’erreur, pas « Aucun résultat ».
  • Modèle de permissions : vous ne divulguez pas les titres d’objets restreints.
  • L’état vide explique la portée et propose une action suivante.
  • Les indices sont corrects sur macOS et Windows, et pas trop présents sur mobile.
  • La fermeture de la modale annule le travail et restaure le focus de façon fiable.

Checklist d’astreinte (quand c’est déjà cassé)

  • Vérifier les logs d’accès pour 429/504 sur /api/search.
  • Vérifier la saturation CPU et I/O du backend.
  • Vérifier si l’UI envoie des requêtes vides ou des requêtes en arrière-plan.
  • Basculer le feature flag si cela provoque des pics de charge.
  • Communiquer un contournement utilisateur (recherche de navigation, page de recherche dédiée) pendant la réparation.

FAQ

Cmd+K doit‑elle être une « palette de commandes » ou une « boîte de recherche » ?

Faites les deux, mais séparez visuellement et sémantiquement les types de résultats. Les utilisateurs taperont « créer facture » aussi bien que « facture 10492 ». Ne les forcez pas à deviner le mode.

Ai-je besoin d’un combobox ARIA, ou puis-je juste utiliser une liste sous une entrée ?

Vous pouvez utiliser un modèle plus simple textbox + listbox, mais vous devez malgré tout fournir des rôles corrects et des annonces de sélection. Une combobox à moitié implémentée est pire qu’un modèle simple bien implémenté.

Combien de résultats devrais-je afficher ?

Commencez par 8–12 lignes visibles et permettez le défilement pour plus. Une palette sert aux actions rapides ; afficher 50 résultats sans virtualisation est un coût performance et cognitif.

La liste doit‑elle se mettre à jour à chaque frappe ?

Oui, mais cela ne signifie pas que vous appelez le backend à chaque frappe. Mettez à jour l’UI immédiatement (état de chargement), puis fetcher avec debounce + annulation.

Comment gérer l’IME/la composition de saisie ?

Ne traitez pas les mises à jour de composition comme des requêtes finalisées. Évitez de lancer des recherches pendant la composition ; déclenchez la recherche quand la composition se termine, ou quand vous recevez un événement d’entrée engagé.

Quelle est la meilleure copie pour un état vide ?

Reprenez la requête, précisez la portée, donnez deux suggestions, et reconnaissez la possibilité de permissions. Évitez de blâmer l’utilisateur. Vous n’êtes pas son professeur d’anglais.

Dois‑je montrer « recherches récentes » ou « éléments récents » ?

Privilégiez les éléments récents (ce qu’ils ont ouvert) plutôt que les requêtes récentes (ce qu’ils ont tapé). Les requêtes peuvent être sensibles. Les éléments sont généralement moins personnels et plus actionnables. Si vous conservez quoi que ce soit, gardez‑le local et borné.

Comment empêcher la palette de marteler le backend ?

Debounce client (petit), annulation des requêtes en vol, cache des requêtes récentes, et gardes côté backend. Puis instrumentez le taux de requêtes-par-utilisateur pour détecter les régressions.

Dois‑je virtualiser la liste de résultats ?

Seulement si vous affichez réellement de grandes listes. La virtualisation augmente la complexité et peut nuire à l’accessibilité. Pour une palette, il est souvent préférable de limiter les résultats et garder le DOM simple.

Comment rendre les indices clavier conformes à la plateforme ?

Détectez la plateforme à l’exécution et affichez Cmd vs Ctrl. N’affichez pas non plus des indices nécessitant beaucoup de modificateurs si l’utilisateur est sur un appareil uniquement tactile.

Conclusion : prochaines étapes réalisables

Si vous voulez une modale Cmd+K qui ressemble à un outil natif plutôt qu’à une superposition fragile, faites trois choses cette semaine :

  1. Rendez les états explicites : idle, loading, loaded, empty, error. Cessez de laisser « Aucun résultat » masquer les timeouts.
  2. Auditez le DOM de la liste de résultats : lignes peu profondes, IDs stables, sélection comme état, et pas de hacks innerHTML pour le surlignage.
  3. Enfilez un bonnet SRE : mesurez les taux de requêtes, latence queue, et fréquence des états vides. Ajoutez du backpressure avant d’en avoir besoin.

Puis déployez derrière un feature flag. Faites un canary. Surveillez‑la comme n’importe quel service pouvant créer des pics de charge. Parce que c’est exactement ce que c’est : un service, avec un clavier.

Conçu avec un biais opérationnel : si on ne peut pas le déboguer rapidement, ce n’est pas terminé.

← Précédent
Ubuntu 24.04 : les caches DNS mentent — videz le bon cache (et arrêtez de vider le mauvais) (cas n°26)
Suivant →
Corruption des boîtes mail Dovecot : procédures de récupération minimisant les dégâts

Laisser un commentaire