Cached egress Supabase : -90% de bande passante en 2026
Un matin, j'ai ouvert mon dashboard Supabase et j'ai failli recracher mon cafe. L'egress — la bande passante sortante de ma base de donnees — grimpait bien plus vite que le nombre d'utilisateurs de TAMSIV. Chaque ouverture de l'app declenchait des dizaines de requetes. Et chaque requete, c'est de la donnee qui transite. Multiplie ca par des centaines d'utilisateurs qui ouvrent l'app plusieurs fois par jour, et tu obtiens une facture qui fait mal.
Je suis dev solo. Je n'ai pas de budget infra illimite. Chaque euro compte. Alors j'ai retrousse mes manches et j'ai cherche comment reduire drastiquement cette consommation sans degrader l'experience utilisateur. Spoiler : j'ai reussi a baisser l'egress de 80 a 90%. Et en bonus, l'app est devenue plus rapide.
Points cles a retenir :
- Le probleme N+1 peut exploser ta bande passante sans que tu t'en rendes compte
- Un cache a deux niveaux (memoire + persistant) elimine la majorite des requetes inutiles
- Supabase Realtime permet d'invalider le cache en temps reel sans polling
- Le batching de requetes transforme 20 appels en 1 seul
- Optimiser les couts et optimiser l'UX, c'est souvent le meme travail
Pourquoi l'egress Supabase explose-t-il sur une app mobile ?
Avant de parler solution, il faut comprendre le probleme. Supabase, comme tout service cloud, facture la bande passante sortante. Chaque fois que ton app fait une requete a la base de donnees, les donnees transitent du serveur vers le client. C'est l'egress.
Sur une app web classique, l'utilisateur charge une page et c'est termine. Sur une app mobile, c'est different. L'utilisateur navigue entre les onglets, pull-to-refresh, ouvre une tache, revient au feed, ouvre une autre tache. Chaque navigation declenche des requetes. Et si ton code n'est pas optimise, chaque requete recharge l'integralite des donnees, meme si rien n'a change depuis 30 secondes.
Dans TAMSIV, j'avais deux problemes majeurs qui multipliaient l'egress par un facteur enorme.
Qu'est-ce que le probleme N+1 et comment le detecter ?
Le premier coupable, c'etait le classique probleme N+1. Dans le feed de TAMSIV, j'affiche une liste de taches avec leurs statistiques de vues (qui a vu la tache, quand, combien de fois). Pour chaque tache, je faisais un appel individuel pour recuperer ses ViewStats.
20 taches affichees = 20 requetes individuelles. 50 taches = 50 requetes. Tu vois le probleme. Chaque requete a un cout fixe en termes de latence reseau et de donnees transferees (headers HTTP, metadonnees de reponse, etc.). Multiplie ca par le nombre de taches, et l'egress explose.
Le pire, c'est que ce pattern est invisible si tu ne regardes pas tes metriques. L'app fonctionne. Les donnees s'affichent. Tout a l'air normal. Mais en arriere-plan, tu fais 20 fois plus de requetes que necessaire.
Comment le detecter ? Dans Supabase, va dans le dashboard, section Reports > API. Regarde le nombre de requetes par endpoint. Si tu vois un endpoint appele des dizaines de fois dans la meme seconde, c'est un N+1. Tu peux aussi utiliser les outils d'inspection Supabase pour analyser les requetes lentes.
Comment fonctionne le batching de requetes avec Supabase ?
La solution au N+1 est simple en theorie : au lieu de faire N requetes individuelles, tu en fais une seule qui recupere toutes les donnees d'un coup. C'est le batching.
J'ai cree une fonction RPC dans Supabase, getTaskViewStatsBatch(taskIds), qui prend un tableau d'IDs en parametre et retourne les stats de toutes les taches en une seule requete. Pareil pour les memos avec getMemoViewStatsBatch(memoIds).
-- Avant : 20 appels individuels
SELECT * FROM view_stats WHERE task_id = 'xxx';
-- x 20 fois...
-- Apres : 1 seul appel
SELECT * FROM view_stats WHERE task_id = ANY($1);
-- $1 = tableau de 20 IDs
Le resultat est immediat : 20 requetes deviennent 1. L'egress pour cette operation est divise par un facteur significatif — pas exactement par 20 car les donnees elles-memes n'ont pas change, mais l'overhead reseau (headers, handshake, etc.) est elimine 19 fois sur 20.
Si tu utilises les fonctions RPC de Supabase, le batching est trivial a implementer. L'operateur ANY() de PostgreSQL est ton meilleur ami.
Qu'est-ce que le ContentCacheService et pourquoi en avoir besoin ?
Le batching a regle le probleme N+1, mais il restait le deuxieme coupable : les donnees rechargees integralement a chaque navigation, meme si rien n'avait change.
Quand l'utilisateur ouvre le feed, les taches sont chargees depuis Supabase. Quand il va sur l'onglet Agenda puis revient au feed, les taches sont rechargees depuis Supabase. Les memes donnees, la meme reponse, le meme cout en egress. Pour rien.
La solution : un cache intelligent. J'ai concu le ContentCacheService, un singleton qui gere le cache de toutes les donnees de contenu dans TAMSIV. Son principe est simple : ne jamais refaire une requete si les donnees n'ont pas change.
Comment implementer un cache a deux niveaux dans une app React Native ?
Le ContentCacheService utilise un cache a deux niveaux, chacun avec un role precis :
- L1 — Cache memoire (Map JavaScript) : Acces instantane, zero latence. Les donnees sont en RAM. Quand l'utilisateur navigue entre les onglets, le feed s'affiche depuis le L1 sans aucune requete. Le probleme : les donnees disparaissent quand l'app est fermee.
- L2 — AsyncStorage : Cache persistant sur le device. Plus lent que le L1 (quelques millisecondes de lecture), mais survit aux redemarrages de l'app. Quand l'utilisateur rouvre TAMSIV, les donnees du L2 sont chargees en L1 et le feed s'affiche immediatement, avant meme que la premiere requete Supabase ne parte.
Le flow de lecture est le suivant :
- Chercher dans le L1 (Map en memoire). Si trouve et pas expire → retourner immediatement.
- Sinon, chercher dans le L2 (AsyncStorage). Si trouve et pas expire → copier en L1 et retourner.
- Sinon, requete Supabase → stocker en L1 et L2 → retourner.
Chaque entree du cache a un timestamp. Un TTL (Time To Live) configurable determine quand une entree est consideree comme perimee. Mais le vrai game-changer, c'est l'invalidation en temps reel avec Supabase Realtime.
Comment Supabase Realtime elimine-t-il le polling ?
Le probleme classique du cache, c'est l'invalidation. Comment savoir que les donnees ont change sans refaire la requete ? La solution naive, c'est le polling : verifier toutes les X secondes si quelque chose a bouge. Mais le polling, c'est du gaspillage — tu fais des requetes pour rien 90% du temps.
Supabase Realtime resout ce probleme elegamment. C'est un systeme de souscription en temps reel base sur les notifications PostgreSQL. Tu t'abonnes a une table, et Supabase te pousse un evenement chaque fois qu'une ligne est creee, modifiee ou supprimee.
Dans TAMSIV, j'ai configure deux channels :
content-cache-tasks: ecoute les changements surprivat.taskscontent-cache-memos: ecoute les changements surprivat.memos
Quand un changement est detecte, le ContentCacheService invalide l'entree correspondante dans le L1 et le L2. Au prochain rendu du composant React, les donnees sont rechargees depuis Supabase et remises en cache. Les composants qui ecoutent les changements via des listeners se re-rendent automatiquement avec les nouvelles donnees.
Le resultat : zero polling, zero requete inutile. Les donnees sont toujours fraiches sans gaspiller de bande passante. C'est exactement le pattern que j'utilise aussi dans le feed de gamification et dans les groupes collaboratifs.
Quel est l'impact reel sur les couts et la performance ?
Les chiffres parlent d'eux-memes. Apres la mise en place du ContentCacheService + batching :
- Egress reduit de 80 a 90% selon les periodes et le nombre d'utilisateurs actifs
- Nombre de requetes API divise par 15 a 20 grace au batching + cache
- Temps d'affichage du feed : quasi-instantane depuis le cache L1, contre 200-500ms avant
- Rechargement apres fermeture : ~50ms depuis le cache L2, contre 300-800ms depuis Supabase
Le bonus inattendu : l'UX s'est considerablement amelioree. Le feed s'affiche instantanement, les transitions entre onglets sont fluides, et le pull-to-refresh est devenu un vrai rafraichissement (qui ne recharge que ce qui a change) au lieu d'un rechargement complet.
Comment appliquer cette strategie a ton propre projet ?
Si tu utilises Supabase (ou n'importe quel backend cloud) et que ton egress commence a grimper, voici la demarche que je recommande :
- Audite tes requetes : Utilise le dashboard Supabase ou un outil de monitoring pour identifier les endpoints les plus appeles. Cherche les patterns N+1.
- Batch les requetes repetitives : Tout ce qui fait une requete par element dans une liste doit etre converti en une seule requete avec un tableau d'IDs.
- Implemente un cache a deux niveaux : Memoire pour la vitesse, storage persistant pour la survie aux redemarrages.
- Utilise Realtime pour l'invalidation : Pas de polling. Les donnees te previennent quand elles changent.
- Mesure avant et apres : Sans metriques, tu ne sais pas si ton optimisation a vraiment fonctionne.
Cette approche n'est pas specifique a React Native ou a Supabase. Le pattern cache L1/L2 + invalidation evenementielle fonctionne avec Firebase, AWS AppSync, ou n'importe quel backend qui supporte les notifications en temps reel.
Quelles erreurs eviter lors de l'optimisation du cache ?
J'ai fait quelques erreurs en cours de route. Voici ce que j'ai appris :
- Ne pas cacher les donnees sensibles dans AsyncStorage : AsyncStorage n'est pas chiffre par defaut sur Android. Pour des donnees sensibles, utilise un stockage securise. Dans mon cas, les taches et memos ne sont pas des donnees critiques en elles-memes (les tokens d'auth sont dans un keychain securise).
- Gerer la taille du cache : Sans limite, le cache L2 peut grossir indefiniment. J'ai implemente une politique d'eviction LRU (Least Recently Used) et une taille maximale.
- Attention aux URLs signees : Les images generees par l'IA dans TAMSIV utilisent des signed URLs Supabase qui expirent apres 1 heure. Le cache doit stocker le
storage_pathet regenerer l'URL au besoin, pas cacher l'URL signee elle-meme. - Tester le cache vide : L'experience premier lancement (cache vide) doit rester correcte. Ne pas supposer que le cache contiendra toujours des donnees.
Comment le cache interagit-il avec les autres services de TAMSIV ?
Le ContentCacheService n'est pas isole. Il interagit avec plusieurs autres composants de l'architecture de TAMSIV :
- GamificationService : Utilise le cache pour afficher les points, badges et streaks sans requetes supplementaires. La methode
refreshFeedImageUrls()regenere les URLs signees expirees. - Pipeline vocal : Quand l'IA cree une tache via le dictaphone, l'evenement Realtime invalide le cache et le feed se met a jour automatiquement.
- Agenda collaboratif : Les evenements partages utilisent le meme pattern de cache avec invalidation Realtime.
- Recherche : Le SearchService interroge d'abord le cache L1 avant de lancer une requete Supabase, ce qui rend la recherche quasi-instantanee pour les donnees deja chargees.
Ce systeme de cache est devenu le pilier invisible de la performance de TAMSIV. L'utilisateur ne le voit jamais directement, mais il le ressent a chaque interaction.
FAQ
L'egress Supabase est-il vraiment un probleme pour les petits projets ?
Le plan gratuit de Supabase inclut un quota d'egress genereux. Mais si ton app fait beaucoup de requetes repetitives (ce qui est courant sur mobile), tu peux atteindre la limite plus vite que prevu. Mieux vaut optimiser tot que decouvrir le probleme en pleine croissance.
Le cache ne risque-t-il pas d'afficher des donnees obsoletes ?
C'est tout l'interet de Supabase Realtime. Des qu'une donnee change en base, un evenement est pousse au client qui invalide le cache. En pratique, le delai entre la modification et la mise a jour cote client est de l'ordre de la seconde. Pour une app de productivite comme TAMSIV, c'est imperceptible.
Pourquoi ne pas utiliser React Query ou SWR a la place ?
React Query et SWR sont d'excellentes librairies pour le cache de requetes sur le web. Mais dans un contexte React Native avec AsyncStorage comme cache persistant et Supabase Realtime pour l'invalidation, un service sur-mesure offre plus de controle. Le ContentCacheService gere les deux niveaux de cache, le TTL, l'eviction, et l'invalidation Realtime dans un seul singleton coherent.
Ce pattern fonctionne-t-il avec d'autres bases de donnees que Supabase ?
Le principe est universel. Le cache L1/L2 est independant du backend. Pour l'invalidation en temps reel, il faut un equivalent aux notifications PostgreSQL : Firebase Realtime Database, AWS AppSync subscriptions, ou meme un simple WebSocket maison. L'important, c'est d'eviter le polling.
Quel est le cout de maintenance de ce systeme de cache ?
Une fois en place, le ContentCacheService est tres stable. Je n'y ai pratiquement pas touche depuis sa mise en place, a part pour ajouter de nouvelles entites au cache (evenements de l'agenda par exemple). Le code est un singleton avec une API claire — les autres services n'ont qu'a appeler get() et invalidate().