Core answer: Character counting has three different answers: bytes (UTF-8: ASCII = 1 byte, Chinese = 3 bytes, emoji = 4), code points (JS string.length: Chinese = 1, emoji = 2!), and grapheme clusters (what humans call characters: family emoji 👨‍👩‍👧 = 1). WeChat/Weibo/Twitter limits count differently again. For user-facing limits, count graphemes; for storage, count bytes; never trust .length for either.

The three counting modes

TextUTF-8 bytesJS .lengthGraphemes
"hello"555
"你好"622
"🎉"42 (surrogate pair)1
"👨‍👩‍👧"25 (3 emoji + 2 joiners)81
"é" (composed)211
"é" (decomposed e + ´)321

JS fixes: [...str].length counts code points; new Intl.Segmenter().segment(str) counts graphemes; new TextEncoder().encode(str).length counts bytes.

Platform limits (know your battlefield)

PlatformLimitCounting
WeChat 朋友圈~1,500 chars practicalchars
Weibo2,000 (members 5,000)chars
Twitter/X280 (weighted: CJK counts 2)weighted
SMS70 Chinese / 160 ASCII per segmentbytes-ish (UCS-2)
Meta description (SEO)~150–160 chars visiblepixels, roughly

Worked examples

Example 1 — The SMS split. "您好,您的验证码是123456" = 13 chars = 26 UCS-2 bytes — fits one 70-char segment. Add English? Mixed content still UCS-2. Over 70 → 67 chars/segment concatenated SMS, and carriers bill per segment.

Example 2 — Twitter weighting. 140 Chinese chars ≈ 280 weighted units = the whole tweet; an all-English tweet needs ~280 chars. Same limit, half the Chinese content.

Example 3 — Database column. VARCHAR(255) in MySQL utf8mb4 = 255 characters, up to 1,020 bytes; index prefixes limit to 767/3,072 bytes historically — emoji-heavy text blew up many 2015-era schemas.

Example 4 — Frontend counter. Build the counter with Intl.Segmenter so 👨‍👩‍👧 counts as 1 like users expect; fall back to [...str].length on ancient browsers.

Common mistakes and myths

  1. Trusting .length — "🎉".length === 2 breaks naive truncation: str.slice(0,1) yields a lone surrogate = tofu box �.
  2. Byte-budget confusion — a 1 KB limit fits 1,024 ASCII chars but only ~341 Chinese chars in UTF-8.
  3. Normalization traps — "é" as one code point vs e+´ are visually identical but string-unequal; normalize (NFC) before comparing or counting.
  4. Zero-width surprises — ZWJ (U+200D), variation selectors, and combining marks are invisible yet count; stripping them changes meaning (👨‍👩‍👧 → 👨👩👧).
  5. Assuming monospace width — CJK chars render double-width; terminal column counts differ from character counts (wcwidth exists for this).