Cập nhật lần cuối:
Đếm ký tự Emoji: Tại sao một Emoji có thể được tính là nhiều ký tự
Lịch sử Emoji và sự phát triển theo phiên bản Unicode
Bộ emoji trở thành khởi điểm cho sự phổ biến của emoji trên toàn thế giới được tạo ra năm 1999 bởi Shigetaka Kurita tại NTT DoCoMo cho nền tảng di động i-mode - 176 biểu tượng được hiển thị dưới dạng pixel art 12×12. Ban đầu, mỗi nhà mạng di động Nhật Bản (DoCoMo, au, SoftBank) triển khai emoji độc lập, gây ra lỗi hiển thị thường xuyên khi tin nhắn được gửi giữa các nhà mạng. Để giải quyết vấn đề tương thích này, Google và Apple đề xuất chuẩn hóa emoji cho Unicode Consortium, và 722 emoji được chính thức đưa vào Unicode 6.0 năm 2010.
Kể từ đó, số emoji được thêm vào thay đổi rất nhiều giữa các phiên bản. Bảng dưới đây liệt kê những phiên bản tiêu biểu mà số lượng bổ sung có thể tra lại được từ tài liệu công khai.
| Phiên bản Unicode | Năm phát hành | Emoji thêm mới |
|---|---|---|
| 6.0 | 2010 | 722 |
| 7.0 | 2014 | khoảng 250 |
| 8.0 | 2015 | 41 |
| 16.0 | 2024 | 8 |
Tổng số emoji thay đổi theo định nghĩa cách đếm: chỉ tính các code point đơn lẻ, hay tính cả những biến thể phái sinh dựng bằng chuỗi ZWJ và bằng tông màu da. Theo số liệu tổng hợp của Unicode Consortium cho bản Emoji 17.0, con số là 3.953 mục.
Số lượng bổ sung mới đã giảm trong những năm gần đây. Một emoji được đề xuất sẽ bị loại nếu nó vốn đã có thể biểu diễn được bằng emoji hoặc chuỗi hiện có. Người đề xuất phải trình bày bằng chứng về tần suất sử dụng (Google Search, Google Trends, Google Books Ngram Viewer) cùng với tính riêng biệt so với emoji hiện có.
Kích thước byte theo mã hóa: Dữ liệu đo lường
| Emoji | Hiển thị | Code Point | Byte UTF-8 | Byte UTF-16 | Byte UTF-32 |
|---|---|---|---|---|---|
| 😀 (U+1F600) | 1 ký tự | 1 | 4 | 4 | 4 |
| 👍🏻 (U+1F44D U+1F3FB) | 1 ký tự | 2 | 8 | 8 | 8 |
| 👨👩👧👦 | 1 ký tự | 7 | 25 | 22 | 28 |
| 🇺🇸 (U+1F1FA U+1F1F8) | 1 ký tự | 2 | 8 | 8 | 8 |
| 1️⃣ (U+0031 U+FE0F U+20E3) | 1 ký tự | 3 | 7 | 6 | 12 |
| 🏳️🌈 | 1 ký tự | 4 | 14 | 12 | 16 |
Trong UTF-8, các code point ngoài Basic Multilingual Plane (BMP) chiếm 4 byte mỗi cái. UTF-16 biểu diễn chúng dưới dạng surrogate pair (hai đơn vị 16-bit), cũng tổng cộng 4 byte. UTF-32 có chiều rộng cố định 4 byte mỗi code point, giúp tính toán đơn giản nhưng hiệu quả bộ nhớ kém nhất trong ba loại. Emoji gia đình 👨👩👧👦 chiếm 25 byte trong UTF-8 là yếu tố quan trọng khi thiết kế kích thước cột cơ sở dữ liệu.
Cách ZWJ, Variation Selector và Surrogate Pair hoạt động
Unicode sử dụng ZWJ (Zero Width Joiner, U+200D) để kết hợp nhiều code point thành một emoji hiển thị duy nhất. Emoji gia đình 👨👩👧👦 được tạo thành từ "man (U+1F468) + ZWJ + woman (U+1F469) + ZWJ + girl (U+1F467) + ZWJ + boy (U+1F466)" - tổng cộng 7 code point.
Thiết kế này được áp dụng vì việc đăng ký riêng lẻ từng tổ hợp sẽ làm số code point cần thiết phình lên rất nhanh: chỉ riêng một gia đình 4 người với 5 mức tông màu da đã cho 54 = 625 cách kết hợp, và nếu nhân thêm các kiểu cấu trúc gia đình khác nhau thì con số lên tới hàng nghìn. Kết hợp ZWJ cho phép đa dạng thông qua việc kết hợp các khối xây dựng cơ bản, ngăn chặn cạn kiệt code point.
Các ô viền nét đứt là những code point ZWJ không bao giờ hiện ra trên màn hình. Bốn emoji người nằm ngoài BMP nên mỗi cái chiếm một surrogate pair gồm 2 đơn vị mã UTF-16 và 4 byte UTF-8, còn mỗi ZWJ chiếm 1 đơn vị và 3 byte. Cùng một biểu tượng duy nhất sẽ trả về 1 (Swift .count), 7 (Python 3 len()) hoặc 11 (JavaScript .length), chỉ khác nhau ở tầng mà bạn đem ra đếm. Vì chạm tới 25 byte trong UTF-8, bộ ký tự utf8 của MySQL - vốn chỉ hỗ trợ tối đa 3 byte cho mỗi ký tự - không thể lưu được nó.
Ngoài ZWJ, một số code point vô hình kiểm soát hiển thị emoji:
- Variation Selector: U+FE0F (hiển thị emoji) và U+FE0E (hiển thị văn bản). Ví dụ, ❤ (U+2764) hiển thị dưới dạng ❤️ (emoji màu) với U+FE0F, hoặc ❤︎ (ký hiệu văn bản) với U+FE0E. Các ký tự vô hình này thêm vào số byte mà không hiển thị cho người dùng.
- Skin Tone Modifier: U+1F3FB đến U+1F3FF cung cấp 5 cấp độ dựa trên thang Fitzpatrick (phân loại tông da da liễu). Mỗi modifier thêm 1 code point (4 byte).
- Regional Indicator Symbol: Emoji cờ sử dụng 26 Regional Indicator Symbol (U+1F1E6–U+1F1FF) tương ứng với A–Z, kết hợp theo cặp. 🇺🇸 là U+1F1FA (U) + U+1F1F8 (S). Chúng ánh xạ đến mã quốc gia ISO 3166-1, nên các tổ hợp không xác định (ví dụ: U+1F1FF + U+1F1FF) tạo ra hiển thị không xác định.
Biểu diễn chuỗi nội bộ của JavaScript sử dụng UTF-16, nên emoji ngoài BMP (U+10000 trở lên) được biểu diễn dưới dạng surrogate pair - hai đơn vị mã 16-bit. Đây là lý do cơ bản "😀".length trả về 2 thay vì 1. Tách một surrogate pair (high surrogate U+D800–U+DBFF, low surrogate U+DC00–U+DFFF) tạo ra chuỗi không hợp lệ, khiến việc cắt ngắn chuỗi đặc biệt nguy hiểm.
Sự khác biệt đếm và hiển thị Emoji theo nền tảng
- X (Twitter): Emoji được đếm nội bộ là 2 ký tự mỗi cái, bất kể độ phức tạp. Ngay cả emoji gia đình ZWJ 👨👩👧👦 cũng chỉ đếm là 2. Trong giới hạn 280 ký tự, sử dụng nhiều emoji có thể khiến bài đăng bị cắt ngắn nếu bạn không tính đến điều này. Các ký tự CJK (tiếng Nhật, tiếng Trung, tiếng Hàn) cũng được tính là 2 cho mỗi ký tự, nên trọng số của emoji và của văn bản tiếng Nhật là ngang nhau.
- Instagram: Trong giới hạn 2.200 ký tự caption, emoji được đếm là 1 ký tự mỗi cái. Vì đơn vị đếm khác với X, nếu bạn dùng lại nguyên một nội dung cho cả hai nơi thì chỉ riêng phía X sẽ không vừa giới hạn.
- LINE: Tin nhắn văn bản đếm emoji là 1 ký tự. Tuy nhiên, sticker emoji độc quyền của LINE và emoji Unicode được xử lý khác nhau nội bộ, điều này quan trọng cho nhà phát triển làm việc với LINE Messaging API.
- Slack: Nội dung tin nhắn đếm emoji là 1 ký tự, nhưng emoji tùy chỉnh sử dụng shortcode (ví dụ:
:thumbsup:), và toàn bộ chuỗi shortcode bao gồm dấu hai chấm tính vào giới hạn ký tự. - SMS: Chỉ cần một emoji cũng chuyển mã hóa từ GSM-7 sang UCS-2, giảm giới hạn mỗi tin nhắn từ 160 xuống 70 ký tự. Lượng nội dung nhét được vào một tin nhắn chỉ còn hơn 40% so với trước, nên số tin nhắn cần gửi và chi phí đều tăng vọt.
Cùng một emoji cũng có thể trông khác biệt đáng kể giữa các nền tảng Apple, Google, Samsung và Microsoft. Ví dụ, 🔫 (súng lục) đã được Apple đổi thành thiết kế súng nước đồ chơi, và các nhà cung cấp khác cũng làm theo - nhưng vào các thời điểm khác nhau. Khi giao diện emoji quan trọng trong marketing hoặc thiết kế UI, hãy xem trước nội dung trên các nền tảng lớn trước khi xuất bản.
Sự khác biệt số ký tự giữa các ngôn ngữ lập trình
Cùng một emoji tạo ra giá trị length khác nhau tùy thuộc vào ngôn ngữ lập trình, vì mỗi ngôn ngữ sử dụng biểu diễn chuỗi nội bộ khác nhau.
| Ngôn ngữ / Phương thức | "😀" | "👍🏻" | "👨👩👧👦" | Đơn vị đếm |
|---|---|---|---|---|
JavaScript .length | 2 | 4 | 11 | Đơn vị mã UTF-16 |
JavaScript [...str].length | 1 | 2 | 7 | Code point Unicode |
Python 3 len() | 1 | 2 | 7 | Code point Unicode |
Rust .len() | 4 | 8 | 25 | Byte UTF-8 |
Rust .chars().count() | 1 | 2 | 7 | Code point Unicode |
Swift .count | 1 | 1 | 1 | Cụm grapheme |
Go len() | 4 | 8 | 25 | Byte UTF-8 |
Java .length() | 2 | 4 | 11 | Đơn vị mã UTF-16 |
Chỉ Swift đếm theo cụm grapheme, trả về 1 cho bất kỳ emoji nào bất kể độ phức tạp nội bộ. JavaScript và Java sử dụng UTF-16 nội bộ, nên emoji ngoài BMP được đếm là 2 (surrogate pair). Rust và Go trả về số byte, khiến chúng không phù hợp để đếm ký tự mà không có xử lý bổ sung. Nhà phát triển phải hiểu chính xác length của ngôn ngữ họ trả về gì.
Lỗi phổ biến và cách tránh
- Sử dụng
String.lengthcho giới hạn ký tự trong JavaScript:"👨👩👧👦".lengthtrả về 11, nhưng số ký tự hiển thị là 1. Xác thực biểu mẫu sử dụngString.lengthsẽ hạn chế không công bằng đầu vào chứa nhiều emoji. Sử dụngIntl.Segmenterđể đếm cụm grapheme chính xác. - Cắt ngắn chuỗi giữa chuỗi ZWJ: Cắt emoji gia đình 👨👩👧👦 tại vị trí byte hoặc đơn vị mã tùy ý sẽ phá vỡ chuỗi ZWJ, khiến các emoji người riêng lẻ hiển thị tách biệt - hoặc tệ hơn, tạo ra ký tự không hợp lệ. Luôn cắt ngắn tại ranh giới cụm grapheme.
- Sử dụng MySQL
utf8thay vìutf8mb4: Bộ ký tựutf8của MySQL chỉ hỗ trợ tối đa 3 byte mỗi ký tự, không thể lưu trữ emoji (code point 4 byte ngoài BMP). Bạn phải sử dụngutf8mb4. Ngoài ra, số 255 trongVARCHAR(255)là số ký tự chứ không phải số byte: một emoji gia đình gồm 7 code point tự nó đã chiếm 7 ký tự, nên chỉ 36 emoji như vậy là cột đã chạm giới hạn. Kích thước hàng và độ dài index thì lại được tính riêng theo byte. PostgreSQL hỗ trợ toàn bộ phạm vi UTF-8 theo mặc định, nên vấn đề này không phát sinh. - Khớp emoji bằng regex cơ bản:
/./trong JavaScript chỉ khớp một nửa của surrogate pair. Sử dụng/./u(cờ Unicode) để khớp toàn bộ code point, nhưng lưu ý rằng dù có cờuhayvthì.vẫn chỉ khớp đúng một code point. Để khớp trọn một chuỗi ZWJ như một đơn vị duy nhất, hãy kết hợp cờv(Unicode Sets, ES2024) với\p{RGI_Emoji}. - Lạm dụng emoji trong dòng tiêu đề email: Một số ứng dụng email không hiển thị emoji chính xác, hiển thị văn bản lỗi hoặc khoảng trống. Đối với email doanh nghiệp, an toàn hơn khi giảm thiểu sử dụng emoji do hiển thị không thể đoán trước trên các môi trường của người nhận.
Hướng dẫn cho nhà phát triển: Đếm Emoji chính xác
- Phân đoạn cụm grapheme với
Intl.Segmenter: Được chuẩn hóa trong API quốc tế hóa (ECMA-402),Intl.Segmenterchia chuỗi theo cụm grapheme - đơn vị mà người dùng nhận thức là ký tự đơn.[...new Intl.Segmenter().segment(str)].lengthcho số ký tự hiển thị chính xác cho bất kỳ emoji nào. Tính đến tháng 8 năm 2026, API này có sẵn trong Chrome 87+, Safari 14.1+, Node.js 16+ và Firefox 125+. - Phát hiện và loại bỏ emoji bằng regex: Thuộc tính Unicode escape
/\p{Emoji_Presentation}/uphát hiện emoji trong chuỗi. Lưu ý rằng\p{Emoji}cũng khớp với chữ số (0-9) và #, nên hãy sử dụng\p{Emoji_Presentation}hoặc\p{Extended_Pictographic}khi chỉ nhắm đến emoji hình ảnh. - Bộ kiểm thử emoji toàn diện: Khi kiểm thử, hãy bao gồm tối thiểu 5 danh mục sau: (1) emoji cơ bản (😀), (2) có modifier tông da (👍🏻), (3) chuỗi ZWJ (👨👩👧👦), (4) cờ (🇺🇸), và (5) chuỗi keycap (1️⃣).
- Phương pháp tốt nhất cho thiết kế cơ sở dữ liệu: Trong MySQL, hãy dùng
utf8mb4và nhớ rằng N trongVARCHAR(N)là số ký tự: khi chọn N, hãy tính trên giả định một cụm grapheme có thể chiếm tới 7 ký tự. Còn khi ước lượng kích thước hàng và độ dài index, hãy quy đổi mỗi ký tự tối đa 4 byte. Đối với ứng dụng chat sử dụng nhiều emoji, hãy cân nhắc sử dụng kiểuTEXTthay thế.
Kết luận
Đếm emoji phức tạp hơn vẻ ngoài. Các nền tảng và ngôn ngữ lập trình khác nhau đếm emoji khác nhau. Sử dụng Bộ đếm ký tự để có số ký tự chính xác tính đến độ phức tạp của emoji.