Le temps de réponse API constitue l'un des facteurs les plus critiques de l'expérience utilisateur. Chaque milliseconde compte : une augmentation de 100ms de latence peut réduire les conversions de 1%.
L'optimisation va au-delà du simple tuning de code. Elle englobe l'architecture, le caching, la gestion des données et l'infrastructure réseau.
Qu'est-ce que l'Optimisation du Temps de Réponse ?
L'optimisation du temps de réponse vise à minimiser le délai entre la réception d'une requête et l'envoi de la réponse complète.
Décomposition du temps de réponse
Le délai se décompose en plusieurs phases :
Client ──► Réseau ──► Serveur ──► Traitement ──► Données ──► Réponse ──► Client
│ │ │ │ │
Latence Parsing Business DB/Cache Sérialisation
réseau requête logic I/O + transfert
Les composantes principales
Chaque phase offre des opportunités d'optimisation :
| Composante | Description | Techniques |
|---|---|---|
| Réseau | Latence client-serveur | CDN, HTTP/2, HTTP/3 |
| Traitement | Logique métier | Optimisation code, parallélisme |
| Données | Accès DB/cache | Indexation, caching, eager loading |
| Transfert | Taille réponse | Compression, pagination |
Mesure avant optimisation
Identifiez les goulots d'étranglement :
import time
from functools import wraps
def profile_endpoint(func):
@wraps(func)
async def wrapper(*args, **kwargs):
timings = {}
start = time.perf_counter()
# Parsing
timings['parse'] = time.perf_counter() - start
db_start = time.perf_counter()
result = await func(*args, **kwargs)
timings['db'] = time.perf_counter() - db_start
serialize_start = time.perf_counter()
response = serialize(result)
timings['serialize'] = time.perf_counter() - serialize_start
timings['total'] = time.perf_counter() - start
logger.info('Request profile', extra=timings)
return response
return wrapper
Pourquoi l'Optimisation est Cruciale
Impact sur l'expérience utilisateur
Les interfaces réactives retiennent l'attention :
Temps de réponse Perception utilisateur
────────────────────────────────────────────
< 100ms Instantané
100-300ms Rapide
300-1000ms Perceptible
> 1000ms Lent, frustrant
> 3000ms Abandon probable
SEO et référencement
Google intègre la vitesse dans son algorithme :
- Core Web Vitals (LCP, FID, CLS)
- Time to First Byte (TTFB)
- Mobile-first indexing
Scalabilité
Des requêtes plus rapides libèrent les ressources :
# Impact de l'optimisation sur la capacité
avant_optimisation = {
"latence_moyenne": "500ms",
"requetes_par_seconde": 200,
"serveurs_necessaires": 10
}
apres_optimisation = {
"latence_moyenne": "100ms",
"requetes_par_seconde": 1000, # 5x plus
"serveurs_necessaires": 2 # 80% de réduction
}
Coûts d'infrastructure
L'efficacité réduit les coûts cloud :
Comment Optimiser le Temps de Réponse
Caching multi-niveaux
Le caching est généralement le levier le plus puissant :
# Cache HTTP avec headers
from fastapi import FastAPI
from fastapi.responses import Response
app = FastAPI()
@app.get("/products/{product_id}")
async def get_product(product_id: int, response: Response):
response.headers["Cache-Control"] = "public, max-age=300"
response.headers["ETag"] = f'"{product_id}-{version}"'
return await fetch_product(product_id)
# Cache applicatif avec Redis
import redis
import json
cache = redis.Redis(host='localhost', port=6379)
async def get_product_cached(product_id: int):
cache_key = f"product:{product_id}"
# Try cache first
cached = cache.get(cache_key)
if cached:
return json.loads(cached)
# Cache miss - fetch from DB
product = await db.fetch_product(product_id)
# Store in cache
cache.setex(cache_key, 300, json.dumps(product))
return product
Niveaux de cache
Implémentez plusieurs niveaux :
┌─────────────────────────────────────────────────┐
│ Client │
│ Browser Cache │
└─────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────┐
│ CDN │
│ Edge Cache │
└─────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────┐
│ Application │
│ Redis / Memcached │
└─────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────┐
│ Database │
│ Query Cache │
└─────────────────────────────────────────────────┘
Optimisation des requêtes base de données
Évitez les problèmes courants :
# Problème N+1 - À éviter
async def get_orders_bad():
orders = await db.fetch_all("SELECT * FROM orders")
for order in orders:
# N requêtes supplémentaires !
order.items = await db.fetch_all(
"SELECT * FROM order_items WHERE order_id = ?",
order.id
)
return orders
# Solution - Eager loading
async def get_orders_good():
return await db.fetch_all("""
SELECT o.*, json_agg(i.*) as items
FROM orders o
LEFT JOIN order_items i ON i.order_id = o.id
GROUP BY o.id
""")
Indexation appropriée
Créez des index ciblés :
-- Index pour les requêtes fréquentes
CREATE INDEX idx_orders_user_date
ON orders (user_id, created_at DESC);
-- Index partiel pour les requêtes filtrées
CREATE INDEX idx_orders_pending
ON orders (created_at)
WHERE status = 'pending';
-- Vérifier l'utilisation des index
EXPLAIN ANALYZE SELECT * FROM orders
WHERE user_id = 123 AND created_at > '2025-01-01';
Traitement asynchrone
Déplacez les opérations non-critiques :
from celery import Celery
celery = Celery('tasks', broker='redis://localhost:6379')
@app.post("/orders")
async def create_order(order: OrderCreate):
# Opération critique - synchrone
order = await db.create_order(order)
# Opérations non-critiques - asynchrone
send_confirmation_email.delay(order.id)
update_analytics.delay(order.id)
notify_warehouse.delay(order.id)
# Réponse immédiate
return order
@celery.task
def send_confirmation_email(order_id):
# Traitement en background
pass
Compression des réponses
Réduisez la taille des transferts :
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
app = FastAPI()
# Compression automatique pour réponses > 500 bytes
app.add_middleware(GZipMiddleware, minimum_size=500)
# Configuration nginx
gzip on;
gzip_types application/json text/plain application/javascript;
gzip_min_length 1000;
gzip_comp_level 6;
Pagination efficace
Limitez les données retournées :
from fastapi import Query
@app.get("/products")
async def list_products(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100)
):
offset = (page - 1) * page_size
products = await db.fetch_all(
"SELECT * FROM products LIMIT ? OFFSET ?",
page_size, offset
)
total = await db.fetch_one("SELECT COUNT(*) FROM products")
return {
"data": products,
"pagination": {
"page": page,
"page_size": page_size,
"total": total,
"pages": (total + page_size - 1) // page_size
}
}
CDN pour APIs distribuées
Rapprochez les données des utilisateurs :
# Headers pour CDN caching
@app.get("/api/public/config")
async def get_config(response: Response):
response.headers["Cache-Control"] = "public, s-maxage=3600"
response.headers["CDN-Cache-Control"] = "max-age=86400"
response.headers["Vary"] = "Accept-Language"
return config
Bonnes Pratiques d'Optimisation
Mesurez avant et après
Validez chaque optimisation :
# Benchmark avant/après
import statistics
def benchmark(func, iterations=100):
times = []
for _ in range(iterations):
start = time.perf_counter()
func()
times.append(time.perf_counter() - start)
return {
"mean": statistics.mean(times),
"median": statistics.median(times),
"p95": sorted(times)[int(iterations * 0.95)],
"p99": sorted(times)[int(iterations * 0.99)]
}
Percentiles, pas moyennes
Surveillez le p95 et p99 :
SLO de latence
Définissez des objectifs mesurables :
slos:
- name: api_latency_p95
target: 200ms
window: 30d
alert_threshold: 250ms
- name: api_latency_p99
target: 500ms
window: 30d
alert_threshold: 750ms
Évitez l'optimisation prématurée
Priorisez correctement :
- Correction : Le code fonctionne-t-il ?
- Clarté : Le code est-il maintenable ?
- Performance : Le code est-il assez rapide ?
Mais adressez les anti-patterns évidents immédiatement.
Tests de charge
Testez sous conditions réalistes :
# Avec k6
k6 run --vus 100 --duration 5m load-test.js
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function() {
const res = http.get('https://api.example.com/products');
check(res, {
'status is 200': (r) => r.status === 200,
'latency < 200ms': (r) => r.timings.duration < 200,
});
sleep(1);
}
Conclusion
L'optimisation du temps de réponse API combine mesure précise, techniques éprouvées et amélioration continue.
Les leviers principaux :
- Caching : Le plus grand impact généralement
- Optimisation DB : Index, eager loading, requêtes efficaces
- Asynchronicité : Déplacer le non-critique
- Compression : Réduire les transferts
L'investissement se rentabilise par la satisfaction utilisateur et les économies d'infrastructure.