Études de cas
Un aperçu plus approfondi de certaines solutions techniques mises en place dans mes expériences et projets.
Optimisation des performances d'une API - Jusqu'à 23x plus rapide
Contexte : ce travail a été réalisé sur une plateforme de gestion de sinistres et de flottes d'assurance, où un Incident représente un sinistre lié à un objet de flotte, à des garanties contractuelles, à plusieurs utilisateurs impliqués, et à bien d'autres données. Le backend est écrit en TypeScript et Express, avec Sequelize (via les décorateurs de sequelize-typescript) comme ORM, sur une base de données PostgreSQL.
J'ai optimisé les performances de deux routes API essentielles : getUserIncidentsList et getAllUsersLinkedToIncidentById, utilisées respectivement sur le tableau de bord principal de l'utilisateur et sur la page d'un Incident. Voici les améliorations apportées.
Réduction du volume de données transférées
La plupart des includes Sequelize récupéraient des lignes entières alors que seules quelques colonnes étaient réellement utilisées en aval. L'ajout de listes d'attributs explicites sur la quasi-totalité des includes a réduit à la fois le coût des requêtes et la taille des réponses. J'ai aussi supprimé plusieurs includes inutilisés (dossiers de dommages, objets utilisateurs créateur/vérificateur/responsable avec leurs données Keycloak imbriquées) après avoir vérifié qu'ils n'étaient pas exploités par la réponse.
Rapprochement du filtrage de la donnée
Un filtre par type de champ, auparavant appliqué côté application après récupération de l'ensemble des champs, a été déplacé dans la clause where de la requête SQL, afin que la base de données ne renvoie que les lignes nécessaires. J'ai également allégé la réponse après récupération, en retirant les champs à valeur vide avant l'envoi au client, ce qui réduit fortement la taille des réponses pour les objets Incident volumineux.
Élimination des requêtes N+1
Le gain le plus important est venu de la refonte de getAllUsersLinkedToIncidentById. L'implémentation précédente résolvait l'accès à la flotte pour chaque utilisateur candidat au sein d'une boucle, déclenchant un nouveau lot de requêtes imbriquées à chaque itération. Je l'ai réécrite pour ne récupérer qu'une seule fois les données de l'objet de l'Incident ainsi que la hiérarchie flotte/groupe du client, puis comparer en mémoire les unités métier, sociétés et périmètres de chaque utilisateur à ces données. Le nombre de requêtes, jusque-là proportionnel au nombre d'utilisateurs candidats, est ainsi devenu constant et faible. J'ai également ajouté un retour anticipé pour les incidents confidentiels, évitant tout le chemin de résolution client/flotte lorsqu'il n'est pas nécessaire.
Adaptation de la forme des requêtes au cas d'usage
Lorsqu'une route devait fournir à la fois une vue complète de l'Incident et une variante allégée pour les incidents liés, j'ai paramétré les listes d'includes et d'attributs plutôt que de réutiliser partout la structure la plus lourde, afin que chaque chemin de requête ne paie que pour ce qu'il utilise réellement.
Résultat
Ensemble, ces changements ont réduit à la fois le nombre d'allers-retours vers la base de données et le volume de données transférées par requête.
getUserIncidentsList, la plus coûteuse des deux routes, est passée d'environ 11.5 secondes à 500ms en moyenne, soit une amélioration de 23x. getAllUsersLinkedToIncidentById est passée d'un peu plus de 5 secondes à environ 350ms, soit une amélioration de 15x.