Le problème de 5 secondes qui a failli tuer notre produit
Notre première démo d'API Excel était un désastre.
Client : « Montrez-moi la vitesse de calcul de nos prix. »
Nous : « Bien sûr ! » clic sur le bouton
Indicateur de chargement : 🔄... 🔄... 🔄... 🔄... 🔄...
5,2 secondes plus tard : « Voici votre prix ! »
Client : « Nous allons rester avec notre solution actuelle. »
Ce jour-là, nous avons appris que personne n'attend 5 secondes pour un calcul. Voici comment nous l'avons réduit à 50ms.
L'anatomie d'un appel API Excel lent
Décomposons où ces 5 secondes partaient :
Temps de réponse original : 5 247ms
├── Analyse requête HTTP : 23ms (0,4%)
├── Authentification : 89ms (1,7%)
├── Chargement fichier Excel : 1 832ms (34,9%) ⚠️
├── Mise à jour cellules d'entrée : 467ms (8,9%)
├── Exécution du calcul : 2 234ms (42,6%) ⚠️
├── Extraction des sorties : 312ms (5,9%)
├── Formatage de la réponse : 178ms (3,4%)
└── Réponse réseau : 112ms (2,1%)Les coupables : Le chargement du fichier et l'exécution du calcul dévoraient 77,5% de notre temps.
Étape 1 : Garder Excel chaud (1 832ms → 0ms)
Le problème
Chaque appel API chargeait Excel depuis le disque :
// La méthode lente
async function calculerPrix(entrees) {
const excel = await chargerFichierExcel('tarifs.xlsx'); // 1,8 secondes !
await excel.definirEntrees(entrees);
await excel.calculer();
return excel.obtenirSorties();
}La solution : Pool de processus
// La méthode rapide
class PoolProcessusExcel {
constructor(config) {
this.processus = [];
this.disponibles = [];
this.enAttente = [];
}
async initialiser() {
// Pré-charger les processus Excel au démarrage
for (let i = 0; i < this.config.taillePool; i++) {
const processus = await this.creerProcessusExcel();
await processus.chargerClasseur(this.config.cheminClasseur);
this.processus.push(processus);
this.disponibles.push(processus);
}
}
async executer(entrees) {
// Obtenir un processus Excel déjà chargé
const processus = await this.obtenirProcessusDisponible(); // 0ms !
try {
return await processus.calculer(entrees);
} finally {
this.libererProcessus(processus);
}
}
}Résultat : Temps de chargement du fichier : 1 832ms → 0ms
Étape 2 : Mise en cache intelligente (2 234ms → 8ms pour les hits de cache)
Le problème
Recalculer des entrées identiques :
// Scénario courant : L'utilisateur ajuste la quantité
obtenirPrix({ produit: 'A', quantite: 100 }); // 2,2s
obtenirPrix({ produit: 'A', quantite: 101 }); // 2,2s
obtenirPrix({ produit: 'A', quantite: 102 }); // 2,2s
obtenirPrix({ produit: 'A', quantite: 100 }); // 2,2s (déjà vu !)La solution : Mise en cache multi-couches
class CacheIntelligent {
constructor() {
// Couche 1 : Cache en mémoire (le plus rapide)
this.cacheMemoire = new LRU({
max: 10000,
ttl: 5 * 60 * 1000 // 5 minutes
});
// Couche 2 : Cache Redis (partagé entre instances)
this.cacheRedis = new RedisClient({
ttl: 30 * 60 * 1000 // 30 minutes
});
// Couche 3 : Empreinte de calcul
this.cacheEmpreinte = new Map();
}
async obtenir(entrees) {
const cle = this.genererCle(entrees);
// Vérifier le cache mémoire d'abord (< 1ms)
const resultatMemoire = this.cacheMemoire.get(cle);
if (resultatMemoire) return resultatMemoire;
// Vérifier le cache Redis (5-10ms)
const resultatRedis = await this.cacheRedis.get(cle);
if (resultatRedis) {
this.cacheMemoire.set(cle, resultatRedis);
return resultatRedis;
}
// Vérifier si nous avons vu un calcul similaire
const empreinte = this.genererEmpreinte(entrees);
const similaire = this.cacheEmpreinte.get(empreinte);
if (similaire && this.peutReutiliserSimilaire(entrees, similaire)) {
return this.ajusterResultatSimilaire(similaire, entrees);
}
return null;
}
genererEmpreinte(entrees) {
// Empreinte intelligente pour calculs similaires
return `${entrees.produit}-${Math.floor(entrees.quantite / 10) * 10}`;
}
}Taux de réussite du cache :
- Cache mémoire : 45% de hits (< 1ms)
- Cache Redis : 30% de hits (8ms)
- Calcul frais : 25% (variable)
Étape 3 : Traitement parallèle (467ms → 89ms)
Le problème
Mises à jour séquentielles des cellules :
// Mises à jour séquentielles lentes
await excel.definirCellule('B2', entrees.quantite); // 93ms
await excel.definirCellule('B3', entrees.produit); // 93ms
await excel.definirCellule('B4', entrees.client); // 93ms
await excel.definirCellule('B5', entrees.region); // 93ms
await excel.definirCellule('B6', entrees.devise); // 93ms
// Total : 465msLa solution : Mises à jour par lots
// Mise à jour rapide par lots
class UpdateurParLots {
async mettreAJourCellules(excel, miseAJour) {
// Préparer toutes les mises à jour
const lotMiseAJour = Object.entries(miseAJour).map(([cellule, valeur]) => ({
cellule,
valeur,
type: this.detecterType(valeur)
}));
// Trier par localité pour l'efficacité du cache
lotMiseAJour.sort((a, b) => {
const ligneA = parseInt(a.cellule.substring(1));
const ligneB = parseInt(b.cellule.substring(1));
return ligneA - ligneB;
});
// Exécuter comme opération unique
await excel.miseAJourParLot(lotMiseAJour); // 89ms au total !
}
}Étape 4 : Optimisation des calculs (2 234ms → 234ms)
Le problème
Calculer l'ensemble du classeur :
// Classeur avec 50 feuilles, 10 000 formules
// Mais nous n'avons besoin que des résultats de Feuille1!A1:A10La solution : Calcul sélectif
class CalculIntelligent {
constructor(classeur) {
this.classeur = classeur;
this.grapheDependances = this.construireGrapheDependances();
}
async calculer(entrees, sortiesRequises) {
// 1. Identifier les cellules affectées
const cellulesAffectees = this.obtenirCellulesAffectees(entrees);
// 2. Trouver les dépendances des sorties requises
const dependances = this.obtenirDependances(sortiesRequises);
// 3. Calculer seulement l'intersection
const cellulesACalculer = this.intersection(cellulesAffectees, dependances);
// 4. Calcul sélectif
if (cellulesACalculer.length < 100) {
// Calculer uniquement les cellules spécifiques
await this.classeur.calculerCellules(cellulesACalculer); // 234ms
} else {
// Revenir au calcul complet
await this.classeur.calculerComplet(); // 2234ms
}
}
construireGrapheDependances() {
// Construire le graphe des dépendances de formules
const graphe = new Map();
this.classeur.formules.forEach(formule => {
const deps = this.extraireDependances(formule);
graphe.set(formule.cellule, deps);
});
return graphe;
}
}Étape 5 : Optimisation de la réponse (312ms → 47ms)
Le problème
Extraire toutes les sorties possibles :
// Extraire tout
const sorties = {
prix: excel.obtenirCellule('E10'),
remise: excel.obtenirCellule('E11'),
taxe: excel.obtenirCellule('E12'),
livraison: excel.obtenirCellule('E13'),
// ... 50 autres champs qui pourraient ne pas être nécessaires
};La solution : Chargement paresseux des sorties
// Extraction intelligente des sorties
class ExtracteurSortieParesseux {
constructor(excel, mappingSorties) {
this.excel = excel;
this.mapping = mappingSorties;
this.cache = new Map();
}
obtenirSortie() {
// Retourner un proxy qui charge à l'accès
return new Proxy({}, {
get: (target, prop) => {
if (this.cache.has(prop)) {
return this.cache.get(prop);
}
if (this.mapping[prop]) {
const valeur = this.excel.obtenirCellule(this.mapping[prop]);
this.cache.set(prop, valeur);
return valeur;
}
return undefined;
}
});
}
}
// Utilisation
const resultat = extracteur.obtenirSortie();
// Charge seulement à l'accès :
console.log(resultat.prix); // Charge E10
// Ne charge pas les autres champs sauf si nécessaireÉtape 6 : Optimisation de l'infrastructure
Distribution géographique
class DeploiementEdge {
constructor() {
this.regions = {
'eu-ouest': { url: 'https://eu-ouest.spreadapi.com', latence: 15 },
'eu-central': { url: 'https://eu-central.spreadapi.com', latence: 10 },
'us-est': { url: 'https://us-est.spreadapi.com', latence: 25 }
};
}
async executer(entrees, regionUtilisateur) {
// Router vers l'edge le plus proche
const edge = this.obtenirEdgeLePlusProche(regionUtilisateur);
// Essayer l'edge principal
try {
return await this.appelerEdge(edge, entrees);
} catch (error) {
// Repli sur le plus proche suivant
return await this.appelerEdgeSecours(regionUtilisateur, entrees);
}
}
}Pool de connexions
// Réutiliser les connexions
const sessionHttp2 = http2.connect('https://api.spreadapi.com', {
peerMaxConcurrentStreams: 100
});
// Plusieurs requêtes sur la même connexion
const requetes = entrees.map(entree =>
faireRequete(sessionHttp2, entree)
);L'architecture finale
Temps de réponse optimisé : 47ms en moyenne
├── Analyse requête : 2ms (4,3%)
├── Vérification cache : 1ms (2,1%)
├── Sélection processus : 0ms (0%)
├── Mises à jour entrées : 8ms (17%)
├── Calcul : 23ms (48,9%)
├── Extraction sorties : 5ms (10,6%)
├── Format réponse : 3ms (6,4%)
└── Réseau : 5ms (10,6%)
Temps de réponse hit cache : 8ms
├── Analyse requête : 2ms
├── Recherche cache : 3ms
├── Format réponse : 1ms
└── Réseau : 2msMétriques de performance réelles
Avant l'optimisation
- Réponse moyenne : 5 247ms
- Réponse P95 : 8 234ms
- Réponse P99 : 12 453ms
- Requêtes/seconde : 3,2
- Utilisation CPU : 95%
- Utilisation mémoire : 4,2GB
Après l'optimisation
- Réponse moyenne : 47ms (111x plus rapide)
- Réponse P95 : 89ms
- Réponse P99 : 234ms
- Requêtes/seconde : 847 (265x plus)
- Utilisation CPU : 45%
- Utilisation mémoire : 2,8GB
Liste de contrôle d'implémentation
Gains rapides (1 jour)
- [ ] Activer le pool de processus
- [ ] Ajouter la mise en cache mémoire de base
- [ ] Grouper les mises à jour de cellules
- [ ] Activer HTTP/2
Effort moyen (1 semaine)
- [ ] Implémenter la mise en cache Redis
- [ ] Construire le graphe de dépendances
- [ ] Ajouter le calcul sélectif
- [ ] Déployer dans plusieurs régions
Avancé (1 mois)
- [ ] Mise en cache basée sur empreinte
- [ ] Pré-calcul prédictif
- [ ] Moteur de calcul Excel personnalisé
- [ ] Déploiement edge computing
Erreurs courantes à éviter
1. Sur-mise en cache
// Faux : Tout mettre en cache pour toujours
cache.set(cle, resultat, { ttl: Infinity });
// Correct : Expiration intelligente
cache.set(cle, resultat, {
ttl: resultat.estVolatile ? 60000 : 300000
});2. Sous-pooling
// Faux : Un processus pour toutes les requêtes
const pool = new PoolExcel({ taille: 1 });
// Correct : Taille basée sur la charge
const pool = new PoolExcel({
taille: Math.max(4, os.cpus().length),
tailleMax: 16
});3. Ignorer les mécanismes internes d'Excel
// Faux : Forcer le recalcul complet
excel.forcerRecalculComplet();
// Correct : Laisser Excel optimiser
excel.definirModeCalcul('automatique');
excel.activerCalculIteratif();Surveillance et débogage
Métriques clés à suivre
class MoniteurPerformance {
suivreRequete(idRequete) {
return {
debut: Date.now(),
marqueurs: new Map(),
marquer(nom) {
this.marqueurs.set(nom, Date.now());
},
terminer() {
const duree = Date.now() - this.debut;
// Envoyer au monitoring
metriques.histogramme('api.temps_reponse', duree);
metriques.incrementer('api.requetes');
// Suivre la performance du cache
if (this.marqueurs.has('cache_hit')) {
metriques.incrementer('cache.hits');
} else {
metriques.incrementer('cache.misses');
}
// Logger les requêtes lentes
if (duree > 100) {
logger.warn('Requête lente', {
idRequete,
duree,
decomposition: Array.from(this.marqueurs.entries())
});
}
}
};
}
}L'impact commercial
Retour client
Avant : « C'est précis mais trop lent pour la production. »
Après : « Plus rapide que notre application native ! »
Métriques techniques
- Erreurs de timeout API : 15% → 0%
- Attrition client due à la performance : 30% → 2%
- Coûts d'infrastructure : Réduits de 60%
- Bonheur des développeurs : 📈
Vos prochaines étapes
- Mesurer d'abord : Profilez vos temps de réponse API actuels
- Cueillir les fruits à portée de main : Commencez par le pool de processus et la mise en cache de base
- Itérer : Chaque optimisation s'appuie sur la précédente
- Surveiller : Suivez les améliorations et les régressions
Rappelez-vous : Les utilisateurs attendent des réponses instantanées. 5 secondes pourraient aussi bien être l'éternité. Mais 50ms ? C'est le point idéal où les calculs Excel semblent instantanés.
Rendez vos API Excel rapides avec SpreadAPI - Nous avons déjà fait l'optimisation pour vous.