Por que o Mesmo Texto Tem Contagens de Caracteres Diferentes
Você escreve uma frase, cola no contador de caracteres, e ele diz 120. Cola no campo do site de destino, e ele acusa 124. Salva no banco de dados, e o sistema reclama que passou do limite. Os três números são diferentes e nenhum deles está errado: eles contam coisas diferentes.
Quatro unidades, um texto só
Um texto pode ser medido de pelo menos quatro maneiras, e cada uma serve a um propósito.
- Unidades UTF-16 são o que a maioria dos contadores em navegador entrega, porque é o que a linguagem devolve nativamente. Caracteres fora do plano básico ocupam duas unidades.
- Pontos de código são os números que o Unicode atribui a cada caractere codificado.
- Grafemas são o que uma pessoa aponta na tela e chama de caractere.
- Bytes são o espaço ocupado no armazenamento, e dependem da codificação usada.
A tabela abaixo mostra as quatro medidas para alguns textos. Todos os valores foram medidos, não estimados.
| Texto | Unidades UTF-16 | Pontos de código | Grafemas | Bytes UTF-8 |
|---|---|---|---|---|
não |
3 | 3 | 3 | 4 |
café (composto) |
4 | 4 | 4 | 5 |
café (decomposto) |
5 | 5 | 4 | 6 |
ação (composto) |
4 | 4 | 4 | 6 |
ação (decomposto) |
6 | 6 | 4 | 8 |
🇧🇷 |
4 | 2 | 1 | 8 |
família 👨👩👧 |
16 | 13 | 9 | 27 |
Repare na coluna de grafemas: ela é a única que concorda com o que os olhos veem em todas as linhas. As outras três discordam por motivos diferentes.
O caso que mais engana em português
O Unicode permite escrever uma letra acentuada de duas formas. O ç pode ser um único ponto de código, ou pode ser a letra c seguida de uma cedilha combinante, que se desenha por cima da anterior. O ã pode ser um ponto de código, ou um a seguido de um til combinante.
O UAX #15 chama isso de equivalência canônica e define as duas formas:
Normalization Form D (NFD): Canonical Decomposition
Normalization Form C (NFC): Canonical Decomposition, followed by Canonical Composition
O mesmo anexo é claro sobre o que as duas têm em comum: sequências canonicamente equivalentes “representam o mesmo caractere abstrato” e, quando exibidas corretamente, “devem sempre ter a mesma aparência visual e comportamento”.
Aí está o problema prático. A palavra ação em forma composta tem 4 pontos de código. Em forma decomposta tem 6, porque ganha dois acentos que passaram a contar separado. Nada muda na tela. Tudo muda no contador, na comparação entre duas strings que deveriam ser iguais e no campo com limite rígido.
Texto decomposto costuma chegar por cópia entre sistemas, e a diferença é invisível até alguém contar.
Por que grafema não é ponto de código
A quarta coluna da tabela mostra o outro lado do mesmo problema. A bandeira do Brasil ocupa 2 pontos de código e 1 grafema; um emoji de família ocupa vários pontos de código e continua sendo um desenho só.
O UAX #29 explica por que a distinção existe:
A single Unicode code point is often, but not always the same as a basic unit of a writing system for a language, or what a typical user might think of as a “character”.
E define o grafema como a aproximação programática dessa unidade percebida, descrita no anexo como uma aproximação de melhor esforço que pode ser determinada de forma inequívoca por um programa. É a contagem que corresponde ao que o leitor enxerga, e a que faz sentido quando o limite é de leitura, não de armazenamento.
Qual número olhar
| Situação | Contagem que importa |
|---|---|
| Limite de campo em banco de dados | Bytes, geralmente UTF-8 |
| Limite anunciado por uma interface | O que aquele campo contar, conferido nele |
| Texto que uma pessoa vai ler | Grafemas |
| Comparar dois textos que deveriam ser iguais | Qualquer uma, depois de normalizar |
A linha do banco de dados é a que mais gera surpresa em português. Em UTF-8, letras sem acento ocupam 1 byte e acentuadas ocupam 2. A palavra ação tem 4 caracteres e 6 bytes. Um campo dimensionado como se caractere e byte fossem a mesma coisa vai estourar antes do esperado, e vai estourar mais cedo em textos com muito acento.
Como conferir o seu texto
O trecho abaixo roda no console de qualquer navegador e mostra as quatro medidas, além de avisar se o texto estava decomposto:
const t = "ação";
const seg = new Intl.Segmenter("pt-BR", { granularity: "grapheme" });
console.log({
utf16: t.length,
pontosDeCodigo: [...t].length,
grafemas: [...seg.segment(t)].length,
bytes: new TextEncoder().encode(t).length,
estavaDecomposto: t.normalize("NFC").length !== t.length,
});
Se estavaDecomposto vier true, o texto chegou com acentos separados. Normalizar para NFC resolve a contagem e a comparação, porque o UAX #15 garante que duas strings equivalentes produzem exatamente a mesma forma normalizada. O que a normalização não resolve é a diferença entre contar caracteres e contar bytes, que continua valendo depois dela.
Fontes e metodologia
As definições de normalização e de grafema vêm dos anexos técnicos do Unicode listados abaixo, abertos na data indicada. Todos os números das tabelas foram medidos, não estimados: rodei cada string no Node.js e conferi String.length, o total de pontos de código, o total de grafemas pelo Intl.Segmenter e o tamanho em bytes UTF-8, com o mesmo trecho de código reproduzido no texto para você refazer a medição. Plataformas específicas podem aplicar regras próprias de contagem que não estão documentadas publicamente, e por isso o guia não afirma o limite de nenhuma delas: quando o limite for rígido, a conferência que vale é a do próprio campo de destino.
- UAX #15 — Unicode Normalization Forms, revisão 57 (30/07/2025) Unicode Consortium As definições de NFD como decomposição canônica e de NFC como decomposição canônica seguida de composição canônica, e o princípio de equivalência canônica entre sequências que representam o mesmo caractere abstrato e devem ter a mesma aparência visual. Consultado em
- UAX #29 — Unicode Text Segmentation, revisão 47 (17/08/2025) Unicode Consortium A definição de grafema como a aproximação programática do que o usuário percebe como um caractere, e a constatação de que um único ponto de código nem sempre corresponde a essa unidade percebida. Consultado em
Perguntas frequentes
Por que o contador mostra um número e o site de destino mostra outro?
Porque os dois podem estar contando unidades diferentes. Contadores em navegador normalmente usam unidades UTF-16, que é o que a linguagem entrega de forma nativa; bancos de dados costumam limitar bytes; e algumas interfaces contam o que o usuário percebe como caractere. Nenhum dos três está errado, e por isso a conferência final deve ser feita no próprio campo de destino.
O que é NFC e NFD?
São formas de normalização definidas pelo Unicode. Pelo UAX #15, NFD é a decomposição canônica, em que a letra e o acento ficam guardados separadamente, e NFC é a decomposição seguida de composição canônica, que junta os dois num único ponto de código. As duas produzem a mesma coisa na tela e tamanhos diferentes na memória.
Por que a palavra "ação" às vezes conta 6 caracteres?
Porque ela chegou em forma decomposta. Em NFC, "ação" tem 4 pontos de código; em NFD, o ç vira c mais uma cedilha combinante e o ã vira a mais um til combinante, somando 6. O texto continua idêntico visualmente, e é por isso que o problema passa despercebido até algum campo com limite rígido recusar o conteúdo.
De onde vem texto em NFD?
Normalmente de cópia entre sistemas. Alguns ambientes armazenam nomes de arquivo e texto em forma decomposta, e o conteúdo mantém essa forma quando é colado adiante. Como a diferença é invisível, ela só aparece quando alguém conta os caracteres ou compara duas strings que deveriam ser iguais.
Como um emoji pode contar mais de um caractere?
Muitos emojis são sequências. A bandeira do Brasil é formada por dois símbolos indicadores regionais, e emojis de família juntam várias figuras com um caractere invisível de junção entre elas. O leitor vê um desenho só; a contagem por pontos de código vê vários.
Qual contagem devo usar para um limite de banco de dados?
Bytes, quase sempre em UTF-8. Nessa codificação, letras sem acento ocupam 1 byte, acentuadas ocupam 2 e emojis costumam ocupar 4. Uma frase curta em português já ocupa mais bytes do que caracteres, e um campo dimensionado como se os dois fossem iguais estoura antes do esperado.
Como sei em que forma o meu texto está?
Compare o texto com a versão normalizada dele. Se o comprimento mudar depois de normalizar para NFC, ele estava decomposto. O trecho de código deste guia faz exatamente essa checagem e roda em qualquer console de navegador.
Normalizar o texto resolve o problema?
Resolve o descompasso de contagem e de comparação, porque o UAX #15 garante que duas strings equivalentes produzem exatamente a mesma forma normalizada. O que ele não resolve é a diferença entre contar caracteres e contar bytes, que continua existindo depois da normalização.