localStorage vs sessionStorage vs IndexedDB vs Cookies — Armazenamento no navegador sem o remorso das 3h
Um guia prático para escolher o armazenamento certo no navegador — com uma tabela de comparação, a discussão sobre JWT explicada claramente e as formas específicas pelas quais cada opção vai arruinar sua noite.
São 3h da manhã 😬. Um usuário relatou um erro: seu carrinho de compras está vazio. Você abre as ferramentas de desenvolvimento, clique na aba Aplicativo e olha para o painel lateral. Onde você colocou isso? A resposta correta depende de algumas perguntas que a maioria dos desenvolvedores só pensa depois que o erro é relatado.
Este não é um artigo de definições. Você pode encontrá-los em qualquer lugar. Este é o momento em que falamos sobre o que quebra, o que realmente usar e por que a discussão sobre o JWT em localStorage geralmente ignora o ponto principal.
Versão Resumida (a.k.a. a tabela que você irá salvar)
| localStorage | sessionStorage | IndexedDB | Biscoitos | |
|---|---|---|---|---|
| Persistência | Permanente | Sessão de aba apenas | Permanente | Configurável (sessão ou data de validade) |
| Tamanho máximo | 5–10 MB | 5–10 MB | GBs (limite do navegador) | Aproximadamente 4 KB por cookie |
| Acesso ao servidor | Não | Não | Não | Sim — enviada com cada solicitação |
| API assíncrona | Não (bloqueia a thread principal) | Não (bloqueia a thread principal) | Sim (baseada em Promise/event) | Não |
| Lê-se em JavaScript | Sim | Sim | Sim | Apenas sem o sinal HttpOnly |
| Acesso de Web Worker | Não | Não | Sim | Não |
| Compartilhado entre abas | Sim | Não — cada aba é isolada | Sim | Sim |
| Evclusão do Safari ITP | Após 7 dias sem interação | Ao fechar a aba | Após 7 dias sem interação | Depende do atributo Expires |
localStorage
Persistente, síncrona, com escopo de origem. A peça de trabalho que todos usam e que, metade das vezes, não deveria ser usada.
O que é realmente
O localStorage armazena pares chave-valor em string. Isso é tudo. O limite de armazenamento é de 5MB em maioria dos navegadores, 10MB em alguns (o Chrome dá mais). É limitado ao escopo da origem — protocolo + domínio + porta — então http://example.com e https://example.com possui armazenamento separado. Sobrevive ao fechamento da aba, reinício do navegador, tudo exceto quando o usuário limpa os dados do navegador ou você chama explicitamente localStorage.clear().
// Read/write is synchronous — it happens right now, on the main thread
localStorage.setItem('theme', 'dark');
const theme = localStorage.getItem('theme'); // 'dark'
// Storing objects? You're serializing manually.
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Alex' }));
const user = JSON.parse(localStorage.getItem('user'));
Onde quebra
- Limite excedido — gera um erro síncrono
DOMException: QuotaExceededError. Se você não envolver as escritas em try/catch, você descobrirá por meio de um relato de erro do usuário. - Privado/incógnito — os navegadores dão um localStorage fresco e isolado com um limite mais rigoroso (ou zero). O Firefox historicamente lançava erros de limite imediatamente. Nunca confie no localStorage estar disponível sem detectá-lo.
- Safari ITP — se um usuário não visitou seu site há 7 dias, o Safari pode limpar o localStorage. Isso é comportamento documentado. Surpreenderá você no pior momento.
- XSS — tudo no localStorage é lido por qualquer JavaScript executado na origem. Se um atacante consegue injetar um script, eles obtêm tudo.
Use para
Preferências de interface (modo escuro, estado do painel lateral, idioma), cache de dados não sensíveis (hora da última requisição, configuração estática), qualquer coisa que deve sobreviver a uma atualização de página, mas que não contém tokens de autenticação ou dados pessoais. Se você estiver pensando em colocar um JWT ou uma chave de API aqui — continue lendo.
sessionStorage
Tudo que o localStorage faz, mas com uma vida mais curta e uma isolamento crucial que ataca constantemente as pessoas.
A armadilha da isolamento por aba
O sessionStorage é por aba, não por navegador. Abrir a mesma página em uma nova aba dá um sessionStorage completamente separado. Isso é não obvio para os usuários, e não será óbvio para você até que alguém relatar um erro sobre os dados de um formulário multistep desaparecerem quando eles "acidentalmente" abriram uma segunda aba.
A única exceção: se o usuário abrir uma nova aba por window.open() ou clicando com o botão do meio em um link, a nova aba recebe uma cópia do sessionStorage do pai no momento de abertura. Depois disso, as duas estão isoladas. Este é o tipo de caso de uso que gera uma excelente pergunta no Stack Overflow às 2h da madrugada.
// Perfect for checkout flows — step data lives until the tab closes
sessionStorage.setItem('checkoutStep', '2');
sessionStorage.setItem('cartSnapshot', JSON.stringify(cart));
// Cleared automatically when the tab closes — no cleanup code needed
Use para
Estado de formulário multistep, dados de wizard de uso único, qualquer coisa que deve existir por uma sessão e desaparecer quando o usuário fecha a aba. É realmente útil para fluxos de checkout — você não quer que o estado parcial de checkout fique pendente de uma sessão anterior. Não use se você precisar de dados compartilhados entre abas ou páginas abertas independentemente.
IndexedDB
A opção madura. Assíncrona, com transação e capaz de armazenar objetos reais de JavaScript — não apenas strings serializadas. Também possui a API nativa mais dolorosa dos três, o que é a razão pela qual quase ninguém a usa diretamente.
O que é realmente
O IndexedDB é um armazenamento completo de chave-valor com suporte para índices e consulta baseada em cursor. Os limites de armazenamento são generosos — os navegadores permitem uma porcentagem do espaço em disco, geralmente gigabytes na prática. Você pode armazenar objetos estruturados, Blobs, ArrayBuffers e Arquivos sem precisar serializar manualmente. É disponível em Web Workers. É o que os PWAs e os aplicativos com funcionalidade offline usam para armazenar dados que seriam inaceitáveis para manter na memória.
// Native IDB API — nobody writes this directly in production
const request = indexedDB.open('myDB', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('users', { keyPath: 'id' });
};
request.onsuccess = (event) => {
const db = event.target.result;
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.put({ id: 1, name: 'Alex', avatar: someBlob });
};
// What you actually use — Dexie.js or the idb wrapper
import Dexie from 'dexie';
const db = new Dexie('myApp');
db.version(1).stores({ users: '++id, name, email' });
await db.users.add({ name: 'Alex', email: 'alex@example.com' });
Onde quebra
- Safari ITP — igual ao localStorage: após 7 dias sem interação, o Safari pode remover os dados do IndexedDB. Isso matou vários PWAs antes que a Apple corrigisse o comportamento nas versões mais recentes do iOS. Se seu público-alvo inclui usuários do Safari no iOS, considere isso.
- Armazenamento baixo no iOS — no iOS, o sistema operacional pode excluir os dados do IndexedDB quando o armazenamento é baixo. Ele não pedirá. Os dados simplesmente não estarão lá.
- Navegação privada — o Chrome permite o IndexedDB em modo incógnito (com um limite por sessão). O Safari em navegação privada usava a lançar erros; o comportamento varia por versão.
- A API nativa — se você escrever código bruto do IDB sem um wrapper, o modelo de eventos baseado em callback produzirá erros que você passará um dia para depurar. Use idb ou Dexie.js.
Use para
Aplicativos offline, grandes conjuntos de dados que ultrapassariam o localStorage, qualquer coisa que você construiria no localStorage que estaria atingindo o limite de 5MB, cache de arquivos em PWAs. Se você estiver armazenando 50 documentos gerados pelo usuário localmente, o IndexedDB é a resposta. Se você estiver armazenando uma preferência de tema do usuário, é excesso.
Biscoitos
O mais antigo dos quatro. O único que o servidor vê. O único com um limite de 4KB que realmente te atingirá se você tentar armazenar um JWT nele.
O que diferencia os cookies
Os cookies são enviados automaticamente com cada solicitação correspondente. Isso é tanto uma característica quanto um problema. Isso significa que seu cookie de sessão chega ao servidor sem qualquer envolvimento de JavaScript — e também significa que cada solicitação a api.example.com carrega o sobrecarga do cookie, independentemente de você gostar ou não.
Os atributos que realmente importam:
- HttpOnly — o JavaScript não pode ler esse cookie. Ataques XSS não conseguem extrair esse dado. Isso é uma exigência básica para cookies de sessão.
- Seguro — enviados apenas por HTTPS. Sem isso em um site de produção, você está enviando cookies de autenticação por HTTP. Não faça isso.
- SameSite=Strict — o cookie é enviado apenas quando a solicitação origina-se do seu próprio domínio. Oferece proteção contra CSRF, mas quebra os fluxos de redirecionamento OAuth. O SameSite=Lax é uma compensação razoável para a maioria dos aplicativos.
- Expires / Max-Age — sem esses, é um cookie de sessão que é apagado quando o navegador é fechado. Defina uma data de expiração explícita para o comportamento "lembre-me".
// Setting a cookie from JavaScript (no HttpOnly, obviously)
document.cookie = "theme=dark; Path=/; Max-Age=31536000; Secure; SameSite=Lax";
// Server-side (Node.js + Express) — where the real power is
res.cookie('sessionId', token, {
httpOnly: true, // JS cannot read this
secure: true, // HTTPS only
sameSite: 'lax', // balanced CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days in ms
});
Se você estiver debugando uma string de cookie complicada, IO Tools’ Cookie Parser o quebrará em pares chave-valor legíveis — útil quando você está olhando para uma cabeçalho de 400 caracteres Set-Cookie e tentando descobrir qual atributo está errado.
Onde quebra
- Limite de 4KB — é o tamanho total incluindo nome + valor + todos os atributos. Um JWT típico tem 400–800 bytes. Um grande com muitos claims tem 1–2KB. Você não tem muito espaço.
- SameSite=Strict mata o OAuth — quando seu IdP redireciona de volta para o seu aplicativo após o login, é uma solicitação de site diferente. O SameSite=Strict significa que seu cookie de sessão não será enviado. Use Lax para qualquer coisa que passe por fluxos de OAuth.
- Ordem dos cookies e correspondência de caminho — quando múltiplos cookies têm caminhos que se sobrepõem, o navegador os envia em ordem indefinida. Não confie na prioridade.
- Depreciação de cookies de terceiros — o Chrome está removendo cookies de terceiros. As dependências de cookies de site diferente são uma responsabilidade de médio prazo.
A discussão sobre o JWT em localStorage (a superfície real de ataque)
Este ponto surge em cada discussão sobre autenticação, e a maioria das pessoas discutindo sobre isso estão falando passo a passo.
A preocupação com armazenar JWTs no localStorage: se seu site tiver uma vulnerabilidade XSS, um script do atacante pode ler o token diretamente e extrair o conteúdo. O atacante agora tem um token de autenticação válido que pode usar de qualquer lugar, em qualquer dispositivo, até que expire.
A argumentação a favor de cookies HttpOnly em vez disso: o JavaScript não pode ler o cookie, então o XSS não pode extrair o conteúdo. A sessão ainda é usável em solicitações (o atacante pode fazer solicitações a partir do navegador do vítima via XSS), mas não pode roubar o token para usar em outro lugar. Isso limita o alcance do ataque.
A avaliação honesta: o problema real é o XSS, não a localização de armazenamento. Se você tiver XSS, um cookie HttpOnly é significativamente melhor — o atacante não pode levar o token para fora. Mas corrigir o XSS é um objetivo mais significativo, alcançável com uma política de segurança rigorosa, sem scripts de terceiros não controlados e com escape de saída adequado.
Se você estiver construindo um SPA com uma política de segurança rigorosa e sem scripts de terceiros que você não controle, o localStorage provavelmente é suficiente para JWTs. Se você estiver rodando um site com Google Tag Manager, pixels de anúncios e dezenas de dependências npm, cookies com HttpOnly são muito mais seguros porque sua superfície de ataque XSS é maior do que você imagina.
Ao depurar problemas com JWTs, o JWT Decoder no IO Tools é útil — cole o token e veja o payload e a data de validade sem escrever código. O Verificador de Expiração JWT é útil para confirmar se um token ainda é válido quando você está rastreando uma cascata de 401.
Fluxo de decisão
Antes de abrir uma aba do Stack Overflow às 3h, passe por isso:
- O servidor precisa ler isso? → Cookie. Fim da discussão.
- É maior que 5MB ou você precisa de consultabilidade? → IndexedDB.
- Deveria desaparecer quando a aba for fechada? → sessionStorage.
- Tudo o resto → localStorage, com cautela apropriada sobre o que você está armazenando.
A única coisa que todos erram
Os APIs de armazenamento não são confiáveis em todos os usuários em todos os momentos. Eles podem ser apagados pelo navegador, pelo sistema operacional, por configurações de privacidade ou pelo usuário. Qualquer arquitetura que trate o armazenamento do cliente como fonte de verdade — em vez de como um cache — eventualmente falhará em alguém.
O padrão que permanece: trate o armazenamento do navegador como uma otimização de desempenho sobre a sua fonte de verdade real (o servidor). Cache com agressividade, mas projete seu aplicativo para se recuperar com graça quando o cache estiver vazio. As sessões de depuração às 3h quase nunca são sobre qual API de armazenamento você usou — são sobre assumir que os dados estariam lá quando não estavam.
Instale nossas extensões
Adicione ferramentas de IO ao seu navegador favorito para acesso instantâneo e pesquisa mais rápida
恵 O placar chegou!
Placar é uma forma divertida de acompanhar seus jogos, todos os dados são armazenados em seu navegador. Mais recursos serão lançados em breve!
Ferramentas essenciais
Ver tudo Novas chegadas
Ver tudoAtualizar: Nosso ferramenta mais recente foi adicionado em 18 de junho de 2026
