KV Storage: o primeiro módulo integrado da Web

Os fornecedores de navegadores e especialistas em desempenho da Web têm dito, durante a maior parte da última década, que o localStorage é lento e que os desenvolvedores da Web precisam parar de usá-lo.

Para ser justo, as pessoas que dizem isso não estão erradas. localStorage é uma API síncrona que bloqueia a linha de execução principal. Sempre que você a acessa, pode impedir que a página seja interativa.

O problema é que a API localStorage é muito simples, e a única alternativa assíncrona para localStorage é a IndexedDB, que, vamos ser sinceros, não é conhecida pela facilidade de uso ou pela API amigável.

Assim, os desenvolvedores precisam escolher entre algo difícil de usar e algo ruim para a performance. Embora existam bibliotecas que oferecem a simplicidade da API localStorage enquanto usam APIs de armazenamento assíncronas por trás, a inclusão de uma dessas bibliotecas no app tem um custo de tamanho de arquivo e pode consumir seu orçamento de performance.

Mas e se fosse possível ter o desempenho de uma API de armazenamento assíncrono com a simplicidade da API localStorage, sem precisar pagar o custo do tamanho do arquivo?

Talvez em breve. O Chrome está testando um novo recurso conhecido como módulos integrados. O primeiro que planejamos lançar é um módulo de armazenamento de chave/valor assíncrono chamado armazenamento KV.

Mas, antes de entrar nos detalhes do módulo de armazenamento KV, vou explicar o que quero dizer com módulos integrados.

O que são módulos integrados?

Os módulos integrados são como módulos do JavaScript, exceto que não precisam ser transferidos por download porque são enviados com o navegador.

Assim como as APIs da Web tradicionais, os módulos integrados precisam passar por um processo de padronização. Cada um deles tem sua própria especificação que exige uma revisão de design e sinais positivos de suporte de desenvolvedores da Web e de outros fornecedores de navegadores antes do lançamento. No Chrome, os módulos integrados seguem o mesmo processo de inicialização que usamos para implementar e enviar todas as novas APIs.

Ao contrário das APIs da Web tradicionais, os módulos integrados não são expostos no escopo global. Eles só estão disponíveis por meio de importações.

Não expor módulos integrados globalmente tem muitas vantagens: eles não adicionam nenhuma sobrecarga para iniciar um novo contexto de execução do JavaScript (por exemplo, uma nova guia, worker ou service worker) e não consomem memória ou CPU, a menos que sejam importados. Além disso, elas não correm o risco de nomear conflitos com outras variáveis definidas no código.

Para importar um módulo integrado, use o prefixo std: seguido pelo identificador do módulo integrado. Por exemplo, em navegadores compatíveis, é possível importar o módulo de armazenamento KV com o seguinte código (confira abaixo como usar um polyfill de armazenamento KV em navegadores sem suporte):

import storage, {StorageArea} from 'std:kv-storage';

O módulo de armazenamento KV

O módulo de armazenamento KV é semelhante à simplicidade da API localStorage, mas a forma da API é mais próxima de uma Map JavaScript. Em vez de getItem(), setItem() e removeItem(), ele tem get(), set() e delete(). Ele também tem outros métodos semelhantes a mapas que não estão disponíveis para localStorage, como keys(), values() e entries(). E, como Map, as chaves não precisam ser strings. Eles podem ser qualquer tipo estruturado serializável.

Ao contrário de Map, todos os métodos do armazenamento KV retornam promessas ou iteradores assíncronos, já que o principal objetivo desse módulo é não ser síncrono, ao contrário de localStorage. Para conferir a API completa em detalhes, consulte a especificação.

Como você pode ter notado no exemplo de código acima, o módulo de armazenamento KV tem uma exportação padrão storage e uma exportação nomeada StorageArea.

storage é uma instância da classe StorageArea com o nome 'default', e é o que os desenvolvedores vão usar com mais frequência no código do aplicativo. A classe StorageArea é fornecida para casos em que é necessário um isolamento adicional (por exemplo, uma biblioteca de terceiros que armazena dados e quer evitar conflitos com dados armazenados pela instância storage padrão). Os dados de StorageArea são armazenados em um banco de dados IndexedDB com o nome kv-storage:${name}, em que "name" é o nome da instância StorageArea.

Confira um exemplo de como usar o módulo de armazenamento KV no código:

import storage from 'std:kv-storage';

const main = async () => {
  const oldPreferences = await storage.get('preferences');

  document.querySelector('form').addEventListener('submit', async () => {
    const newPreferences = Object.assign({}, oldPreferences, {
      // Updated preferences go here...
    });

    await storage.set('preferences', newPreferences);
  });
};

main();

E se um navegador não oferecer suporte a um módulo integrado?

Se você já usou módulos JavaScript nativos em navegadores, provavelmente sabe que (pelo menos até agora) importar qualquer coisa que não seja um URL gerará um erro. E std:kv-storage não é um URL válido.

Isso levanta a questão: precisamos esperar até que todos os navegadores ofereçam suporte a módulos integrados antes de podermos usá-los no código? Felizmente, a resposta é não.

Você pode usar módulos integrados assim que um navegador os oferecer suporte, graças a outro recurso que estamos testando chamado mapas de importação.

Importar mapas

Os mapas de importação são basicamente um mecanismo em que os desenvolvedores podem associar um alias a um ou mais identificadores alternativos.

Isso é útil porque oferece uma maneira de mudar (no momento da execução) como um navegador resolve um identificador de importação específico em todo o aplicativo.

No caso de módulos integrados, isso permite referenciar um polyfill do módulo no código do aplicativo, mas um navegador compatível com o módulo integrado pode carregar essa versão.

Veja como declarar um mapa de importação para que ele funcione com o módulo de armazenamento KV:

<!-- The import map is inlined into your page -->
<script type="importmap">
{
  "imports": {
    "/path/to/kv-storage-polyfill.mjs": [
      "std:kv-storage",
      "/path/to/kv-storage-polyfill.mjs"
    ]
  }
}
</script>

<!-- Then any module scripts with import statements use the above map -->
<script type="module">
  import storage from '/path/to/kv-storage-polyfill.mjs';

  // Use `storage` ...
</script>

O ponto principal do código acima é que o URL /path/to/kv-storage-polyfill.mjs está sendo mapeado para dois recursos diferentes: std:kv-storage e, novamente, o URL original, /path/to/kv-storage-polyfill.mjs.

Portanto, quando o navegador encontra uma instrução de importação que faz referência a esse URL (/path/to/kv-storage-polyfill.mjs), ele primeiro tenta carregar std:kv-storage. Se isso não for possível, ele vai voltar a carregar /path/to/kv-storage-polyfill.mjs.

Novamente, a mágica aqui é que o navegador não precisa oferecer suporte a mapas de importação ou módulos integrados para que essa técnica funcione, já que o URL transmitido para a instrução de importação é o URL do polyfill. O polyfill não é um substituto, é o padrão. O módulo integrado é um aprimoramento progressivo.

E os navegadores que não oferecem suporte a módulos?

Para usar mapas de importação e carregar módulos integrados condicionalmente, é necessário usar instruções import, o que também significa usar scripts de módulo, ou seja, <script type="module">.

Atualmente, mais de 80% dos navegadores oferecem suporte a módulos. Para os que não oferecem, é possível usar a técnica module/nomodule para exibir um pacote legado. Ao gerar o build nomodule, você precisará incluir todos os polyfills porque sabe com certeza que os navegadores que não oferecem suporte a módulos definitivamente não oferecem suporte a módulos integrados.

Demonstração do KV Storage

Para ilustrar que é possível usar módulos integrados e ainda oferecer suporte a navegadores mais antigos, montei uma demonstração que incorpora todas as técnicas descritas acima e é executada em todos os navegadores atuais:

  • Os navegadores que oferecem suporte a módulos, mapas de importação e o módulo integrado não carregam códigos desnecessários.
  • Os navegadores que oferecem suporte a módulos e mapas de importação, mas não ao módulo integrado, carregam o polyfill de armazenamento KV (pelo carregador de módulos do navegador).
  • Os navegadores que oferecem suporte a módulos, mas não a mapas de importação, também carregam o polyfill de armazenamento KV (pelo carregador de módulos do navegador).
  • Os navegadores que não oferecem suporte a módulos recebem o polyfill de armazenamento KV no pacote legado (carregado por <script nomodule>).

A demonstração é hospedada no Glitch, então você pode acessar a fonte. Também tenho uma explicação detalhada da implementação no README. Confira como ele foi criado.

Para conferir o módulo integrado nativo em ação, carregue a demonstração no Chrome 74 ou versão mais recente com a flag experimental de recursos da plataforma da Web ativada (chrome://flags/#enable-experimental-web-platform-features).

É possível verificar se o módulo integrado está sendo carregado porque você não vai ver o script de polyfill no painel de origem no DevTools. Em vez disso, você vai ver a versão do módulo integrado. Divertido: é possível inspecionar o código-fonte do módulo ou até mesmo colocar pontos de interrupção nele:

A origem do módulo de armazenamento KV no Chrome DevTools

Envie seu feedback

Esta introdução deve ter dado uma ideia do que é possível fazer com módulos integrados. Esperamos que você esteja animado! Gostaríamos muito que os desenvolvedores testassem o módulo de armazenamento KV (assim como todos os novos recursos discutidos aqui) e nos enviassem feedback.

Confira os links do GitHub em que você pode enviar feedback sobre cada um dos recursos mencionados neste artigo:

Se o site usa localStorage, tente mudar para a API Storage KV para ver se ela atende a todas as suas necessidades. Se você se inscrever no teste de origem do KV Storage, poderá implantar esses recursos hoje mesmo. Todos os usuários vão se beneficiar de uma melhor performance de armazenamento, e os usuários do Chrome 74 ou mais recente não vão precisar pagar nenhum custo extra de download.