Práticas recomendadas para usar o IndexedDB

Conheça as práticas recomendadas para sincronizar o estado do aplicativo entre o IndexedDB e as bibliotecas de gerenciamento de estado conhecidas.

Quando um usuário carrega um site ou app pela primeira vez, muitas vezes há uma quantidade razoável de 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 no lado do cliente e fazer várias solicitações de API antes de ter todos os dados necessários para mostrar na página.

Armazenar o estado do aplicativo no IndexedDB pode ser uma ótima maneira de acelerar o tempo de carregamento de acessos repetidos. Assim, o app poderá ser sincronizado com qualquer serviço de API em segundo plano e atualizar a interface com novos dados lentamente, empregando uma estratégia desatualizado ao revalidar.

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 de dados remotos do lado do cliente 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 novos nas APIs. Este artigo responde a perguntas comuns e discute alguns dos itens mais importantes a serem considerados ao armazenar dados no IndexedDB.

Como manter seu app previsível

Muitas complexidades relacionadas ao IndexedDB são decorrentes 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, especificamente, 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, mas uma ArrayBuffer não. Você vai 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 uma 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

Erros durante a gravação no IndexedDB podem ocorrer por vários motivos e, em alguns casos, eles estão fora do seu controle como desenvolvedor. Por exemplo, atualmente alguns navegadores não permitem a gravação no IndexedDB quando no modo de navegação privada. Também é possível que um usuário esteja em um dispositivo com quase nenhum espaço em disco, e o navegador impedirá o armazenamento de qualquer coisa.

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

Você pode detectar erros em operações do IndexedDB adicionando um manipulador de eventos para o evento error sempre que 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 é possível restringir o acesso não autorizado, os bancos de dados 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 modificar os dados armazenados localmente, é comum que os usuários os limpem. É importante que seu aplicativo consiga lidar com os 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 seu código, possivelmente uma versão com bugs.

O IndexedDB tem suporte integrado para versões de esquema e upgrade por meio do método IDBOpenDBRequest.onupgradeneeded(). No entanto, você ainda precisa escrever seu código de upgrade de modo que possa processar o usuário de uma versão anterior (inclusive uma versão com bug).

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

Como manter o desempenho do seu app

Um dos principais recursos do IndexedDB é a API assíncrona, mas não se leve a pensar que 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 do que o necessário para os dados que estão sendo acessados.

Embora o IndexedDB permita armazenar objetos grandes e aninhados como um único registro (e isso é bem conveniente do ponto de vista do desenvolvedor), essa prática precisa ser evitada. Isso acontece 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 a Redux) funciona gerenciando toda a árvore de estado como um único objeto JavaScript.

Embora o gerenciamento do estado dessa maneira tenha muitos benefícios (por exemplo, facilita a compreensão e a depuração do seu código), e o simples armazenamento de toda a árvore de estados como um único registro no IndexedDB pode ser tentador e conveniente. Fazer isso depois de cada mudança (mesmo que limitada/rejeitado) resulta no bloqueio desnecessário da linha de execução principal, aumenta a probabilidade de erros de gravação e, em alguns casos, a guia do navegador pode até falhar.

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

O mesmo acontece se você armazenar 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 a maioria das práticas recomendadas, essa não é uma regra de tudo ou nada. Nos casos em que não é viável dividir um objeto de estado e apenas gravar o conjunto mínimo de mudanças, dividir os dados em subárvores e só gravá-los ainda é preferível do que sempre gravar toda a árvore de estados. Pequenas melhorias são melhores do que nenhuma melhoria.

Por fim, sempre meça o impacto no desempenho do código que escrever. Embora seja verdade que pequenas gravações no IndexedDB terão melhor desempenho do que gravações grandes, isso só será importante se as gravações no IndexedDB que seu aplicativo estiver realizando realmente levarem a tarefas longas que bloqueiam a linha de execução principal e prejudicam a experiência do usuário. É importante medir para que você entenda para que está otimizando.

Conclusões

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

Embora o uso correto do IndexedDB possa melhorar significativamente a experiência do usuário, usá-lo incorretamente ou não conseguir processar 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 seu código seja bem testado e processe corretamente os erros, mesmo os que podem parecer improváveis no início.