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, la creazione dello stato iniziale dell'applicazione da parte di un utente richiede spesso un'ingente quantità di lavoro. Ad esempio, a volte l'app deve autenticare il lato client dell'utente e poi effettuare diverse richieste API prima che contenga tutti i dati necessari da visualizzare sulla pagina.

La memorizzazione dello stato dell'applicazione in IndexedDB può essere un ottimo modo per ridurre il tempo di caricamento per le visite ripetute. L'app può quindi sincronizzarsi con qualsiasi servizio API in background e aggiornare lentamente l'interfaccia utente con nuovi dati, utilizzando una strategia instabile durante la riconvalida.

Un altro buon utilizzo di IndexedDB è l'archiviazione dei contenuti generati dagli utenti, come archivio temporaneo prima che vengano 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 descrive alcuni degli aspetti più importanti da tenere a mente quando si ripristinano i dati in IndexedDB.

Mantenere un'app prevedibile

Molte delle complessità relative a IndexedDB derivano dal fatto che ci sono così tanti fattori su cui (lo sviluppatore) non avete il controllo. Questa sezione esplora molti dei problemi da tenere a mente quando si lavora con IndexedDB.

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

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

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

Tuttavia, ricorda che Blob ha un tipo MIME, mentre ArrayBuffer no. Per eseguire correttamente la conversione, dovrai archiviare il tipo insieme al buffer.

Per convertire ArrayBuffer in Blob, è sufficiente utilizzare 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 nel lettore. Puoi racchiudere questo processo in un Promise, ad esempio:

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 diversi motivi, che non rientrano nel tuo controllo in qualità di sviluppatore. Ad esempio, alcuni browser attualmente non consentono la scrittura in IndexedDB in modalità di navigazione privata. C'è anche la possibilità che un utente utilizzi un dispositivo che ha quasi esaurito lo spazio su disco e che il browser ti limiti l'archiviazione di qualsiasi cosa.

Per questo motivo, è fondamentale implementare sempre una corretta gestione degli errori nel codice IndexedDB. Questo significa anche che è generalmente opportuno mantenere lo stato dell'applicazione in memoria (oltre all'archiviazione), in modo che l'interfaccia utente non si interrompa quando viene eseguita in modalità di navigazione privata o quando lo spazio di archiviazione non è disponibile (anche se alcune altre funzionalità dell'app che richiedono spazio di archiviazione non funzioneranno).

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 è possibile limitare gli accessi non autorizzati, quelli lato client sono accessibili alle estensioni del browser e agli strumenti per sviluppatori e possono essere cancellati dall'utente.

Per quanto possa essere raro che gli utenti modifichino i propri dati archiviati in locale, sono piuttosto comuni cancellarli. È importante che la tua applicazione sia in grado di gestire entrambi questi casi senza errori.

I dati archiviati potrebbero non essere aggiornati

Come per la sezione precedente, anche se l'utente non ha modificato i dati, è anche possibile che i dati in suo possesso siano stati scritti da una versione precedente del codice, magari una versione con bug.

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

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

Mantenere le prestazioni dell'app

Una delle funzionalità principali di IndexedDB è la sua API asincrona, ma non farti ingannare intendendoti a non preoccuparti delle prestazioni quando la utilizzi. In alcuni casi, un utilizzo non corretto può comunque bloccare il thread principale, il che può causare jank e mancata risposta.

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

Sebbene IndexedDB consenta di archiviare oggetti nidificati di grandi dimensioni come un singolo record (e farlo è sicuramente abbastanza 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 dell'oggetto e il processo di clonazione strutturata avviene sul thread principale. Più grande è l'oggetto, maggiore sarà il tempo di blocco.

Ciò presenta alcune sfide durante la pianificazione di come mantenere lo stato dell'applicazione in IndexedDB, poiché la maggior parte delle librerie di gestione dello stato più diffuse (come Redux) funziona gestendo l'intero albero dello stato come un singolo oggetto JavaScript.

Sebbene la gestione dello stato in questo modo offra molti vantaggi (ad es. semplifica il ragionamento e il debug del codice), la semplice archiviazione dell'intero albero dello stato come singolo record in IndexedDB potrebbe essere allettante e pratica; eseguire questa operazione dopo ogni modifica (anche se limitata/debattezzata) comporterà il blocco superfluo del thread principale, aumentando la probabilità che si verifichino errori di scrittura o addirittura l'arresto anomalo della scheda.

Anziché archiviare l'intero albero dello stato in un unico record, dovresti suddividerlo in singoli record 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 anche 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 sia possibile suddividere un oggetto di stato e scrivere semplicemente il set di modifiche minimo, è comunque preferibile scrivere i dati in sotto-alberi e scriverli solo se si scrive sempre l'intero albero dello stato. Piccoli miglioramenti sono meglio che nessun miglioramento.

Infine, dovresti sempre misurare l'impatto sulle prestazioni del codice che scrivi. Anche se è vero che le scritture di piccole dimensioni in IndexedDB avranno prestazioni migliori di quelle di grandi dimensioni, ciò è importante solo se le scritture su IndexedDB eseguite dalla tua applicazione portano effettivamente a attività lunghe che bloccano il thread principale e riducono l'esperienza utente. È importante misurare i risultati per capire l'obiettivo dell'ottimizzazione.

Conclusioni

Gli sviluppatori possono sfruttare meccanismi di archiviazione del client come IndexedDB per migliorare l'esperienza utente della loro applicazione non solo ripristinando lo stato tra le sessioni, 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, un utilizzo errato o la mancata gestione dei casi di errore può portare ad app non funzionanti e utenti infelici.

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