Você tem que lidar com o formato Base64? Então esse site é perfeito para você! Use o nosso ferramenta online super útil para codificar ou decodificar os seus dados.

Codificação Base64 em JavaScript/Browser: um guia completo

Você tem algo que precisa viajar, e a estrada só é larga o bastante para ASCII puro. Pode ser uma imagem que mora dentro de uma resposta JSON, um objeto de configuração que precisa embarcar numa URL, um token cujos três segmentos são pontos e letras, um arquivo que uma API insiste em receber como string Base64 dentro de um body JSON. O Base64 é a barreira de pedágio para exatamente essa situação, e a página inicial deste site já passou pelo formato - quatro caracteres imprimíveis representando cada três bytes, com padding de = para fechar o grupo - então aqui vai o único número para guardar na cabeça enquanto lê: codificar é a direção de crescimento. Cada três bytes que você entrega voltam como quatro caracteres, um imposto de tamanho de mais ou menos 33 por cento, cobrado em banda, storage e memória. Use Base64 quando o canal exige texto imprimível, e saiba exatamente quanto esse imposto custa.

A notícia animadora é que o navegador sempre soube fazer esse trabalho sem um único pacote. btoa() vem sendo entregue desde o início dos anos 2000, TextEncoder transformou o seu texto Unicode de verdade em bytes honestos há uma década, e na onda Baseline 2025 a plataforma finalmente adicionou Uint8Array.toBase64(), que codifica arrays de bytes direto, com opção para o alfabeto URL-safe. Este artigo é o mapa de decisões: qual ferramenta para qual trabalho, onde as arestas vivas se escondem (todas levam de volta à mesma fronteira), e as receitas concretas para os lugares onde você de verdade será pedido para produzir Base64.

Escolhendo o seu encoder

Já não existe um único "the" encoder, e pegar o errado é assim que nascem os bugs clássicos. A tabela abaixo é a árvore de decisão inteira:

Situação Vá de
Texto ASCII puro, valor de uso único btoa(text)
Texto de verdade com acentos, emoji, CJK new TextEncoder().encode(text), depois btoa ou toBase64
Bytes já num Uint8Array bytes.toBase64() em navegadores 2025+, a ponte chunked de btoa nos demais
URLs, JWTs, nomes de arquivo toBase64({ alphabet: 'base64url', omitPadding: true })
Navegadores antigos ou um codebase compartilhado js-base64, ou a receita clássica de TextEncoder + btoa

O padrão embaixo da tabela: btoa() só lê caracteres de um byte, então qualquer coisa que não seja ASCII precisa virar um array de bytes primeiro, e é esse array de bytes em torno do qual as APIs modernas foram construídas. Guarde "texto vira bytes, bytes viram Base64" na cabeça e toda receita deste artigo é o mesmo par de passos com nomes diferentes por cima.

btoa e a fronteira Latin1

btoa(stringToEncode) - string binária para string ASCII - é o encoder original, disponível em todo navegador que importa (Chrome 4, Firefox 1, Safari 3, IE 10 e acima, todos os escopos de worker, e Node a partir da versão 16). O contrato dela tem uma cláusula, e é nessa cláusula que tudo dá errado: cada caractere da entrada precisa ter um code point entre 0 e 255. A função lê code points, não bytes UTF-8, então "é" (code point 233) passa voando enquanto "你" (code point 20320) lança uma DOMException chamada InvalidCharacterError antes que um único caractere seja codificado. A fronteira não é "ASCII", não é "Unicode", é exatamente 256, e ela inclui os caracteres de controle embaixo - codificar um byte NUL é legal e significativo, o que é um dos motivos da função existir.

O comportamento completo, linha por linha:

Entrada Resultado
"Hello, World!" "SGVsbG8sIFdvcmxkIQ==" - o caso de manual
"" (string vazia) "" - nada entra, nada sai
"\u0000" (NUL) "AA==" - caracteres de controle são cidadãos de primeira classe
"a\u00e9z" (é, code point 233) "Yel6" - a faixa inteira Latin1 passa
"\u0100" (code point 256) lança InvalidCharacterError - um passo além da fronteira
"h\u4f60" (你, code point 20320) lança InvalidCharacterError - e o mesmo vale para todo emoji, porque todos estão bem acima de 255

Duas notas práticas. A mensagem de erro é diferente por engine - o Firefox diz "String contains an invalid character", o Chrome diz que a string "contains characters outside of the Latin1 range" - então em qualquer código defensivo você pega pelo nome da exceção. E o throw acontece no primeiro caractere infrator, não no final: btoa não codifica metade da string e pede desculpas. Quando você quer o comportamento Latin1 de propósito (codificar uma string de bytes construída de propósito de code points 0-255), a função está fazendo exatamente o que você pediu, e a tabela acima é a personalidade inteira dela.

A ponte de bytes

Então a pergunta fica: como dados reais - os bytes UTF-8 do seu texto, o conteúdo de um arquivo, a saída de um canvas - entram na entrada de btoa()? A resposta é a "ponte de bytes": uma string JavaScript em que cada caractere guarda um valor de byte, o mesmo truque que os decoders produzem e que btoa entende nativamente. A versão ingênua é um loop:

function bytesToBase64 (bytes) {
  let binary = '';
  for (let i = 0; i < bytes.length; i += 1) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

Correto, mas concatenação de string num loop é lenta para arquivos grandes, e o atalho popular - String.fromCharCode.apply(null, bytes), que alimenta o array inteiro como argumentos numa chamada só - tem um penhasco duro. Chamadas de função têm limite de número de argumentos, e ele é alcançado bem antes do seu primeiro megabyte:

const big = new Uint8Array(1000000);
btoa(String.fromCharCode.apply(null, big));
// RangeError no Firefox: "too many arguments provided for a function call"
// RangeError no Chrome: "Maximum call stack size exceeded"

A correção que salvou mais features de upload de arquivo que qualquer outra mudança é atravessar a ponte em chunks, alguns milhares de caracteres de cada vez, e juntar os resultados:

function bytesToBase64Chunked (bytes) {
  const CHUNK = 0x8000;
  const parts = [];
  for (let i = 0; i < bytes.length; i += CHUNK) {
    parts.push(String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK)));
  }
  return btoa(parts.join(''));
}

Cada chunk é pequeno o bastante para aplicar com segurança, subarray dá uma view sem copiar, e o join produz exatamente a mesma string binária que o loop teria produzido. Agora o lado do texto da moeda. Para qualquer texto de verdade, TextEncoder - o encoder UTF-8 da plataforma, disponível no Firefox 18, Chrome 38, Safari 10.1 e em todo lugar desde então - transforma a sua string em bytes honestos antes da ponte fazer o seu trabalho:

const bytes = new TextEncoder().encode('hello 你好');
const base64 = bytesToBase64Chunked(bytes);
console.log(base64); // "aGVsbG8g5L2g5aW9"

Esse output é o que "hello 你好" de verdade é no fio: seis bytes ASCII mais seis bytes UTF-8 para os dois caracteres chineses, todos usando o mesmo disfarce imprimível. Se o seu texto não é UTF-8 - e na web, geralmente é - você precisa do outro charset primeiro, o que significa codificá-lo em algum lugar que fale aquele charset, geralmente o servidor. TextEncoder recusa de propósito adivinhar, e faz bem.

O atalho de 2025: Uint8Array.toBase64

Se você já segura um Uint8Array, a ponte é um desvio, porque o novo recurso do ECMAScript (ES2026) codifica o array direto: bytes.toBase64(options). Ele chegou no Chrome 140, Edge 140, Firefox 133, Safari 18.2, Node 25 e Deno 2.5 - a mesma onda Baseline 2025 do seu irmão de decodificação - e aceita duas opções que o transformam no encoder mais versátil da plataforma. A primeira é alphabet: "base64" (o padrão) ou "base64url". A segunda é omitPadding: deixe em true e os caracteres = no final caem, que é a forma que a maioria dos consumidores amigáveis a URL quer. Passar qualquer outra coisa como opções lança um TypeError, que é a API sendo educada sobre o seu typo:

const bytes = new Uint8Array([251, 255]);
console.log(bytes.toBase64()); // "+/8="
console.log(bytes.toBase64({ omitPadding: true })); // "+/8"
console.log(bytes.toBase64({ alphabet: 'base64url' })); // "-_8="

Esses dois bytes foram escolhidos para serem o mais grosseiro possível com o alfabeto: eles produzem um + e um / no modo padrão, então a última linha mostra exatamente o que muda quando você troca para base64url. Performance é o bônus silencioso: num Firefox recente, codificar dez megabytes leva cerca de cinco milissegundos com toBase64, enquanto o caminho da ponte de string acima leva mais ou menos quinze vezes mais, porque ele constrói uma string intermediária gigante no processo. Em navegadores antigos a ponte continua perfeitamente útil para qualquer coisa abaixo de alguns megabytes - e a versão chunked acima é a que você quer, pelos motivos da seção anterior.

Saída URL-safe

O Base64 tem uma variante dedicada para os lugares onde +, / e = causam dano, e vale a sua própria seção porque tanto código quebrado é só Base64 padrão que encontrou uma URL. Numa query string, + é espaço; num path, / é separador; e = quer percent-encoding em algumas posições. O alfabeto seguro para URL e nome de arquivo do RFC 4648, seção 5 - base64url - troca esses dois caracteres por - e _, e como o comprimento dos dados costuma ser conhecido do lado receptor, ele também permite tirar o padding por completo. A saída viaja por URLSearchParams, segmentos de caminho, fragments e nomes de arquivo sem um único sinal de percent.

Com a API de 2025 isso é um objeto de opções:

const params = new URLSearchParams();
params.set('payload', bytes.toBase64({ alphabet: 'base64url', omitPadding: true }));
console.log(params.toString()); // "payload=-_8" - nenhum percent-encoding

Em navegadores antigos, converta depois de codificar com btoa. Dois replaces e um trim fazem o trabalho inteiro:

function toUrlBase64 (base64) {
  return base64
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}
console.log(toUrlBase64(btoa('hi?/x'))); // "aGk_L3g"

Três regras mantêm o canal limpo. Escolha um alfabeto por canal e fique nele - um valor que mistura + e - não pertence a nenhuma família, e nenhum decoder vai adivinhar qual você quis. Padding é um contrato, não uma sugestão: se você omite, o receptor tem que estar pronto para um valor sem padding, e se você mantém, o receptor não pode engasgar com ele (navegadores são tolerantes, alguns schemas JSON não). E lembre que a troca é reversível e sem perda - - e _ mapeiam para as mesmas posições 62 e 63 do alfabeto ocupadas por + e /, então nada se perde escolhendo o par mais amigável.

Fazendo imagens viajar: data URLs

O uso mais antigo e mais visível de Base64 no navegador é a data URL: data:, um media type opcional, um flag opcional ;base64, uma vírgula, e depois o payload. Payloads de texto são percent-encoded; payloads binários - imagens, fontes, áudio - são Base64, e o navegador os renderiza com zero pedidos HTTP. Para um arquivo de imagem que o usuário acabou de escolher, o FileReader faz a codificação por você e devolve a URL pronta:

const reader = new FileReader();
reader.onload = () => {
  console.log(reader.result); // "data:image/png;base64,iVBORw0KGgo..."
  imageElement.src = reader.result;
};
reader.readAsDataURL(file);

O resultado é um src pronto, um valor que você pode guardar em localStorage ou enviar num body JSON. Se a imagem está num canvas - um screenshot, uma foto processada, um gráfico gerado - canvas.toDataURL() faz esse trabalho desde as primeiras releases dos navegadores, e ainda deixa você escolher o formato e, para formatos lossy, a qualidade:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.drawImage(photo, 0, 0);
const pngUrl = canvas.toDataURL('image/png');
const jpegUrl = canvas.toDataURL('image/jpeg', 0.8);

Três armadilhas para planejar em volta. Primeira, a regra do canvas contaminado: se você desenhou uma imagem cross-origin no canvas sem permissão CORS, toda tentativa de ler os pixels de volta - incluindo toDataURL - lança um SecurityError. A correção é carregar a imagem com crossOrigin = 'anonymous' e garantir que o servidor envie os headers certos. Segunda, o argumento de qualidade é ignorado para PNG e só tem sentido para JPEG (e WebP) - uma fonte comum de "por que meu PNG está maior". Terceira, e a maior: o payload fica cerca de 33 por cento maior que o arquivo, e ele mora na página como string. Para imagens que nunca saem do navegador, existe uma alternativa de graça - uma object URL, que embrulha o Blob sem codificá-lo de jeito nenhum:

const objectUrl = URL.createObjectURL(blob);
imageElement.src = objectUrl;
URL.revokeObjectURL(objectUrl); // quando você terminar de usar

A divisão de trabalho que sai disso: object URLs para qualquer coisa que fica na página, data URLs para qualquer coisa que precisa ser copiada, guardada ou enviada como texto. As duas são de primeira classe; elas só estão resolvendo problemas diferentes.

Construindo e assinando um JWT

Se você gera tokens no navegador - para um fluxo de auth auto-hospedado, uma demo, ou um front end serverless - o formato compacto JWS é três segmentos base64url: header, payload, assinatura, sem padding em lugar nenhum. A Web Crypto API cuida da assinatura; a codificação é exatamente a saída URL-safe de duas seções atrás:

const encoder = new TextEncoder();
const segment = (bytes) =>
  bytes.toBase64({ alphabet: 'base64url', omitPadding: true });
const header = segment(encoder.encode(JSON.stringify({ alg: 'HS256', typ: 'JWT' })));
const payload = segment(encoder.encode(JSON.stringify({ sub: '1234567890', name: 'John Doe' })));
const key = await crypto.subtle.importKey(
  'raw',
  encoder.encode('shared-secret'),
  { name: 'HMAC', hash: 'SHA-256' },
  false,
  ['sign']
);
const signature = segment(
  new Uint8Array(
    await crypto.subtle.sign('HMAC', key, encoder.encode(header + '.' + payload))
  )
);
const token = header + '.' + payload + '.' + signature;

Dois detalhes importam mais que a tubulação. A assinatura cobre exatamente header + '.' + payload - os segmentos crus, não o JSON - então qualquer edição em qualquer parte invalida o token, que é o ponto inteiro dela. E crypto.subtle.sign devolve um ArrayBuffer cru, daí o wrap de uma linha num Uint8Array antes do encoder de segmento. Para tokens baseados em RSA o fluxo é idêntico com RS256 e um par de chaves, e se você exportar uma chave pública como JWK (crypto.subtle.exportKey('jwk', key)), os membros numéricos - n, e, e para chaves privadas d, p, q - saem como base64url sem padding automaticamente. As ressalvas de segurança são as mesmas de qualquer token: um header alg: "none" é um pedido para pular a verificação, claims de tempo (exp, nbf) precisam ser aplicadas, e um servidor que aceita HMAC e RSA para a mesma audience abre a porta clássica da confusão de chaves. Codifique certo, assine certo, verifique no lado receptor.

Cabeçalhos de autenticação

O esquema de autenticação mais simples da web também é o mais instrutivo sobre o que o Base64 é e não é. O HTTP Basic envia Authorization: Basic seguido do Base64 de username:password - uma chamada, sem ponte de bytes, porque nomes de usuário e senhas são (esperemos) texto puro:

const credentials = btoa('alice:secret123');
fetch('/api/me', {
  headers: { Authorization: 'Basic ' + credentials }
});
// Authorization: Basic YWxpY2U6c2VjcmV0MTIz

E aqui está a lição que cabe numa linha: Base64 não é criptografia. O header acima está a uma chamada de atob de alice:secret123 - para o atacante e para quem estiver lendo os logs - então a auth Basic só é aceitável sobre HTTPS, onde o transporte é a proteção de verdade e o Base64 é só o formato. Para qualquer coisa com vida maior que um pedido, prefira esquemas baseados em token: um token Bearer também é um único header, mas é um valor aleatório cujo segredo nunca precisa ser carregado no header de jeito nenhum, e ele pode ser revogado. A escolha de codificação entre os dois é trivial - os dois são btoa ou texto puro - mas a escolha de segurança não é, e ela deve ser feita de propósito.

Arquivos entram, texto sai

O upload é onde o imposto de 33 por cento sai em dinheiro de verdade, porque o arquivo costuma ser a coisa maior da página. São duas estradas, e a primeira é a que você deve tomar por padrão: form data multipart. FormData carrega o arquivo como bytes brutos num body padrão, com o navegador fazendo o framing, e não há Base64 em lugar nenhum - sem imposto de tamanho, sem string intermediária, e os bytes fazem stream para o servidor conforme são lidos:

const form = new FormData();
form.append('upload', file);
await fetch('/api/upload', { method: 'POST', body: form });

A segunda estrada é para as APIs que insistem num body JSON com o arquivo como string - algumas funções serverless, alguns backends mobile, alguns serviços legados. Lá a codificação é uma linha por arquivo, e o custo é exatamente o que o imposto diz: um arquivo de 5 megabytes vira uma string de 6.7 megabytes, que então é serializada em JSON, que então é enviada. Tudo bem para uma foto, doloroso para um vídeo:

const bytes = new Uint8Array(await file.arrayBuffer());
const body = JSON.stringify({
  name: file.name,
  content: bytes.toBase64()
});
await fetch('/api/upload-json', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body
});

Para arquivos grandes nessa estrada, não construa uma string gigante numa chamada só - construa em fatias, onde cada fatia é um múltiplo de três bytes. Esse alinhamento é o que torna o truque legal: um múltiplo de três bytes codifica num múltiplo limpo de quatro caracteres sem padding, então, concatenadas, as fatias codificadas independentes dão exatamente a codificação do arquivo inteiro, e só a última fatia carrega padding:

async function encodeLargeFile (file) {
  const bytes = new Uint8Array(await file.arrayBuffer());
  const SLICE = 3 * 1000 * 1000;
  const parts = [];
  for (let i = 0; i < bytes.length; i += SLICE) {
    parts.push(bytes.subarray(i, i + SLICE).toBase64());
  }
  return parts.join('');
}

A mesma ideia de alinhamento explica por que você nunca deve partir uma string Base64 num ponto arbitrário e esperar que os pedaços decodifiquem sozinhos - um grupo de três bytes é o átomo, e um corte no meio dele deixa um fragmento pendurado. Downloads são o espelho: para um arquivo gerado, um pequeno pode sair por uma data URL num link de download, mas para qualquer coisa substancial, Blob mais object URL é o caminho saudável, porque o navegador nunca precisa carregar o payload inteiro como string em primeiro lugar.

Guardando e compartilhando estado

Mais dois canais só texto onde o Base64 faz trabalho de verdade. O primeiro é storage: localStorage e sessionStorage guardam strings, então dados estruturados ou binários são codificados antes de entrar. A ida e volta é um encode e um decode, e vale a pena ver os dois lados juntos, porque um bug de storage quase sempre é um mismatch de charset entre eles:

const state = { theme: 'dark', draft: 'hello' };
const packed = new TextEncoder().encode(JSON.stringify(state));
localStorage.setItem('app-state', new Uint8Array(packed).toBase64());
const raw = atob(localStorage.getItem('app-state'));
const bytes = Uint8Array.from(raw, (c) => c.codePointAt(0));
const state = JSON.parse(new TextDecoder().decode(bytes));

Mas faça o orçamento direito: a origem fica com mais ou menos 5 megabytes de localStorage, a sua string armazenada é 33 por cento mais gorda que os dados, e enquanto a página está aberta a string também vive em memória como UTF-16 - o dobro do comprimento de novo. Um asset de 3 megabytes é 4 megabytes de storage e 8 megabytes de memória, que é como uma feature "pequena" vira erro de quota. O segundo canal é a própria URL: links compartilhados, deep links e estado OAuth todos querem dados estruturados num lugar que sobrevive a copy-paste. A receita é estado compacto, JSON, depois base64url sem padding, para o valor não precisar de percent-encoding de jeito nenhum - e mantenha a URL inteira abaixo de um par de mil caracteres, que é onde clientes antigos, proxies e ferramentas de log começam a ficar nervosos.

E-mail e MIME

O Base64 é mais velho que a web, e o seu território natal é o e-mail. Anexos MIME com Content-Transfer-Encoding: base64 são o jeito que um arquivo binário viaja dentro de um protocolo de texto, e a convenção que veio do antigo limite de 76 caracteres por linha do formato de mensagem vale a pena conhecer: quebre o corpo codificado a 76 caracteres por linha. O navegador não envia SMTP, mas faz dois trabalhos de e-mail - montar corpos MIME que um backend relay vai enviar, e exibir os anexos das mensagens que recebe - e os dois tocam na codificação. O wrap em si é uma função de duas linhas, e a ordem das operações importa: codifique primeiro, quebre depois, porque btoa não vai lançar por causa de uma quebra de linha na entrada - ela vai codificar a quebra como um byte de payload, e as suas quebras de linha acabam dentro da saída:

function wrapForMime (base64, width) {
  const w = width || 76;
  return base64.match(new RegExp('.{1,' + w + '}', 'g')).join('\r\n');
}

O lado receptor é o de graça: atob pula espaços em branco ASCII como parte do seu comportamento padrão, então um corpo MIME quebrado decodifica exatamente como chegou, quebras de linha e tudo, sem um passo de unwrap. Se você está construindo um cliente webmail ou um picker de anexos, essa única assimetria - o encoder tem que produzir linhas limpas, o decoder não se importa - é a história inteira do MIME numa frase.

Quando recorrer a uma biblioteca

Com as ferramentas nativas acima, uma biblioteca raramente é necessária, e o conselho honesto é: vá de plataforma por padrão, e adicione um pacote só quando um requisito de verdade aponte para ele. As três que de fato aparecem em codebases:

js-base64 (npm install js-base64) é a de uso geral: um transcoder pequeno e de JavaScript puro que trata strings UTF-8 como cidadãos de primeira classe - Base64.encode numa string CJK faz a dança UTF-8 por você - e, útil para decodificar tanto quanto para codificar, aceita os dois alfabetos em decode e traz uma checagem isValid. É a resposta certa quando você mira navegadores onde as APIs de 2025 estão faltando e quer um import só para cobrir strings e bytes:

import { Base64 } from 'js-base64';
const encoded = Base64.encode('小飼弾'); // "5bCP6aO85by+" - UTF-8 tratado por você
const decoded = Base64.decode('5bCP6aO85by-'); // lê padrão e URL-safe por igual
const valid = Base64.isValid(encoded); // true

base64-js é a focada em bytes: fromByteArray e toByteArray em Uint8Arrays, sem dependências, o cavalo de batalha do antigo ecossistema browserify e ainda uma ótima escolha quando o seu código mora em typed arrays e você quer que a codificação seja uma função pura de bytes. E se o seu motivo para querer uma biblioteca é "gosto da API de 2025 mas não posso exigir navegadores de 2025", a resposta não é um pacote Base64 de jeito nenhum, é um polyfill: o core-js (e o preset do Babel que puxa ele) implementa Uint8Array.fromBase64 e companhia, então você escreve o código novo uma vez e deixa o shim preencher o buraco em engines antigas. Escolha pelo constraint - navegadores antigos, conveniência de string, ou pureza de bytes - não por hábito.

Armadilhas que custaram horas de desenvolvedores

  • Chamar btoa numa string com um caractere acima do code point 255. Ela lança, não corrompe, e para no primeiro infrator. A correção é sempre a mesma: TextEncoder primeiro, ponte depois.
  • O penhasco do fromCharCode.apply em arrays grandes. Um milhão de argumentos é RangeError nas duas engines principais. Faça a ponte em chunks, ou vá para toBase64.
  • Esquecer o imposto de tamanho onde dói mais: storage. Um arquivo em localStorage fica 33 por cento maior que o arquivo, e a quota é por origem, compartilhada com tudo o mais que o seu app salva.
  • Base64 padrão encontrando uma query string. O + chega como espaço, o / quebra o path, e os relatórios de bug dizem "a API está instável". Saída URL-safe, sem padding, e o gênero inteiro de bug some.
  • Padding inconsistente entre serviços. Um gateway mantém o =, outro tira, um terceiro adiciona de volta. O receptor tem que estar pronto para as duas formas, e o contrato deve dizer qual é a canônica.
  • Tratar Base64 como um cadeado. É um formato de serialização, a uma chamada de função de ser texto puro, e "codificado em Base64" numa revisão de segurança é uma constatação, não um controle.
  • Binary strings como modelo de memória. Um megabyte decodificado ou codificado viaja em UTF-16 ocupando dois megabytes; um Uint8Array guarda ocupando um. Para payloads grandes, mantenha os bytes em typed arrays do começo ao fim.
  • Dupla codificação. Um valor que já era Base64 é codificado de novo, e o consumidor decodifica uma vez e ganha uma string de letras em vez de dados. Em dúvida, cheque antes de embrulhar - uma string que já está no alfabeto com padding válido é um cheiro ruim.
  • Confiar num payload JWT porque ele decodificou limpo. Decodificabilidade não é autenticidade. Verifique a assinatura com a chave certa e o algoritmo certo antes de ler uma única claim.

Performance: o que um milhão de bytes custa

O Base64 no navegador é barato onde costumava ser caro, e o orçamento agora tem três itens em vez de um. CPU: num Firefox recente, Uint8Array.toBase64 codifica dez megabytes em mais ou menos cinco milissegundos, enquanto a ponte chunked de btoa leva mais ou menos quinze vezes mais - não porque btoa é lenta, mas porque a ponte constrói uma string intermediária gigante no caminho. Se o seu orçamento de codificação é em milissegundos, use o método nativo; se você está codificando um objeto de configuração de 2 kilobytes, os dois estão abaixo do limiar de percepção. Banda: esse é o imposto permanente - cada byte que você codifica custa 1.33 bytes no fio, mais o framing que o transporte adicionar. Meça a transferência antes de "otimizar" a codificação. Memória: a string codificada é a maior alocação transitória que você vai fazer, e para um arquivo de 5 megabytes é uma string de 6.7 megabytes, ou mais ou menos 13.4 megabytes de memória UTF-16 enquanto a página segura ela. As consequências práticas saem da aritmética: faça encodings grandes em fatias para nenhuma string única ficar gigante, libere os bytes intermediários assim que a string existir, prefira object URLs e multipart quando os bytes nunca precisaram ser imprimíveis, e mova trabalho de vários megabytes para um Web Worker se a thread principal tiver que manter o scroll suave. O formato tem quase quatro décadas; a plataforma finalmente alcançou ele.

Como os navegadores aprenderam a codificar

O encoder tem uma história, e ela explica as relíquias que você vai herdar. btoa - "de binário para ASCII", o nome é literal, e atob é só as mesmas palavras invertidas - foi escrito na spec HTML em 2011, reengenhariado a partir dos navegadores que já a traziam: Firefox desde 2004, Safari 3, Chrome 4. O Internet Explorer, com seu estilo, pulou as duas funções até a versão 10 em 2012, e essa única ausência é a razão de uma década de JavaScript estar cheia de tabelas Base64 feitas à mão e de uma invocação em particular para Unicode: btoa(unescape(encodeURIComponent(str))). Ela funcionava - encodeURIComponent produz UTF-8 com percent-escaping, e unescape transformava isso numa string de bytes - mas foi construída sobre unescape(), a única do par que a linguagem deprecou, e sobreviveu em código de navegadores por anos de pura inércia. A correção baseada em princípios chegou com o padrão Encoding: TextEncoder e TextDecoder, no Firefox 18 (2013), Chrome 38 (2014), Safari 10.1 (2017), e em nenhuma versão de IE - outra falta do IE, outra década de workarounds. O Node.js conta a metade do lado servidor: tinha Buffer com Base64 desde o primeiro dia, mas atob e btoa como globals só a partir da versão 16 em 2021, com dois shims pequenos de npm carregando a conta antes disso. E então, ao longo do fim de 2024 e 2025, a linguagem em si entregou Base64 - Uint8Array.toBase64 e companhia no Firefox 133 (novembro de 2024), Safari 18.2 (dezembro de 2024), Chrome 140 (setembro de 2025) e Node 25 (outubro de 2025) - e o recurso foi marcado Baseline 2025 - o mesmo conjunto de recursos que a plataforma vinha aproximando com helpers por vinte anos, agora padrão. As curiosidades no final do artigo são quase todas sobre quanto tempo cada pedaço levou para chegar.

Você sabia?

  • Os nomes das funções são uma frase: btoa é "de binário para ASCII" e atob é "de ASCII para binário". A direção está no nome, que é por que o par é auto-documentado desde os anos 2000.
  • A string mais codificada da história da computação é provavelmente "hello": btoa('hello') é aGVsbG8=, a saída de todo tutorial, suíte de testes e quadro branco de entrevista do planeta.
  • Toda string Base64 válida tem comprimento múltiplo de quatro, padding incluído. Os caracteres = são uma impressão digital: um deles significa que o último grupo guardou dois bytes, dois deles significa que guardou um.
  • A quebra de linha de 76 caracteres no MIME e na maioria das ferramentas de linha de comando é uma herança da era do e-mail, quando o comprimento de linha do formato de mensagem estabelecia o limite. O número sobreviveu a três décadas de tudo mais rápido.
  • "Data URI" é um nome aposentado. O WHATWG mudou o nome para "data URL" durante a grande harmonização de URI para URL, o que explica por que specs, posts de blog e nomes de pacotes grafam tudo diferente no mesmo parágrafo.
  • btoa('') devolve '': uma entrada vazia produz uma saída vazia, sem padding, sem caso especial - a única string Base64 com zero caracteres (o comprimento dela, 0, ainda é múltiplo de quatro).
  • Um canvas pode transformar uma foto numa data URL com toDataURL - uma capacidade que existe desde o IE 9, Firefox 2 e Safari 4, anterior à maior parte da plataforma web que achamos "moderna" - e trazê-la de volta com uma tag <img> e um FileReader.
  • O handshake do WebSocket codifica SHA-1(key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11) em Base64, e o GUID é uma constante fixa no RFC escolhida precisamente para que nenhum servidor HTTP comum completasse o handshake por acidente.

Para onde ir a partir daqui

O ofício inteiro de codificação no navegador cabe numa página: btoa para os casos puros, de um byte, para os quais ela nasceu; TextEncoder mais a ponte chunked para texto e arquivos de verdade em qualquer navegador; Uint8Array.toBase64 com suas opções de alfabeto e padding para o caminho moderno e direto; e a variante URL-safe, com ou sem padding, para qualquer coisa que vai morar numa URL. O resto é juízo: conheça o imposto de 33 por cento antes de gastá-lo, mantenha os bytes em typed arrays enquanto são grandes, codifique primeiro e quebre depois, e nunca chame um formato de serialização de cadeado. Quando o canal consegue carregar bytes brutos, leve os bytes - o Base64 é para as estradas que só admitem texto imprimível, e agora você sabe exatamente como pagar o pedágio.

A outra metade da viagem - receber uma dessas strings e puxar de volta os bytes, o texto e o significado dela - é coberta em detalhe no guia companheiro de decodificação Base64 em JavaScript, linkado abaixo.

Última atualização: 2026-09-08

Artigo relacionado: Decodificação Base64 em JavaScript/Browser: um guia completo