Best practice per l'utilizzo di IndexedDB

Scopri le best practice per la sincronizzazione dello stato delle applicazioni tra IndexedDB, una popolare libreria di gestione dello stato.

Quando un utente carica per la prima volta un sito web o un'applicazione, spesso è necessaria una discreta quantità di lavoro per creare lo stato iniziale dell'applicazione utilizzato per eseguire il rendering dell'interfaccia utente. Ad esempio, a volte l'app deve autenticare il lato client dell'utente ed effettuare diverse richieste API prima di avere tutti i dati necessari da visualizzare sulla pagina.

L'archiviazione dello stato dell'applicazione in IndexedDB può essere un ottimo modo per accelerare il tempo di caricamento per visite ripetute. L'app può quindi sincronizzarsi con qualsiasi servizio API in background e aggiornare l'UI con nuovi dati in modo lento, utilizzando una strategia inattiva durante la riconvalida.

Un altro buon uso di IndexedDB è per archiviare contenuti generati dall'utente, come un archivio temporaneo prima di essere caricati sul server o come cache lato client di dati remoti o, ovviamente, entrambi.

Tuttavia, quando si utilizza IndexedDB, ci sono molti aspetti importanti da considerare che potrebbero non essere immediatamente ovvi per gli sviluppatori che non hanno mai utilizzato le API. Questo articolo risponde a domande comuni e illustra alcuni degli aspetti più importanti da tenere a mente quando i dati vengono conservati in IndexedDB.

Mantieni la tua app prevedibile

Molte delle complessità relative a IndexedDB derivano dal fatto che ci sono così tanti fattori su cui tu (lo sviluppatore) non hai alcun controllo. Questa sezione esplora molti dei problemi che devi tenere a mente quando lavori con IndexedDB.

Non tutto può essere archiviato in IndexedDB su tutte le piattaforme

Se archivi file di grandi dimensioni generati dagli utenti, come immagini o video, puoi provare a archiviarli come oggetti File o Blob. Questa operazione funzionerà su alcune piattaforme, ma non su altre. Safari su iOS, in particolare, non può archiviare Blob in IndexedDB.

Fortunatamente non è troppo difficile convertire un Blob in un ArrayBuffer e viceversa. L'archiviazione di ArrayBuffer in IndexedDB è molto supportata.

Ricorda, tuttavia, che un Blob ha un tipo MIME mentre un ArrayBuffer non lo ha. Dovrai archiviare il tipo insieme al buffer per eseguire correttamente la conversione.

Per convertire un ArrayBuffer in un Blob, basta usare il costruttore Blob.

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

L'altra direzione è leggermente più complessa ed è un processo asincrono. Puoi utilizzare un oggetto FileReader per leggere il blob come ArrayBuffer. Al termine della lettura, viene attivato un evento loadend sul lettore. Puoi racchiudere questa procedura in un Promise in questo modo:

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

La scrittura nello spazio di archiviazione potrebbe non riuscire

Gli errori durante la scrittura in IndexedDB possono verificarsi per una serie di motivi e, in alcuni casi, questi motivi esulano dal tuo controllo in qualità di sviluppatore. Ad esempio, alcuni browser al momento non consentono di scrivere su IndexedDB quando è attiva la modalità di navigazione privata. Esiste anche la possibilità che un utente si trovi su un dispositivo che ha quasi esaurito lo spazio su disco e il browser ti impedisce di archiviare qualsiasi tipo di dati.

Per questo motivo, è fondamentale implementare sempre una corretta gestione degli errori nel codice IndexedDB. Ciò significa anche che in genere è consigliabile mantenere lo stato dell'applicazione in memoria (oltre a memorizzarla), in modo che la UI non si interrompa quando viene eseguita in modalità di navigazione privata o quando non è disponibile spazio di archiviazione (anche se alcune delle altre funzionalità dell'app che richiedono spazio di archiviazione non funzionano).

Puoi individuare gli errori nelle operazioni IndexedDB aggiungendo un gestore di eventi per l'evento error ogni volta che crei un oggetto IDBDatabase, IDBTransaction o IDBRequest.

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

I dati archiviati potrebbero essere stati modificati o eliminati dall'utente

A differenza dei database lato server, in cui puoi limitare l'accesso non autorizzato, i database lato client sono accessibili alle estensioni del browser e agli strumenti per sviluppatori e possono essere cancellati dall'utente.

Sebbene sia raro che gli utenti modifichino i propri dati archiviati localmente, è abbastanza comune per gli utenti cancellarli. È importante che l'applicazione possa gestire entrambi questi casi senza errori.

I dati archiviati potrebbero non essere aggiornati

Analogamente alla sezione precedente, anche se l'utente non ha modificato i dati stesso, è anche possibile che i dati nello spazio di archiviazione siano stati scritti da una versione precedente del codice, forse una versione con bug.

IndexedDB offre supporto integrato per le versioni dello schema e l'upgrade tramite il metodo IDBOpenDBRequest.onupgradeneeded(); tuttavia, devi comunque scrivere il codice di upgrade in modo che possa gestire l'utente proveniente da una versione precedente (inclusa una versione con un bug).

I test delle unità possono essere molto utili in questo caso, dato che spesso non è possibile testare manualmente tutti i percorsi e i casi di upgrade possibili.

Mantenere le prestazioni dell'app

Una delle caratteristiche principali di IndexedDB è la sua API asincrona, ma questo non ti inducono a pensare di non doverti preoccupare delle prestazioni quando lo usi. In alcuni casi, un utilizzo non corretto può comunque bloccare il thread principale, il che può causare jank e mancata reattività.

Come regola generale, le letture e le scritture in IndexedDB non devono essere superiori a quanto richiesto per i dati a cui si accede.

Sebbene IndexedDB consenta di archiviare oggetti nidificati e di grandi dimensioni come un singolo record (e farlo è sicuramente molto pratico dal punto di vista degli sviluppatori), questa pratica dovrebbe essere evitata. Il motivo è che, quando IndexedDB archivia un oggetto, deve prima creare un clone strutturato di quell'oggetto e il processo di clonazione strutturato avviene sul thread principale. Più grande è l'oggetto, più lungo sarà il tempo di blocco.

Ciò presenta alcune sfide quando si pianifica la modalità di mantenimento dello stato dell'applicazione in IndexedDB, poiché la maggior parte delle librerie di gestione dello stato più diffuse (come Redux) funzionano gestendo l'intero albero dello stato come un singolo oggetto JavaScript.

Sebbene la gestione dello stato in questo modo abbia molti vantaggi (ad esempio, semplifica il ragionamento e il debug del codice), anche se archiviare l'intero albero di stato come singolo record in IndexedDB può essere allettante e conveniente, eseguire questa operazione dopo ogni modifica (anche se limitata/debounceta) comporterà un blocco non necessario del thread principale, ma aumenterà le probabilità di errori di scrittura o anche in alcuni casi il browser potrebbe arrestarsi in modo anomalo.

Anziché archiviare l'intero albero di stati in un unico record, dovresti suddividerlo in record singoli e aggiornare solo i record che effettivamente cambiano.

Lo stesso vale se archivi elementi di grandi dimensioni come immagini, musica o video in IndexedDB. Archivia ogni elemento con la propria chiave anziché all'interno di un oggetto più grande, in modo da poter recuperare i dati strutturati senza pagare il costo del recupero del file binario.

Come per la maggior parte delle best practice, questa non è una regola del tutto o niente. Nei casi in cui non è possibile suddividere un oggetto di stato e scrivere semplicemente il set di modifiche minimo, suddividendo i dati in sottoalberi e scrivendoli è comunque preferibile scrivere sempre l'intero albero di stato. Piccoli miglioramenti sono meglio che nessun miglioramento.

Infine, dovresti sempre misurare l'impatto sulle prestazioni del codice che scrivi. Sebbene sia vero che le scritture di piccole dimensioni su IndexedDB avranno prestazioni migliori di quelle di grandi dimensioni, questo è importante solo se le scritture su IndexedDB che la tua applicazione sta effettivamente portando a attività lunghe che bloccano il thread principale e peggiorano l'esperienza utente. È importante effettuare misurazioni in modo da capire per cosa vuoi ottimizzare.

Conclusioni

Gli sviluppatori possono sfruttare i meccanismi di archiviazione dei client come IndexedDB per migliorare l'esperienza utente della loro applicazione non solo mantenendo lo stato tra una sessione e l'altra, ma anche riducendo il tempo necessario per caricare lo stato iniziale in visite ripetute.

Sebbene l'utilizzo corretto di IndexedDB possa migliorare notevolmente l'esperienza utente, l'utilizzo non corretto o la mancata gestione dei casi di errore può portare ad app non funzionanti e utenti insoddisfatti.

Poiché lo spazio di archiviazione del client coinvolge molti fattori al di fuori del tuo controllo, è fondamentale che il tuo codice sia ben testato e gestisca correttamente gli errori, anche quelli che inizialmente possono sembrare improbabili.