Práticas recomendadas para usar o IndexedDB

Conheça as práticas recomendadas para sincronizar o estado do aplicativo entre o IndexedDB, uma biblioteca de gerenciamento de estado muito usada.

Quando um usuário carrega um site ou aplicativo pela primeira vez, geralmente há muito trabalho envolvido na construção do estado inicial do aplicativo usado para renderizar a interface. Por exemplo, às vezes, o app precisa autenticar o usuário do lado do cliente e fazer várias solicitações de API antes de ter todos os dados necessários para exibir na página.

Armazenar o estado do aplicativo no IndexedDB pode ser uma ótima maneira de acelerar o tempo de carregamento para visitas repetidas. Assim, o app pode ser sincronizado com qualquer serviço de API em segundo plano e atualizar a interface com novos dados lentamente, empregando uma estratégia descontinuada durante a revalidação.

Outro bom uso para o IndexedDB é armazenar conteúdo gerado pelo usuário, como um armazenamento temporário antes do upload para o servidor, como um cache do lado do cliente de dados remotos ou, claro, ambos.

No entanto, ao usar o IndexedDB, há muitos fatores importantes a serem considerados que podem não ser imediatamente óbvios para desenvolvedores iniciantes em APIs. Este artigo responde a perguntas comuns e discute alguns dos pontos mais importantes a serem considerados ao manter dados no IndexedDB.

Como manter seu app previsível

Muitas das complexidades do IndexedDB resultam do fato de que há muitos fatores sobre os quais você (o desenvolvedor) não tem controle. Esta seção explora muitos dos problemas que você precisa ter em mente ao trabalhar com o IndexedDB.

Nem tudo pode ser armazenado no IndexedDB em todas as plataformas

Se você estiver armazenando arquivos grandes gerados pelo usuário, como imagens ou vídeos, tente armazená-los como objetos File ou Blob. Isso funciona em algumas plataformas, mas falha em outras. O Safari no iOS, em especial, não pode armazenar Blobs no IndexedDB.

Felizmente, não é muito difícil converter um Blob em um ArrayBuffer e vice-versa. O armazenamento de ArrayBuffers no IndexedDB é muito compatível.

No entanto, lembre-se de que uma Blob tem um tipo MIME, e uma ArrayBuffer não. Você precisará armazenar o tipo junto com o buffer para fazer a conversão corretamente.

Para converter um ArrayBuffer em um Blob, basta usar o construtor Blob.

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

A outra direção é um pouco mais complexa e é um processo assíncrono. É possível usar um objeto FileReader para ler o blob como um ArrayBuffer. Quando a leitura é concluída, um evento loadend é acionado no leitor. É possível unir esse processo em um Promise da seguinte forma:

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);
  });
}

A gravação no armazenamento pode falhar

Os erros ao gravar no IndexedDB podem ocorrer por vários motivos e, em alguns casos, eles estão fora do seu controle como desenvolvedor. Por exemplo, alguns navegadores atualmente não permitem a gravação no IndexedDB no modo de navegação privada. Também existe a possibilidade de um usuário estar em um dispositivo que está quase sem espaço em disco, e o navegador vai impedir que você armazene qualquer coisa.

Por isso, é extremamente importante que você sempre implemente o tratamento de erros adequado no código do IndexedDB. Isso também significa que geralmente é uma boa ideia manter o estado do aplicativo na memória, além de armazená-lo, para que a interface não falhe ao ser executada no modo de navegação privada ou quando o espaço de armazenamento não estiver disponível, mesmo que alguns dos outros recursos do app que exigem armazenamento não funcionam.

Para detectar erros nas operações do IndexedDB, adicione um manipulador de eventos para o evento error sempre que você criar um objeto IDBDatabase, IDBTransaction ou IDBRequest.

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

Os dados armazenados podem ter sido modificados ou excluídos pelo usuário

Ao contrário dos bancos de dados do lado do servidor, em que você pode restringir o acesso não autorizado, os do lado do cliente podem ser acessados por extensões do navegador e ferramentas para desenvolvedores, e podem ser apagados pelo usuário.

Embora seja incomum os usuários modificarem os dados armazenados localmente, é bastante comum limpá-los. É importante que seu aplicativo possa lidar com esses dois casos sem gerar erros.

Os dados armazenados podem estar desatualizados

Assim como na seção anterior, mesmo que o usuário não tenha modificado os dados, também é possível que os dados armazenados tenham sido gravados por uma versão antiga do código, possivelmente uma versão com bugs.

O IndexedDB tem suporte integrado para versões de esquema e upgrade pelo método IDBOpenDBRequest.onupgradeneeded(). No entanto, ainda é necessário escrever o código de upgrade de modo que ele possa lidar com o usuário de uma versão anterior (incluindo uma versão com um bug).

Os testes de unidade podem ser muito úteis aqui, já que geralmente não é viável testar manualmente todos os caminhos e casos de upgrade possíveis.

Como manter a performance do seu app

Um dos principais recursos do IndexedDB é a API assíncrona, mas não se engane fazendo pensar que você não precisa se preocupar com o desempenho ao usá-lo. Há vários casos em que o uso inadequado ainda pode bloquear a linha de execução principal, o que pode levar a instabilidade e falta de resposta.

Como regra geral, as leituras e gravações no IndexedDB não podem ser maiores que o necessário para os dados acessados.

Embora o IndexedDB permita armazenar objetos grandes e aninhados como um único registro (e fazer isso é suficientemente conveniente do ponto de vista do desenvolvedor), essa prática precisa ser evitada. Isso ocorre porque, quando o IndexedDB armazena um objeto, ele precisa primeiro criar um clone estruturado desse objeto, e o processo de clonagem estruturado acontece na linha de execução principal. Quanto maior o objeto, maior será o tempo de bloqueio.

Isso apresenta alguns desafios ao planejar como manter o estado do aplicativo no IndexedDB, já que a maioria das bibliotecas de gerenciamento de estado conhecidas (como Redux) funciona gerenciando toda a árvore de estados como um único objeto JavaScript.

Embora gerenciar o estado dessa maneira tenha muitos benefícios (por exemplo, facilitar a compreensão do código e depurar) e, embora o simples armazenamento de toda a árvore de estado como um único registro no IndexedDB possa ser tentador e conveniente, fazer isso depois de cada mudança (mesmo que limitada/retraída) resultará no bloqueio desnecessário da linha de execução principal, mas aumentará a probabilidade de erros de gravação e, em alguns casos, a guia travará ou até mesmo causará a falha da guia.

Em vez de armazenar toda a árvore de estado em um único registro, divida-a em registros individuais e atualize apenas os que realmente mudam.

O mesmo acontece se você armazena itens grandes, como imagens, músicas ou vídeos no IndexedDB. Armazene cada item com a própria chave em vez de dentro de um objeto maior. Assim, é possível recuperar os dados estruturados sem pagar o custo de recuperar o arquivo binário.

Como na maioria das práticas recomendadas, essa não é uma regra "tudo ou nada". Nos casos em que não é viável dividir um objeto de estado e apenas gravar o conjunto de alterações mínimas, é recomendável dividir os dados em subárvores e gravá-los somente em vez de sempre gravar toda a árvore de estado. Pequenas melhorias são melhores do que nenhuma melhoria.

Por fim, é preciso sempre medir o impacto no desempenho (link em inglês) do código que você cria. Embora pequenas gravações no IndexedDB tenham um desempenho melhor do que gravações grandes, isso só importa se as gravações no IndexedDB que seu aplicativo está fazendo realmente resultarem em tarefas longas que bloqueiam a linha de execução principal e prejudicam a experiência do usuário. É importante medir para entender para que você está otimizando.

Conclusões

Os desenvolvedores podem aproveitar os mecanismos de armazenamento de clientes, como o IndexedDB, para melhorar a experiência do usuário do aplicativo, não apenas mantendo o estado entre as sessões, mas também reduzindo o tempo necessário para carregar o estado inicial em visitas repetidas.

Embora o uso correto do IndexedDB possa melhorar muito a experiência do usuário, usá-lo incorretamente ou não lidar com casos de erro pode levar a apps corrompidos e usuários insatisfeitos.

Como o armazenamento do cliente envolve muitos fatores fora do seu controle, é fundamental que o código seja bem testado e lide com os erros adequadamente, mesmo aqueles que inicialmente improváveis de ocorrer.