Concept #027
ArchitectureUTF-8
- encodage
- standard
- fondamentaux
Le principe en une phrase
UTF-8 est un encodage de caractères à longueur variable capable de représenter tous les caractères Unicode, tout en restant rétrocompatible avec l'ASCII historique. C'est le standard dominant du web, utilisé par plus de 98 % des sites.
Le coup de génie d'UTF-8, c'est cette rétrocompatibilité. Un texte en anglais pur encodé en ASCII est déjà du UTF-8 valide, sans aucune modification. C'est cette transition douce, sans casser l'existant, qui lui a permis de conquérir le monde.
Le problème historique : la tour de Babel des encodages
Avant Unicode et UTF-8, chaque langue (ou groupe de langues) avait son propre encodage. L'ASCII couvrait l'anglais avec 128 caractères (7 bits). Pour les langues européennes, ISO-8859-1 (Latin-1) étendait à 256 caractères. Le japonais utilisait Shift-JIS, le russe KOI8-R, le chinois GB2312...
Le résultat : ouvrir un fichier avec le mauvais encodage produisait du charabia. Les caractères accentués français apparaissaient comme des symboles incompréhensibles sur un système configuré en japonais. C'était le règne du mojibake (terme japonais pour "texte corrompu").
Texte original : "Développeur français"
Lu en Latin-1 : "Développeur français" (correct)
Lu en Shift-JIS : "Développeur français" (mojibake)
Unicode : le catalogue universel
Unicode est né de l'ambition de cataloguer tous les caractères de toutes les langues humaines dans un seul référentiel. Chaque caractère reçoit un numéro unique appelé code point, noté U+XXXX.
Quelques exemples :
| Caractère | Code point | Description |
|---|---|---|
| A | U+0041 | Lettre latine majuscule A |
| é | U+00E9 | Lettre latine minuscule e accent aigu |
| U+4E16 | Caractère chinois "monde" | |
| U+1F600 | Emoji visage souriant |
Unicode définit actuellement plus de 150 000 caractères couvrant 161 écritures. Mais Unicode est un catalogue, pas un encodage. Il dit "le caractère é a le numéro 233" mais ne dit pas comment stocker ce numéro en mémoire. C'est le rôle des encodages comme UTF-8, UTF-16 et UTF-32.
UTF-8 : l'encodage à longueur variable
UTF-8 encode chaque code point Unicode sur 1 à 4 octets, selon sa valeur :
| Plage de code points | Octets | Bits utiles | Préfixe binaire |
|---|---|---|---|
| U+0000 -- U+007F | 1 | 7 | 0xxxxxxx |
| U+0080 -- U+07FF | 2 | 11 | 110xxxxx 10xxxxxx |
| U+0800 -- U+FFFF | 3 | 16 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 -- U+10FFFF | 4 | 21 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Exemple concret : encoder "Café"
C → U+0043 → 0x43 → 1 octet : 01000011
a → U+0061 → 0x61 → 1 octet : 01100001
f → U+0066 → 0x66 → 1 octet : 01100110
é → U+00E9 → 0xC3 0xA9 → 2 octets : 11000011 10101001
"Café" occupe donc 5 octets en UTF-8. En ASCII, "Cafe" (sans accent) occupe 4 octets. Le surcoût est minimal pour les langues latines.
La rétrocompatibilité ASCII
Les 128 premiers caractères (U+0000 à U+007F) sont encodés sur un seul octet, dont le bit de poids fort est toujours 0. C'est exactement le même encodage que l'ASCII.
Conséquence : tout fichier ASCII valide est automatiquement un fichier UTF-8 valide. Les outils, protocoles et systèmes qui ne connaissent que l'ASCII continuent de fonctionner sur du UTF-8 anglais. C'est cette compatibilité transparente qui a permis une adoption progressive sans casser l'infrastructure existante.
La propriété de tri des octets
UTF-8 possède une propriété souvent méconnue mais particulièrement utile : trier les octets bruts revient à trier les caractères selon l'ordre Unicode.
C'est possible grâce à la structure des préfixes binaires. Un caractère sur 1 octet commence toujours par 0, un caractère sur 2 octets par 110, sur 3 octets par 1110, sur 4 octets par 11110. Ces préfixes sont ordonnés de telle sorte que l'ordre lexicographique des octets respecte l'ordre des code points.
// En UTF-8, le tri d'octets bruts est cohérent avec l'ordre Unicode
const words = ["banana", "apple", "cherry"];
// Tri par octets bruts (Buffer.compare)
words.sort((a, b) =>
Buffer.from(a, "utf8").compare(Buffer.from(b, "utf8"))
);
// Résultat : ["apple", "banana", "cherry"]Cette propriété simplifie les index de bases de données, les arbres de recherche et les algorithmes de tri sur des données textuelles encodées en UTF-8 -- pas besoin de décoder les octets en code points pour les comparer.
UTF-8 vs UTF-16 vs UTF-32
| UTF-8 | UTF-16 | UTF-32 | |
|---|---|---|---|
| Taille par caractère | 1 à 4 octets | 2 ou 4 octets | 4 octets fixe |
| ASCII compatible | Oui | Non | Non |
| Taille texte anglais | Compacte (1 octet/car.) | Gaspille (2 octets/car.) | Très lourd (4 octets/car.) |
| Taille texte CJK | 3 octets/car. | 2 octets/car. | 4 octets/car. |
| Utilisé par | Web, Linux, protocoles | Windows, Java, JavaScript | Peu utilisé |
UTF-8 domine le web et les systèmes Unix/Linux. UTF-16 est utilisé en interne par JavaScript (String en JS est du UTF-16) et Windows. UTF-32 est rarement utilisé en stockage mais pratique en mémoire quand on a besoin d'un accès par index en O(1) à chaque caractère.
Les pièges courants en développement
Longueur d'une chaîne vs nombre d'octets
const text = "Café";
console.log(text.length); // 4 (caractères)
console.log(Buffer.from(text, "utf8").length); // 5 (octets)
const emoji = "Bonjour ";
console.log(emoji.length); // 10 en JavaScript !
// JavaScript utilise UTF-16 en interne.
// L'emoji est un surrogate pair → compte pour 2 "unités" en .lengthCouper une chaîne UTF-8 au mauvais endroit
Si vous stockez du texte en base de données avec une limite en octets (pas en caractères), vous risquez de couper un caractère multi-octets en plein milieu :
const text = "Café au lait";
const buffer = Buffer.from(text, "utf8");
// Couper à 5 octets : "Caf" + premier octet de "é"
const truncated = buffer.subarray(0, 4).toString("utf8");
// Résultat : "Caf" — le "é" incomplet est perdu ou remplacé par un caractère de remplacement
// La bonne approche : couper au niveau des caractères
const safe = [...text].slice(0, 4).join("");
// Résultat : "Café"Le BOM (Byte Order Mark)
Certains éditeurs (notamment sur Windows) ajoutent un BOM (EF BB BF) au début des fichiers UTF-8. Ce marqueur est invisible mais peut casser des parseurs JSON, des scripts shell ou des headers HTTP. Préférez toujours UTF-8 sans BOM.
UTF-8 en pratique
Quelques règles pour éviter les problèmes d'encodage :
- Déclarez toujours l'encodage explicitement. En HTML :
<meta charset="UTF-8">. En HTTP :Content-Type: text/html; charset=utf-8. - Configurez vos bases de données en UTF-8. En MySQL, utilisez
utf8mb4(pasutf8qui ne supporte que 3 octets et ne couvre pas les emoji). - Utilisez UTF-8 partout. Fichiers sources, configuration, logs, API. Un seul encodage dans tout le système élimine les problèmes de conversion.
- Attention aux opérations sur les octets quand vous manipulez du texte. Taille, découpe, recherche -- travaillez au niveau des caractères, pas des octets.
Résumé
UTF-8 est le standard d'encodage du web moderne. Sa rétrocompatibilité avec ASCII, sa compacité pour les textes latins, et sa capacité à représenter l'intégralité d'Unicode en font le choix par défaut pour pratiquement tous les contextes. Et cerise sur le gâteau : trier les octets bruts revient à trier les caractères dans l'ordre Unicode -- une propriété qui simplifie de nombreux algorithmes de traitement de texte.