Cache frontend: localStorage, sessionStorage e IndexedDB

frontend cache header


Ogni giorno interagiamo con centinaia di applicazioni e webapp: apriamo un sito di notizie e ci aspettiamo di vedere immediatamente le news da tutto il mondo, apriamo un social network ed ecco un carosello di post di ogni genere. In tutto questo non pensiamo mai a quanto sia veloce ottenere informazioni da qualsiasi parte del mondo. In pochi secondi o meno la nostra singola richiesta raggiunge un server e ci viene data una risposta. Queste tempistiche sono possibili grazie a molti aspetti, uno dei quali è il grande tema delle cache.

Questo articolo esplora le tre principali tecnologie di archiviazione lato client: localStorage, sessionStorage e IndexedDB.

Nel panorama dello sviluppo web moderno, la gestione efficiente dei dati lato client è diventata una componente fondamentale per creare applicazioni performanti e reattive.

Le applicazioni web di ogni genere necessitano di strategie di caching ben definite per ridurre le chiamate al backend e migliorare l’esperienza utente.

Caching frontend: quando come e perchè

La cache applicativa frontend rappresenta un insieme di tecniche per memorizzare temporaneamente dati sul dispositivo dell’utente. In un’applicazione web, questa strategia è particolarmente vantaggiosa per diverse ragioni

Vantaggi

  • Riduzione delle chiamate HTTP al backend
  • Esperienza utente più fluida con tempi di risposta immediati
  • Funzionalità offline o parzialmente offline
  • Diminuzione del carico sul server e risparmio di risorse

Quando utilizzare le cache?

  • Quando i dati cambiano raramente (Esempio: configurazioni generali, dizionari, enumerable)
  • Dati richiesti spesso con una bassa frequenza di aggiornamento (Esempio: lista delle ultime attività pianificate, lista degli ultimi todo,..)
  • Quando dobbiamo memorizzare temporaneamente degli stati applicativi (Esempio: filtri o step di un wizard)
  • Quando vogliamo mostrare all’utente un dato immediato, caricando in background i dati più aggiornati per poi andare.

Considerazioni importanti:

  • La durata del dato in cache va gestita: ogni tipologia ha una durata differente, inoltre dobbiamo preoccuparci di gestire un refresh in funzione del dato che memorizziamo. Esempio: se memorizziamo i dati di un enumerable bisognerà gestire un refresh dei dati memorizzati con un meccanismo di scadenza dei dati
  • Bisogna scrivere una logica aggiuntiva che vada a leggere il dato in cache anziché richiederlo al backend. In applicazioni Angular questa logica può essere implementata nei service
  • A differenza di una cache nel backend, dove questa è condivisa per tutte le richieste in arrivo dai client, la cache frontend è univoca per browser. Inoltre quando l’utente pulità che famose “cache” del browser, anche tutti i nostri dati memorizzati andranno persi e dovranno essere richiesti nuovamente al backend. E’ quindi importante testare ogni casistica in fase di sviluppo, gestendo anche i casi in cui la cache è vuota.

LocalStorage

Il localStorage offre uno storage persistente che sopravvive anche dopo la chiusura del browser, rendendolo ideale per dati che devono essere mantenuti a lungo termine, per esempio un token o delle informazioni di configurazione.

A prescindere dal framework le api del webstorage ci mettono a disposizione alcuni metodi per andare ad inserire, rimuovere e aggiornare i dati:

JavaScript
localStorage.setItem('my-key-item', payload);
localStorage.getItem('my-key-item');
localStorage.removeItem('my-key-item');
localStorage.clear();

Qui vediamo un esempio contestualizzato in un’applicazione Angular:

TypeScript
// Servizio Angular per gestire il localStorage
@Injectable({
providedIn: 'root'
})
export class LocalStorageService {

  saveUserPreferences(preferences: UserPreferences): void {
    localStorage.setItem('userPreferences', JSON.stringify(preferences));
  }
  
  getUserPreferences(): UserPreferences | null {
    const data = localStorage.getItem('userPreferences');
    return data ? JSON.parse(data) : null;
  }
  
  clearUserPreferences(): void {
    localStorage.removeItem('userPreferences');
  }
}

Limitazioni

  • Spazio limitato (tipicamente 5MB)
  • E’ possibile memorizzare solamente stringhe di testo (richiede serializzazione/deserializzazione tramite JSON.stringify(payload) )
  • Le API di accesso ai dati sono sincrone.
  • Nessun supporto per query complesse
  • Da utilizzare per la memorizzazione di oggetti semplici. Più il dato memorizzato sarà complesso, più la serializzazione/deserializzazione ci impiegherà tempo, più l’esperienza utente sarà peggiore (perchè il processo è sincrono)

SessionStorage

Il sessionStorage è simile al localStorage ma mantiene i dati solo per la durata della sessione di navigazione e viene cancellato alla chiusura della scheda o finestra. Un esempio di utilizzo può essere il salvataggio temporaneo dei dati inseriti nei filtri di ricerca, o il salvataggio dello stato temporaneo di un processo non completato.

Le api sono le stesse viste prima, con la differenza che utilizziamo sessionStorage anziché localStorage:

JavaScript
sessionStorage.setItem('my-key-item', payload);
sessionStorage.getItem('my-key-item');
sessionStorage.removeItem('my-key-item');
sessionStorage.clear();

Implementazione in Angular:

TypeScript
@Injectable({
providedIn: 'root'
})
export class WizardStateService {

  saveFilters(pageName: string, filtersData: FormFilters ): void {
    sessionStorage.setItem(filters_${pageName}, JSON.stringify(filtersData));
  }
  
  getFilters(pageName: string): FormFilters | null {
    const data = sessionStorage.getItem(filters_${pageName});
    return data ? JSON.parse(data) : null;
  }
}

Limitazioni

  • Spazio limitato (5-10MB)
  • E’ possibile memorizzare solamente stringhe di testo (richiede serializzazione/deserializzazione tramite JSON.stringify(payload) )
  • Le API di accesso ai dati sono sincrone
  • Nessun supporto per query complesse
  • Da utilizzare per la memorizzazione di oggetti semplici. Più il dato memorizzato sarà complesso, più la serializzazione/deserializzazione ci impiegherà tempo, più l’esperienza utente sarà peggiore (perchè il processo è sincrono)

IndexedDB

IndexedDB rappresenta la soluzione più avanzata tra le tecnologie di storage lato client, offrendo un vero e proprio database NoSQL nel browser. A differenza del local/session storage, l’indexDB ci permette di:

  • Salvare grandi quantità di dati strutturati
  • Le api di accesso ai dati sono asincrone
  • Qualsiasi inserimento dei dati avviene tramite una transazione
  • Permette di salvare qualsiasi tipologia di dato, anche blob storage

Considerato che le API di accesso sono molte, vi rimando alla documentazione per ulteriori approfondimenti.
In Angular possiamo sfruttare le API native per poter gestire l’indexDB, oppure è possibile utilizzare delle librerie molto valide, come ad esempio Dixie.js o ngx-indexed-db.

A prescindere dalla libreria scelta è importante capire quando andare ad utilizzare questa tipologia di storage:

  • Quando ho tanti dati da memorizzare
  • Quando ho dei dati complessi da memorizzare
  • Quando ho la necessità di eseguire delle query su dati memorizzati

Esempi reali di utilizzo

  • In una applicazione che ho sviluppato il menù utente era calcolato a livello di backend. Il frontend (Angular), dopo il login dell’utente, chiedeva al backend il menù: una volta ottenuto il json veniva storicizzato in un object store (possiamo vederla come una “tabella” del database). Il service Angular che si occupava di gestire il caricamento in cache o chiamando l’api gateway del backend.
  • Sempre nella stessa applicazione, ogni model aveva un “contratto” che ne descriveva le proprietà di ogni property (es. se erano ricercabili, se dovevano essere mostrate in lista, eventuali validazioni,..). Questa configurazione era standard per ogni installazione e cambiava molto raramente. Ogni volta che l’applicazione interagiva con un modello si preoccupava di recuperare il “contratto” per applicare le logiche applicative: al primo giro era richiesto al backend, dal secondo giro in poi il tutto era memorizzato nell’indexdb e l’accesso all’informazione transitava solamente da qui. I meccanismi di refresh del contratto erano gestiti tramite eventi SignalR in arrivo dal backend.

Confronto diretto: quale tecnologia scegliere?

CaratteristicalocalStoragesessionStorageIndexedDB
PersistenzaPermanenteSolo per sessionePermanente
Capacità5MB5-10MBCentinaia di MB
Complessità implementazioneBassaBassaMedia/Alta
Tipi di datiSolo stringheSolo stringheQualsiasi tipo
OperazioniSincroneSincroneAsincrone
Supporto queryNoNo
  1. Scegliamo il localStorage se:
    • Hai bisogno di persistenza tra sessioni
    • I dati sono semplici e limitati
    • Vuoi un’implementazione veloce e diretta
    • Esempio: token JWT, preferenze utente, tema dell’interfaccia
  2. Scegliamo il sessionStorage se:
    • I dati servono solo nella sessione corrente
    • Vuoi evitare conflitti tra schede diverse
    • Esempio: stato avanzamento wizard, dati form temporanei, filtri impostati,..
  3. Scegliamo l’IndexedDB se:
    • Hai dataset voluminosi o complessi
    • Necessiti di query e indici
    • Sviluppi funzionalità offline avanzate
    • Esempio: sincronizzazione di cataloghi prodotti, documenti, dati applicativi complessi

Considerazioni sulla sicurezza

Quando si utilizzano meccanismi di storage client è essenziale considerare alcuni aspetti inerenti al tema della sicurezza:

  • Evitare di salvare dati personali identificabili in localStorage/sessionStorage poiché saranno sempre visibili dall’utente. Non saranno però visibili da altre webapp, ogni sito web avrà accesso ad un suo indexDB dedicato.
  • Se è necessario salvare dei dati cifrati bisognerà sfruttare le Web Crypto API e scriversi un metodo che si occupi di cifratura/decifratura dei dati (o ancora meglio sfruttare una libreria già esistente).
  • Sanitizzare sempre i dati prima di inserirli nel DOM (questa è una best practice generica, a prescindere da dove mi arrivi il dato)
  • Utilizzare [innerHTML] solo quando necessario e con sanitizzazione (e sfruttare la sicurezza integrata di Angular contro attacchi XSS)

Conclusioni

La scelta della giusta strategia di storage lato client per le applicazioni web dipende dalle specifiche esigenze del progetto, spero che questo articolo ti abbia chiarito maggiormente le idee, l’obiettivo è di aver bene chiaro cosa abbiamo a disposizione.
Ricorda che una buona strategia di caching frontend non è solo una questione di performance, ma anche di architettura complessiva dell’applicazione. Integrando correttamente queste tecnologie nell’ecosistema della tua applicazione, otterrai webapp più veloci, reattive e resistenti ai problemi di connettività.

Condividi questo articolo
Shareable URL
Post precedente

Aggiungere un server MCP ad un progetto esistente

Prosimo post

Angular: da environment.ts ad entpoint http

Leggi il prossimo articolo