Blog
Performance
18 febbraio 202610 min

Egress Supabase in cache: -90% di larghezza di banda nel 2026

Una mattina, ho aperto la mia dashboard Supabase e ho quasi sputato il caffè. L'egress — la larghezza di banda in uscita dal mio database — stava crescendo molto più velocemente del numero di utenti di TAMSIV. Ogni apertura dell'app scatenava decine di richieste. E ogni richiesta significa dati in transito. Moltiplica questo per centinaia di utenti che aprono l'app più volte al giorno, e ottieni una fattura che fa male.

Sono uno sviluppatore solista. Non ho un budget infrastrutturale illimitato. Ogni euro conta. Così mi sono rimboccato le maniche e ho cercato come ridurre drasticamente questo consumo senza degradare l'esperienza utente. Spoiler: sono riuscito a ridurre l'egress dell'80-90%. E in più, l'app è diventata più veloce.

Punti chiave da ricordare:
- Il problema N+1 può far esplodere la tua larghezza di banda senza che tu te ne accorga
- Una cache a due livelli (memoria + persistente) elimina la maggior parte delle richieste inutili
- Supabase Realtime permette di invalidare la cache in tempo reale senza polling
- Il batching delle richieste trasforma 20 chiamate in 1 sola
- Ottimizzare i costi e ottimizzare l'UX è spesso lo stesso lavoro

Perché l'egress di Supabase esplode su un'app mobile?

Prima di parlare di soluzioni, dobbiamo capire il problema. Supabase, come qualsiasi servizio cloud, fattura la larghezza di banda in uscita. Ogni volta che la tua app effettua una richiesta al database, i dati transitano dal server al client. Questo è l'egress.

Su un'app web classica, l'utente carica una pagina e il gioco è fatto. Su un'app mobile, è diverso. L'utente naviga tra le schede, fa pull-to-refresh, apre un'attività, torna al feed, apre un'altra attività. Ogni navigazione innesca richieste. E se il tuo codice non è ottimizzato, ogni richiesta ricarica tutti i dati, anche se nulla è cambiato negli ultimi 30 secondi.

In TAMSIV, avevo due problemi principali che moltiplicavano l'egress per un fattore enorme.

Sviluppatore che analizza una dashboard che mostra la riduzione dei costi della larghezza di banda del server
Il momento in cui ti rendi conto che la tua dashboard dei costi assomiglia a una curva di crescita di una startup — tranne che è la tua fattura.

Cos'è il problema N+1 e come rilevarlo?

Il primo colpevole era il classico problema N+1. Nel feed di TAMSIV, mostro un elenco di attività con le loro statistiche di visualizzazione (chi ha visto l'attività, quando, quante volte). Per ogni attività, facevo una chiamata individuale per recuperare le sue ViewStats.

20 attività visualizzate = 20 richieste individuali. 50 attività = 50 richieste. Vedi il problema. Ogni richiesta ha un costo fisso in termini di latenza di rete e dati trasferiti (header HTTP, metadati di risposta, ecc.). Moltiplica questo per il numero di attività, e l'egress esplode.

Il peggio è che questo pattern è invisibile se non guardi le tue metriche. L'app funziona. I dati vengono visualizzati. Tutto sembra normale. Ma in background, stai facendo 20 volte più richieste del necessario.

Come rilevarlo? In Supabase, vai nella dashboard, sezione Reports > API. Guarda il numero di richieste per endpoint. Se vedi un endpoint chiamato decine di volte nello stesso secondo, è un N+1. Puoi anche usare gli strumenti di ispezione Supabase per analizzare le richieste lente.

Come funziona il batching delle richieste con Supabase?

La soluzione al problema N+1 è semplice in teoria: invece di fare N richieste individuali, ne fai una sola che recupera tutti i dati in una volta. Questo è il batching.

Ho creato una funzione RPC in Supabase, getTaskViewStatsBatch(taskIds), che prende un array di ID come parametro e restituisce le statistiche di tutte le attività in una singola richiesta. Lo stesso vale per i memo con getMemoViewStatsBatch(memoIds).

-- Prima: 20 chiamate individuali
SELECT * FROM view_stats WHERE task_id = 'xxx';
-- x 20 volte...

-- Dopo: 1 sola chiamata
SELECT * FROM view_stats WHERE task_id = ANY($1);
-- $1 = array di 20 ID

Il risultato è immediato: 20 richieste diventano 1. L'egress per questa operazione è diviso per un fattore significativo — non esattamente per 20 perché i dati stessi non sono cambiati, ma l'overhead di rete (header, handshake, ecc.) viene eliminato 19 volte su 20.

Se usi le funzioni RPC di Supabase, il batching è banale da implementare. L'operatore ANY() di PostgreSQL è il tuo migliore amico.

Cos'è il ContentCacheService e perché ne abbiamo bisogno?

Il batching ha risolto il problema N+1, ma rimaneva il secondo colpevole: i dati ricaricati integralmente ad ogni navigazione, anche se nulla era cambiato.

Quando l'utente apre il feed, le attività vengono caricate da Supabase. Quando va sulla scheda Agenda e poi torna al feed, le attività vengono ricaricate da Supabase. Gli stessi dati, la stessa risposta, lo stesso costo in egress. Per niente.

La soluzione: una cache intelligente. Ho progettato il ContentCacheService, un singleton che gestisce la cache di tutti i dati di contenuto in TAMSIV. Il suo principio è semplice: non rifare mai una richiesta se i dati non sono cambiati.

Sala server con cavi in fibra ottica e indicatori luminosi blu e verdi
Ogni byte che esce da questi server ha un costo. La cache riduce drasticamente questo traffico in uscita.

Come implementare una cache a due livelli in un'app React Native?

Il ContentCacheService utilizza una cache a due livelli, ognuno con un ruolo preciso:

  • L1 — Cache in memoria (Map JavaScript): Accesso istantaneo, latenza zero. I dati sono in RAM. Quando l'utente naviga tra le schede, il feed viene visualizzato dal L1 senza alcuna richiesta. Il problema: i dati scompaiono quando l'app viene chiusa.
  • L2 — AsyncStorage: Cache persistente sul dispositivo. Più lenta del L1 (pochi millisecondi di lettura), ma sopravvive ai riavvii dell'app. Quando l'utente riapre TAMSIV, i dati del L2 vengono caricati in L1 e il feed viene visualizzato immediatamente, ancor prima che parta la prima richiesta Supabase.

Il flusso di lettura è il seguente:

  1. Cerca nel L1 (Map in memoria). Se trovato e non scaduto → restituisci immediatamente.
  2. Altrimenti, cerca nel L2 (AsyncStorage). Se trovato e non scaduto → copia in L1 e restituisci.
  3. Altrimenti, richiesta Supabase → memorizza in L1 e L2 → restituisci.

Ogni voce della cache ha un timestamp. Un TTL (Time To Live) configurabile determina quando una voce è considerata scaduta. Ma il vero game-changer è l'invalidazione in tempo reale con Supabase Realtime.

Come Supabase Realtime elimina il polling?

Il problema classico della cache è l'invalidazione. Come sapere che i dati sono cambiati senza rifare la richiesta? La soluzione ingenua è il polling: controllare ogni X secondi se qualcosa è cambiato. Ma il polling è uno spreco — fai richieste inutili il 90% del tempo.

Supabase Realtime risolve questo problema elegantemente. È un sistema di sottoscrizione in tempo reale basato sulle notifiche PostgreSQL. Ti abboni a una tabella, e Supabase ti invia un evento ogni volta che una riga viene creata, modificata o eliminata.

In TAMSIV, ho configurato due canali:

  • content-cache-tasks: ascolta i cambiamenti su privat.tasks
  • content-cache-memos: ascolta i cambiamenti su privat.memos

Quando viene rilevato un cambiamento, il ContentCacheService invalida la voce corrispondente nel L1 e nel L2. Al prossimo rendering del componente React, i dati vengono ricaricati da Supabase e rimessi in cache. I componenti che ascoltano i cambiamenti tramite listener si ri-renderizzano automaticamente con i nuovi dati.

Il risultato: zero polling, zero richieste inutili. I dati sono sempre freschi senza sprecare larghezza di banda. Questo è esattamente il pattern che uso anche nel feed di gamification e nei gruppi collaborativi.

Qual è l'impatto reale sui costi e sulle prestazioni?

I numeri parlano da soli. Dopo l'implementazione del ContentCacheService + batching:

  • Egress ridotto dell'80-90% a seconda dei periodi e del numero di utenti attivi
  • Numero di richieste API diviso per 15-20 grazie al batching + cache
  • Tempo di visualizzazione del feed: quasi istantaneo dalla cache L1, contro 200-500ms prima
  • Ricaricamento dopo la chiusura: ~50ms dalla cache L2, contro 300-800ms da Supabase

Il bonus inatteso: l'UX è migliorata considerevolmente. Il feed viene visualizzato istantaneamente, le transizioni tra le schede sono fluide, e il pull-to-refresh è diventato un vero aggiornamento (che ricarica solo ciò che è cambiato) invece di un ricaricamento completo.

Telefono cellulare che mostra un'applicazione veloce con contenuto in cache accanto a un laptop
L'esperienza utente beneficia direttamente della cache: il contenuto viene visualizzato istantaneamente.

Come applicare questa strategia al tuo progetto?

Se usi Supabase (o qualsiasi backend cloud) e il tuo egress inizia a salire, ecco il processo che raccomando:

  1. Verifica le tue richieste: Usa la dashboard Supabase o uno strumento di monitoraggio per identificare gli endpoint più chiamati. Cerca i pattern N+1.
  2. Raggruppa le richieste ripetitive: Tutto ciò che fa una richiesta per elemento in un elenco deve essere convertito in una singola richiesta con un array di ID.
  3. Implementa una cache a due livelli: Memoria per la velocità, storage persistente per la sopravvivenza ai riavvii.
  4. Usa Realtime per l'invalidazione: Niente polling. I dati ti avvisano quando cambiano.
  5. Misura prima e dopo: Senza metriche, non sai se la tua ottimizzazione ha davvero funzionato.

Questo approccio non è specifico per React Native o Supabase. Il pattern cache L1/L2 + invalidazione basata su eventi funziona con Firebase, AWS AppSync, o qualsiasi backend che supporti le notifiche in tempo reale.

Quali errori evitare durante l'ottimizzazione della cache?

Ho commesso alcuni errori lungo il percorso. Ecco cosa ho imparato:

  • Non memorizzare dati sensibili in AsyncStorage: AsyncStorage non è crittografato per impostazione predefinita su Android. Per i dati sensibili, usa un archivio sicuro. Nel mio caso, le attività e i memo non sono dati critici di per sé (i token di autenticazione sono in un keychain sicuro).
  • Gestire la dimensione della cache: Senza limiti, la cache L2 può crescere indefinitamente. Ho implementato una politica di eliminazione LRU (Least Recently Used) e una dimensione massima.
  • Attenzione agli URL firmati: Le immagini generate dall'IA in TAMSIV utilizzano URL firmati Supabase che scadono dopo 1 ora. La cache deve memorizzare il storage_path e rigenerare l'URL quando necessario, non memorizzare l'URL firmato stesso.
  • Testare la cache vuota: L'esperienza al primo avvio (cache vuota) deve rimanere corretta. Non dare per scontato che la cache conterrà sempre dati.

Come interagisce la cache con gli altri servizi di TAMSIV?

Il ContentCacheService non è isolato. Interagisce con diversi altri componenti dell'architettura di TAMSIV:

  • GamificationService: Utilizza la cache per visualizzare punti, badge e streak senza richieste aggiuntive. Il metodo refreshFeedImageUrls() rigenera gli URL firmati scaduti.
  • Pipeline vocale: Quando l'IA crea un'attività tramite il dittafono, l'evento Realtime invalida la cache e il feed si aggiorna automaticamente.
  • Agenda collaborativa: Gli eventi condivisi utilizzano lo stesso pattern di cache con invalidazione Realtime.
  • Ricerca: Il SearchService interroga prima la cache L1 prima di avviare una richiesta Supabase, rendendo la ricerca quasi istantanea per i dati già caricati.

Questo sistema di cache è diventato il pilastro invisibile delle prestazioni di TAMSIV. L'utente non lo vede mai direttamente, ma lo percepisce ad ogni interazione.

FAQ

L'egress di Supabase è davvero un problema per i piccoli progetti?

Il piano gratuito di Supabase include una generosa quota di egress. Ma se la tua app effettua molte richieste ripetitive (il che è comune sui dispositivi mobili), potresti raggiungere il limite più velocemente del previsto. È meglio ottimizzare presto che scoprire il problema in piena crescita.

La cache non rischia di visualizzare dati obsoleti?

Questo è il punto di forza di Supabase Realtime. Non appena un dato cambia nel database, un evento viene inviato al client che invalida la cache. In pratica, il ritardo tra la modifica e l'aggiornamento lato client è dell'ordine del secondo. Per un'app di produttività come TAMSIV, è impercettibile.

Perché non usare React Query o SWR invece?

React Query e SWR sono eccellenti librerie per la cache delle richieste sul web. Ma in un contesto React Native con AsyncStorage come cache persistente e Supabase Realtime per l'invalidazione, un servizio su misura offre più controllo. Il ContentCacheService gestisce i due livelli di cache, il TTL, l'eliminazione e l'invalidazione Realtime in un unico singleton coerente.

Questo pattern funziona con database diversi da Supabase?

Il principio è universale. La cache L1/L2 è indipendente dal backend. Per l'invalidazione in tempo reale, è necessario un equivalente alle notifiche PostgreSQL: Firebase Realtime Database, AWS AppSync subscriptions, o anche un semplice WebSocket personalizzato. L'importante è evitare il polling.

Qual è il costo di manutenzione di questo sistema di cache?

Una volta implementato, il ContentCacheService è molto stabile. Non l'ho praticamente toccato dalla sua implementazione, se non per aggiungere nuove entità alla cache (ad esempio, eventi dell'agenda). Il codice è un singleton con un'API chiara — gli altri servizi devono solo chiamare get() e invalidate().