Vai al contenuto
OneKitly

I limiti di caratteri che mordono davvero: unità di codice, code point e grafemi

Pubblicato il 01/07/2026 · 12 min di lettura · Strumenti testo e lingua

Daniel Okonkwo

Daniel OkonkwoSviluppatore front-end e redattore Tech presso OneKitly

Performance web · Formati di file

Verificato su 7 fonti

Vedi il profilo
In breve

La parola carattere indica tre unità diverse, e ogni limite che incontri è espresso in una di esse senza dire quale. Un'unità di codice UTF-16 è ciò che restituiscono .length in JavaScript e String.length() in Java. Un code point è un valore scalare Unicode, ciò che restituisce len() in Python 3. Un grafema è ciò che un lettore chiama carattere, ciò che restituisce String.count in Swift. Divergono appena il testo esce dall'ASCII puro. L'emoji pollice in su con tonalità di pelle media è 1 grafema, 2 code point e 4 unità UTF-16, e occupa 8 byte in UTF-8. L'emoji famiglia di quattro è 1 grafema, 7 code point, 11 unità UTF-16 e 25 byte. La stringa Shipping to, bandiera francese, today, pollice in su con tonalità, lineetta e thanks misura 37 unità UTF-16, 33 code point, 31 grafemi e 47 byte UTF-8: quattro numeri per una sola stringa. Le piattaforme scelgono unità diverse: X conta una lunghezza pesata, Bluesky impone 300 grafemi e 3000 byte insieme, Mastodon conta code point. Un SMS contiene 160 caratteri in GSM-7 ma solo 70 in UCS-2, e un solo apostrofo tipografico converte l'intero messaggio. E VARCHAR(255) significa caratteri in PostgreSQL e MySQL, byte in Oracle per impostazione predefinita.

Un carattere è tre cose insieme. Un'emoji con tonalità di pelle è 1 grafema, 2 code point e 4 unità UTF-16. Ogni conteggio di questa guida è stato misurato in Node, più il motivo per cui un SMS scende da 160 a 70 e perché VARCHAR(255) non sono 255 di niente in particolare.

Tre unità, tutte chiamate carattere

Un'unità di codice UTF-16 è a 16 bit. Tutto ciò che sta sopra U+FFFF - cioè ogni emoji, ogni scrittura storica e buona parte del CJK - ne richiede due, dette coppia surrogata. Le stringhe JavaScript sono definite come sequenze di unità di codice UTF-16, quindi .length conta quelle, così come String.length() in Java e il tipo string di .NET in C#.

Un code point è una voce del database dei caratteri Unicode, scritta U+0041 o U+1F44D. È l'unità che restituisce len() in Python 3 e quella usata dallo spread e dall'iterazione for-of in JavaScript. È più vicina all'intuizione di un'unità di codice, ma non è ancora ciò che vede un lettore, perché più code point si combinano abitualmente in un unico segno visibile.

Un grafema - lo standard lo chiama grafema esteso, definito nell'allegato 29 di Unicode - è il carattere percepito dall'utente. È ciò che il cursore scavalca, ciò che il tasto backspace cancella e ciò che una persona conta. String.count in Swift restituisce grafemi; lo fa anche Intl.Segmenter in JavaScript con granularità grapheme. Nessun altro linguaggio diffuso li restituisce di default, ed è la causa profonda di quasi tutti i bug con le emoji che hai visto.

Una stringa, quattro numeri

Prendi un messaggio breve e del tutto ordinario: Shipping to, un'emoji bandiera francese, today, un pollice in su con tonalità di pelle media, una lineetta e thanks con punto esclamativo. Misurato in Node 22 sono 37 unità di codice UTF-16, 33 code point, 31 grafemi e 47 byte in UTF-8. Una persona che lo legge direbbe 31 caratteri. JavaScript ti dirà 37. Una colonna di database limitata in byte ne vedrà 47.

Gli scarti vengono da due punti. L'emoji bandiera è una coppia di indicatori regionali - due code point, quattro unità UTF-16, una sola bandiera visibile - e il pollice in su è un'emoji base più un modificatore di tonalità, di nuovo due code point e quattro unità UTF-16 per un solo segno visibile. Insieme spiegano 37 contro 31. Nulla di esotico: è l'aspetto di un messaggio normale da un telefono normale.

Lo stesso testo visibile può avere due lunghezze diverse

La parola Cafe con accento acuto, seguita da uno spazio, un pollice in su con tonalità e un punto esclamativo, misura 10 unità UTF-16, 8 code point, 7 grafemi e 15 byte UTF-8 quando la lettera accentata è il code point precomposto U+00E9. Digita la stringa dall'aspetto identico su un Mac che emette invece e seguita da un accento acuto combinante e diventa 11 unità UTF-16, 9 code point, sempre 7 grafemi, e 16 byte UTF-8.

Stessi pixel, lunghezza diversa, e un controllo sui byte che passa in un caso fallisce nell'altro. La soluzione è normalizzare in ingresso: applica la forma di normalizzazione Unicode C, che ricompone e più accento combinante in U+00E9, prima di misurare, salvare, confrontare o calcolare un hash. Quasi ogni bug di rilevamento duplicati con nomi accentati risale al confronto fra una stringa NFC e una NFD.

Il troncamento è dove l'astrazione si rompe in pubblico

Taglia quella stringa Cafe a sei unità UTF-16 con un normale slice e ottieni C, a, f, la e accentata, uno spazio e poi U+D83D da solo: la metà alta di una coppia surrogata senza compagno. Quel surrogato isolato non è un carattere valido. I renderer mostrano un riquadro di sostituzione, i codificatori JSON emettono un escape che alcuni parser rifiutano, e i database con validazione UTF-8 stretta rifiutano la scrittura senza appello.

Troncare sui code point evita il crash del surrogato isolato, ma taglia comunque un'emoji famiglia in due adulti e un bambino, o toglie il modificatore di tonalità a un pollice in su che torna giallo. L'unico troncamento sicuro davanti a un utente è quello sui grafemi: segmenta con Intl.Segmenter, prendi i primi n segmenti, riuniscili. Se il tuo limite è in byte, fai entrambe le cose: conta i grafemi per decidere dove è lecito tagliare, e i byte UTF-8 per decidere quanti te ne puoi permettere.

Quale piattaforma conta quale unità

X non conta affatto caratteri: calcola una lunghezza pesata in cui i caratteri degli intervalli latino, supplemento latino-1 e punteggiatura generale contano 1, e tutto il resto - CJK, arabo, cirillico oltre il blocco base e ogni emoji - conta 2. Un post con limite 280 contiene quindi 280 lettere latine o 140 emoji. Bluesky impone due limiti insieme sullo stesso campo: 300 grafemi e 3.000 byte, quindi un post di 300 emoji bandiera è valido sui grafemi e fallisce sui byte. Il limite predefinito di 500 di Mastodon si conta in code point, con ogni URL addebitata forfettariamente 23 a prescindere dalla lunghezza reale.

La conseguenza pratica è che un solo contatore non può servire tutte le destinazioni. Se programmi lo stesso testo su quattro reti, ti servono quattro conteggi e un'anteprima che mostri la stringa come la vedrà il lettore, non come la misura il livello di archiviazione. Nel dubbio, conta i grafemi per il limite rivolto all'umano e i byte UTF-8 per quello rivolto alla macchina, e diffida di qualunque numero singolo etichettato caratteri finché non sai quale unità l'ha prodotto.

SMS: 160 diventa 70 appena un carattere esce dal GSM-7

Il corpo di un SMS è di 140 ottetti. Codificato nell'alfabeto GSM a 7 bit definito in 3GPP TS 23.038, sette bit per carattere, fa 160 caratteri. Ma quell'alfabeto ha solo circa 128 posti più una piccola tabella di estensione, e se anche un solo carattere del messaggio ne resta fuori, l'intero messaggio va ricodificato in UCS-2 a 16 bit per carattere: 140 ottetti diviso 2 fa 70 caratteri. Non 70 per il carattere colpevole: 70 per il messaggio intero.

Facciamo un esempio. Una conferma di prenotazione di 145 caratteri sta in un solo segmento GSM-7, perché 145 è sotto 160. Incollala da un elaboratore di testi che ha convertito in silenzio l'apostrofo dritto in uno tipografico, oppure aggiungi una lineetta o un'emoji: il messaggio passa a UCS-2. Ora 145 supera 70, quindi viene spezzato in parti concatenate, e la concatenazione sottrae sei ottetti per parte per l'intestazione di segmentazione, lasciando 67 caratteri ciascuna. 145 diviso 67 si arrotonda a 3. Una sostituzione invisibile ha trasformato un messaggio fatturabile in tre.

Quali caratteri siano sicuri non è intuitivo. Diverse lettere accentate stanno nel set base GSM-7 - e con accento acuto, e grave, a grave, u grave, la esse tedesca e le vocali con dieresi fra queste - quindi un SMS francese o tedesco non è automaticamente UCS-2. Ma a, i, o e u con accento acuto non ci sono, il che significa che gli accenti spagnoli e portoghesi di solito forzano il passaggio. Il simbolo dell'euro vive nella tabella di estensione e costa due settetti invece di uno. Virgolette curve, lineette medie e lunghe, il carattere dei puntini di sospensione e tutte le emoji semplicemente non esistono.

VARCHAR(255) non sono 255 di niente in particolare

In PostgreSQL, varchar(255) sono 255 caratteri, cioè code point, e lo spazio cresce fino a quanto serve a UTF-8. In MySQL, VARCHAR(255) conta caratteri e non byte dalla versione 5.0, quindi con il set utf8mb4 la stessa colonna può occupare fino a 1.020 byte. In Oracle, VARCHAR2(255) significa 255 byte a meno che tu scriva VARCHAR2(255 CHAR) o cambi NLS_LENGTH_SEMANTICS, ed è per questo che un campo con limite 255 in un sistema Oracle rifiuta in silenzio un nome di 200 caratteri scritto in un alfabeto accentato. In SQL Server, VARCHAR sono byte in una collazione a un byte e NVARCHAR sono unità UTF-16 da due byte ciascuna.

C'è una trappola di secondo ordine specifica di MySQL. Il vecchio limite di prefisso d'indice di InnoDB, 767 byte, faceva sì che una colonna VARCHAR(255) in utf8mb4 - fino a 1.020 byte - non fosse indicizzabile per intero, e da lì viene il folclore del VARCHAR(191): 191 per 4 fa 764, appena sotto il limite. L'InnoDB moderno con formato di riga DYNAMIC porta il limite a 3.072 byte e l'espediente è obsoleto, ma le colonne di lunghezza 191 che ha generato sono ancora ovunque negli schemi di produzione.

Unità UTF-16 (.length)
Cinque stringhe misurate in quattro modi in Node 22. La colonna grafemi usa Intl.Segmenter; la colonna UTF-16 è ciò che restituisce .length in JavaScript.
StringaUnità UTF-16 (.length)Code pointGrafemiByte UTF-8
La lettera a1111
e con accento acuto, scritta come un solo code point U+00E91112
La stessa lettera scritta come e più un accento combinante U+0065 U+03012213
Emoji pollice in su con tonalità di pelle media (U+1F44D U+1F3FD)4218
Emoji famiglia: uomo, donna, bambina, bambino uniti da tre zero-width joiner117125
Contatore di caratteri per i socialConta i caratteri secondo i limiti di 15 social — X/Twitter, Instagram, TikTok, LinkedIn, YouTube, Threads e altri — con le regole ponderate di X (link = 23, CJK = 2) e stima del thread.Prova lo strumento

Domande frequenti

Perché JavaScript dice che la mia emoji è lunga 2 caratteri?
Perché .length conta unità di codice UTF-16, e ogni emoji sta sopra U+FFFF, quindi richiede una coppia surrogata: due unità. Aggiungi un modificatore di tonalità e diventano quattro. Usa [...str].length per contare i code point, oppure Intl.Segmenter con granularità grapheme per contare ciò che conterebbe un lettore. In una riga: new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(str) ti dà un iterabile di caratteri visibili.
Quanti byte occupa un carattere in UTF-8?
Da uno a quattro, secondo il code point. L'ASCII fino a U+007F occupa 1 byte. Accenti latini, greco e cirillico fino a U+07FF ne occupano 2. Gran parte del piano multilingue di base, CJK incluso, ne occupa 3. Tutto ciò che sta sopra U+FFFF, cioè tutte le emoji, ne occupa 4. Quindi un pollice in su con tonalità sono 8 byte, perché sono due code point da 4 byte ciascuno, e l'emoji famiglia di quattro sono 25 byte: quattro emoji da 4 byte più tre zero-width joiner da 3 byte ciascuno.
Perché il mio SMS di 145 caratteri è stato fatturato come tre messaggi?
Perché un carattere non era nell'alfabeto GSM a 7 bit, quindi l'intero messaggio è passato a UCS-2, a 70 caratteri per segmento. Sopra 70, i segmenti portano un'intestazione di concatenazione che costa sei ottetti, lasciando 67 caratteri ciascuno, e 145 diviso 67 si arrotonda a 3. I colpevoli soliti sono un apostrofo tipografico, una lineetta lunga, il carattere dei puntini di sospensione o un'emoji, tutti inseriti automaticamente da elaboratori di testi e tastiere del telefono. Passa il testo in un contatore di byte e codifica prima di inviare una campagna, non dopo.
Devo salvare un limite in caratteri o in byte?
Applica due limiti, non uno. Un limite in grafemi per ciò che mostri all'utente, perché è il numero che può verificare a occhio. Un limite in byte per archiviazione e trasporto, perché è ciò che colonna, payload e protocollo vincolano davvero. Se puoi averne uno solo, scegli i byte e rendi l'interfaccia onesta al riguardo: un solo byte di troppo è un errore di scrittura, mentre un grafema di troppo è solo un problema estetico.
Le stesse regole valgono per una meta description o un tag title?
No, ed è proprio il caso in cui contare i caratteri è l'istinto sbagliato. I motori di ricerca troncano titoli e descrizioni in base alla larghezza in pixel resa, non al numero di caratteri: un titolo di 60 lettere strette può entrare dove 50 larghe non entrano, e un'emoji in un titolo occupa molta più larghezza di quanto suggerisca il suo singolo grafema. Gli obiettivi di 60 e 155 caratteri sono regole empiriche che approssimano un budget di pixel. Scrivi per il budget di pixel, verifica in un'anteprima dei risultati e usa il contatore di caratteri solo per non scivolare in lunghezze palesemente irrecuperabili.

Articoli che potrebbero interessarti

Tutte le guide
SpiegazioneChe cosa misurano davvero gli indici di leggibilità (e le tre cose che non vedono)Il Flesch Reading Ease e il livello Flesch-Kincaid contano sillabe e lunghezza delle frasi. Nient'altro. Entrambe le formule per intero, un brano valutato da cima a fondo e il trucco della virgola che regala 3,9 anni scolastici senza cambiare una parola.SpiegazioneContare le parole è ambiguo, e ogni strumento risponde in modo diversoUn conteggio di parole è una definizione, non una misura. Abbiamo contato lo stesso paragrafo in quattro modi ottenendo 25, 28, 33 e 38; poi abbiamo contato 50.000 caratteri di prosa ordinaria ottenendo accordo entro il 4,5 %. Lo scarto dipende interamente da composti, cifre e URL.GuidaChe cosa rende buono uno slug di URL: stabilità, leggibilità e il conflitto fra le dueUno slug ha due compiti che tirano in direzioni opposte: è un identificatore permanente ed è un testo leggibile. Lunghezza, trattini, parole vuote, date, caratteri non ASCII e il modello identificatore + slug che ottiene entrambe le proprietà — con i numeri reali di un sito che localizza 1736 slug di strumenti in sei lingue.GuidaTag title, meta description, e che cosa ci fanno i motori di ricercaChe cosa dice la documentazione di Google sulla riscrittura dei titoli e sulla meta description, invece di quel che dice il folklore SEO. Poi la parte misurabile: i titoli vengono troncati per larghezza in pixel, quindi due titoli di esattamente sessanta caratteri possono renderizzarsi a 204,55 pixel di distanza e solo uno sopravvive.SpiegazioneLa densità di parole chiave è una metrica morta, ed ecco che cosa l'ha sostituitaLa densità contava le occorrenze perché il recupero contava le occorrenze. TF-IDF, poi BM25 con la sua curva di saturazione, poi gli embedding l'hanno sostituita. Ecco la stessa pagina da 800 parole valutata in tre modi, e perché i tre non concordano.SpiegazioneContare caratteri contro un limite fissato da qualcun altroUn'emoji vale 1 carattere, o 7, o 11, o 25, a seconda di chi conta. Quale intendono il tuo modulo, il tuo database e il tuo gateway SMS — e un test con un solo incolla per capire con quale hai a che fare.

Strumenti correlati

Fonti

Hai notato un errore in questo articolo?