Đếm ký tự UTF-16 vs code point vs byte: Twitter, SMS, VARCHAR và tiếng Việt có dấu

(Cập nhật: 19 tháng 7, 2026 ) đếm ký tự UTF-16 UTF-8 Unicode emoji VARCHAR Twitter tiếng Việt
Kết luận

“Độ dài chuỗi” không phải một số duy nhất. Ít nhất cần tách: code point, UTF-16 code unit (String.length trong JS), byte UTF-8, và đôi khi grapheme (chữ người dùng thấy). Tiếng Việt có dấu + emoji làm lệch Twitter/X, SMS và VARCHAR. Đo nhanh trên trình duyệt với công cụ đếm ký tự Kawa (số ký tự theo JS length + byte UTF-8, dòng/từ, không cần đăng ký).

Bạn cắt bio.slice(0, 160) cho form, trên máy nhìn ổn — user Việt dán câu có dấu và emoji 👨‍💻 thì API trả too long, hoặc SMS nhà mạng tính 3 segments. Root cause gần như luôn là đang đếm nhầm đơn vị.

Bài viết này giúp bạn

  • Phân biệt code point / UTF-16 / byte / grapheme
  • Hiểu cạm bẫy tiếng Việt có dấuemoji
  • Map sang Twitter/X, SMS, DB VARCHAR
  • Có ví dụ số liệu và checklist trước khi validate độ dài
  • Dùng char-count để soi JS length vs UTF-8 bytes

Bốn thước đo (đừng gộp một chữ “ký tự”)

ThướcNghĩa thực tếVí dụ trong JS / DB
GraphemeCụm người dùng coi là 1 chữ (có thể gồm nhiều code point)Cần Intl.Segmenter hoặc lib
Code pointĐiểm mã Unicode[...str].length hoặc for…of
UTF-16 code unitĐơn vị lưu trong string JS/Java/C# nhiều nơistr.length
UTF-8 byteByte trên mạng / nhiều cột DB / fileTextEncoder().encode(str).length
const s = "👨‍💻"; // man + ZWJ + computer
console.log(s.length);        // 5  — UTF-16 code units
console.log([...s].length);   // 3  — code points
// Grapheme thường = 1 “ký tự nhìn thấy”
const vi = "Xin chào";
console.log(vi.length);                           // 8 (UTF-16 units ≈ code points ở đây)
console.log(new TextEncoder().encode(vi).length); // 9 bytes UTF-8

Chữ à trong “chào” thường là 1 code point (NFC) nhưng 2 byte UTF-8 — đã lệch “8 chữ / 9 byte”. Nếu chuỗi ở dạng NFD (base + combining mark), code point còn tăng.

Mở đếm ký tự, dán Xin chào và một emoji: tool hiển thị số ký tự = length kiểu JS (UTF-16)byte UTF-8 — đủ để bắt lệch trước khi đổ lỗi cho backend.


Tiếng Việt có dấu: NFC, NFD, byte

Hai chuỗi trông giống nhau có thể khác bytes:

NFC "Xin chào" → thường 8 code points, 9 UTF-8 bytes
NFD "Xin chào" → nhiều code points hơn nếu dấu tách riêng, byte cũng khác

Hệ quả:

  • Hash / cache key / chữ ký lệch (cùng bài MD5)
  • VARCHAR theo byte đầy sớm
  • So sánh string trong test fail dù “nhìn giống”

Thói quen tốt: normalize NFC ở biên API trước khi đếm và lưu.

const normalized = input.normalize("NFC");

Twitter / X, SMS, meta description

Limit thực tế hay gặp
Kênh Limit (gần đúng) Đơn vị cần nhớ
X (Twitter) 280 (weighted) Không = JS length; emoji/URL có rule riêng
SMS GSM 7-bit ~160 / segment ASCII/GSM; ít dấu Việt
SMS UCS-2 ~70 / segment Khi có Unicode (nhiều tin Việt)
Meta description SEO ~150–160 hiển thị Pixel/hiển thị ≈ ký tự; vẫn kiểm byte nếu CMS limit
MySQL VARCHAR(n) utf8mb4 n character (thường) Xác nhận version/config — đừng nhầm với byte của utf8 cũ

SMS marketing tiếng Việt

Soạn 120 “chữ” có dấu → tưởng còn dưới 160. Nhà mạng đếm UCS-2 → hết 70/segment → tin bị xé 2–3 đoạn, giá tin nhắn tăng. Trước khi gửi chiến dịch: dán copy vào char-count xem byte + length, và hỏi nhà cung cấp SMS đang UCS-2 hay Unicode segmentation thế nào.

Cắt chuỗi trên frontend

// ❌ Nguy hiểm với emoji / surrogate
bio.slice(0, 50);

// ✅ Cắt theo code point (vẫn chưa = grapheme)
[...bio].slice(0, 50).join("");

// ✅✅ UI “một chữ nhìn thấy” — Intl.Segmenter khi target browser đủ

slice theo UTF-16 có thể cắt giữa surrogate pair → ký tự lạ “.


Case study 1: Form bio VARCHAR(100) + tiếng Việt

API: reject khi Buffer.byteLength(s, 'utf8') > 100.
FE: validate value.length <= 100.

User nhập ~90 chữ Việt có dấu → FE OK, BE fail.
Fix: FE dùng cùng hàm đếm byte (hoặc gọi cùng normalize + đếm) — đối chiếu bằng tool trước khi viết test.

Case study 2: Đếm emoji trong comment

"ok 👨‍💻" — PM đếm 4 “ký tự”; length = 2 + 5 = 7; code point = 2 + 3 = 5; grapheme ≈ 3.
Limit “max 5 characters” trong spec mơ hồ → QA và dev cãi nhau một buổi.

Fix spec: ghi rõ UTF-8 bytes hoặc grapheme clusters hoặc JS length.


Checklist trước khi ship validate độ dài

  1. Spec ghi đơn vị (byte / code point / grapheme / rule SNS)
  2. FE và BE dùng cùng hàm đếm
  3. normalize('NFC') ở input boundary nếu nhận tiếng Việt
  4. Test fixture: ASCII, tiếng Việt có dấu, emoji đơn, emoji ZWJ, CRLF
  5. Không dùng slice UTF-16 cho user-facing truncate mà không kiểm tra
  6. Đo mẫu thật trên char-count — nhìn cả length và bytes

Cách đọc số trên công cụ Kawa

Trên /vi/tools/char-count/:

  • Ký tựString.length (UTF-16 code units) — gần với nhiều validate JS
  • Byte = UTF-8 qua TextEncoder — gần với nhiều API Node/byteLength
  • Dòng / từ — hữu ích bản thảo; SNS/DB thường không dùng “từ” tiếng Việt như English word count

Tool không thay composer X và không mô phỏng segment SMS nhà mạng — nhưng đủ để phát hiện “trông ngắn mà byte dài” trước khi khách complaint.


Tóm tắt nhanh

Nhu cầuNên đếm
Debug str.length / nhiều validate JSUTF-16 units
Payload HTTP, file, nhiều DB byte limitUTF-8 bytes
“User thấy bao nhiêu chữ” (pure UI)Grapheme
Twitter/XRule weighted của X
SMS Việt có dấuGiả định UCS-2 ~70/segment trừ khi provider khẳng định khác

Bookmark lý do: bảng bốn thước đo + case VARCHAR/SMS tiếng Việt — hết cãi “sao 80 chữ mà vẫn over limit” trong team VN.