Pourquoi 80% des APIs sont lentes (et comment éviter les erreurs classiques)

Matthieu Werner
Temps de lecture : 5 min
symfony php performance
Pourquoi 80% des APIs sont lentes (et comment éviter les erreurs classiques)
Pourquoi 80% des APIs sont lentes (et comment éviter les erreurs classiques)

La majorité des APIs que l’on rencontre en production ne sont pas catastrophiques. Elles tiennent la charge, elles répondent, elles remplissent leur rôle.

Certaines (beaucoup) sont lentes.

Pas au point de bloquer un produit immédiatement, mais suffisamment pour que les lenteurs s’accumulent : le front compense, du cache est ajouté, les coûts montent, et la latence devient une sorte de norme implicite.

Dans la plupart des cas, ce n’est pas un problème d’infrastructure, mais un problème de conception.


Le moment où l’on regarde réellement

Prenons un cas assez classique : un endpoint de listing paginé avec enrichissement.

Temps moyen observé : ~400–500 ms.

Rien d’anormal côté infrastructure :

  • CPU peu sollicité
  • réseau stable
  • aucune saturation évidente

À ce stade, le réflexe est souvent d’ajouter du cache. Ce qui améliore légèrement (voire énormément) la situation, mais sans en traiter la cause.

En tout 1er lieu, il est indispensable de logger quelques métriques :

$start = hrtime(true);
$orders = $this->service->getOrders($criteria);
$afterDomain = hrtime(true);
$response = $this->presenter->toArray($orders);
$afterSerialization = hrtime(true);
logger()->info('api_breakdown', [
    'domain_ms' => ($afterDomain - $start) / 1e6,
    'serialization_ms' => ($afterSerialization - $afterDomain) / 1e6,
]);

On obtient typiquement :

domain_ms: 120
serialization_ms: 280
total: ~450ms

À ce stade, on a une intuition, mais pas encore de diagnostic fiable.


Le "N+1 query problem"

Le N+1 “classique” est connu. Dans notre contexte il peut cependant se cacher dans les détails :

return $this->json($orders, context: [
    'groups' => ['order:list']
]);

Avec une configuration du type :

#[Groups(['order:list'])]
public function getUser(): User
{
    return $this->user;
}

Puis :

#[Groups(['order:list'])]
public function getCompany(): Company
{
    return $this->company;
}

Et ainsi de suite.

Le code applicatif reste propre. Mais la traversée de graphe déclenche :

  • lazy loading Doctrine
  • requêtes multiples
  • hydration implicite
  • sérialisation récursive

Le point critique est que ce comportement est diffus. Il n’existe pas de point unique dans le code où il est explicitement visible.


Reprendre le contrôle

Le correctif ne consiste pas simplement à “ajouter des join”.

Il consiste à reprendre la main sur ce qui est réellement nécessaire.

$query = $em->createQuery("
    SELECT NEW App\\ReadModel\\OrderListItem(
        o.id,
        u.id,
        u.name,
        c.name,
        SUM(i.price)
    )
    FROM Order o
    JOIN o.user u
    JOIN u.company c
    JOIN o.items i
    WHERE o.status = :status
    GROUP BY o.id, u.id, c.id
");

$query->setParameter('status', OrderStatus::PAID);

Dans cette approche :

  • aucune entité complète n’est hydratée
  • aucune relation n’est traversée implicitement
  • le résultat est directement exploitable

Le coût réel des objets

Une grande partie du coût ne vient pas de la base, mais de la manipulation d’objets.

Hydrater un graphe riche pour n’en exploiter qu’une fraction est extrêmement coûteux :

  • allocation mémoire
  • CPU
  • garbage collection
  • sérialisation

Un cas réel observé :

  • ~150 entités
  • une dizaine de relations par entité
  • hydration complète

Après refonte vers des read models ciblés :

RAM: -60%
Temps total: -70%

Le gain ne vient pas d’une optimisation fine, mais de la suppression de travail inutile.


La sérialisation comme facteur dominant

La sérialisation est souvent perçue comme un coût marginal. En pratique, elle devient rapidement dominante.

Un comportement intéressant est son caractère non linéaire :

100 items → 40ms
200 items → 140ms
400 items → 480ms

Ce phénomène s’explique par :

  • la récursion
  • la réflexion
  • les métadonnées
  • les normalizers imbriqués

Dans certains cas, une approche plus directe devient pertinente :

return new JsonResponse(
    $connection->fetchAllAssociative($sql)
);

Cela réduit drastiquement :

  • le nombre d’objets
  • les transformations
  • la variabilité des temps de réponse

(mais il faut aimer, et accepter de perdre un peu de confort pour gagner en maîtrise)


L’accumulation des couches

Une architecture “propre” mal maîtrisée peut introduire un coût important.

Un chemin classique :

Controller → Handler → Service → Mapper → DTO → Presenter → Serializer

Pour produire une réponse simple.

Chaque couche est justifiable individuellement. L’ensemble peut devenir excessif.

Sur les chemins critiques, un compromis est souvent nécessaire :

  • conserver la séparation métier
  • simplifier les lectures

Le rôle des métriques

Sans métriques, les décisions sont basées sur des hypothèses.

Un minimum d’observabilité change radicalement la situation :

logger()->info('perf', [
    'sql_count' => $sqlLogger->getQueryCount(),
    'sql_time_ms' => $sqlLogger->getTotalTime(),
    'total_ms' => $total,
]);

Cela permet :

  • d’identifier les endpoints problématiques
  • de comprendre la répartition du temps
  • d’éviter les optimisations inutiles

Conclusion

Une API lente n’est presque jamais le résultat d’un problème unique.

C’est une accumulation :

  • accès aux données inefficace
  • sur-hydratation
  • sérialisation coûteuse
  • empilement de couches
  • absence de mesure

Une API performante n’est pas une API optimisée après coup.

C’est une API :

  • explicite
  • mesurée
  • maîtrisée

TL;DR

  • Le N+1 est toujours présent, mais plus diffus
  • Les objets coûtent plus cher que les données
  • La sérialisation peut devenir dominante
  • Les abstractions ont un coût réel
  • Les métriques sont indispensables

Auteur

Matthieu Werner

Partager cet article

Prêt à transformer vos idées ?

Découvrez comment La Programmerie peut donner vie à vos projets avec passion, expertise et une touche de magie technologique ✨

Email

Réponse garantie sous 24h

Téléphone

Disponible 9h-18h

Rendez-vous

Parlons de votre projet

Notes techniques

Recevez nos explorations par e-mail — articles et veille, sans spam.

Nous respectons votre vie privée et ne partageons jamais vos données.