Skip to main content
text-tools•5 min read•Updated 2026-10-15

How Word, Grapheme, and UTF-8 Byte Counters Actually Work

Understand why Unicode grapheme clusters, UTF-16 code units, and UTF-8 byte lengths differ across social media, SMS, and SEO limits.

Engineering Summary & Key Takeaways

  • JavaScript String.prototype.length counts 16-bit UTF-16 code units, not user-visible characters or database UTF-8 bytes.
  • The ECMAScript Intl.Segmenter API (UAX #29) segments text into true user-perceived grapheme clusters, words, and sentences across international scripts.
  • Multi-codepoint Zero-Width Joiner (ZWJ) sequences and accented characters can occupy 4 to 25 UTF-8 bytes while rendering as a single visible character.
  • Social platforms, SEO snippets, SMS gateways, and SQL VARCHAR(N) columns each measure length using different underlying units.

1. Why Graphemes, UTF-16 Code Units, and UTF-8 Bytes Differ

In ASCII text (characters U+0000 through U+007F), one visible character equals one UTF-16 code unit and one UTF-8 byte. Outside basic ASCII, those three measurements diverge immediately.

JavaScript stores strings internally as sequences of 16-bit UTF-16 code units. Any Unicode code point above U+FFFF (such as mathematical symbols, historic scripts, or pictographs) requires a surrogate pair of two UTF-16 code units (string.length === 2) and four UTF-8 bytes. When multiple code points are joined by a Zero-Width Joiner (U+200D) or followed by a combining diacritical mark (such as "e" + U+0301), a single visible grapheme cluster can span 2 to 11 UTF-16 code units and up to 25 UTF-8 bytes.

String Measurement Comparison Across Encoding Layers
Sample SequenceVisible Graphemes (Intl.Segmenter)UTF-16 Code Units (str.length)UTF-8 Byte Length (TextEncoder)
ASCII ("Hello")555 bytes
Precomposed Latin ("Caf\u00E9")445 bytes
Combining Accent ("Cafe\u0301")456 bytes
Sinhala Word ("\u0D86\u0DBA\u0DD4")239 bytes
4-Person ZWJ Sequence11125 bytes

2. Word and Sentence Boundaries Under Unicode UAX #29

Naive word counters that split strings on whitespace (text.split(/\s+/)) miscount punctuation-only tokens as words, break on em-dashes, and fail completely on scripts without spaces between words (such as Japanese, Thai, or Chinese).

FetchToolKit uses the browser-native Intl.Segmenter API configured with granularity: "word" and granularity: "sentence", filtering segments where segment.isWordLike is true. This follows Unicode Standard Annex #29 (UAX #29) text boundary rules directly inside your browser.

Measuring Graphemes, Words, and UTF-8 Bytes Locallytypescript
const graphemeSegmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const wordSegmenter = new Intl.Segmenter('en', { granularity: 'word' });

const graphemes = Array.from(graphemeSegmenter.segment(input)).length;
const words = Array.from(wordSegmenter.segment(input)).filter((s) => s.isWordLike).length;
const utf8Bytes = new TextEncoder().encode(input).byteLength;

3. Reference Table of Social, SEO, and Messaging Character Limits

When writing meta descriptions, social posts, or SMS notifications, exceeding a platform cap truncates your message or splits an SMS into multiple billed segments.

Common Web, SEO, and Messaging Length Thresholds
Platform / FieldTarget LimitCounting MethodTruncation Behavior
SEO Meta Description150–160 charsVisible characters / pixel widthEllipsis (...) in search results
YouTube Video Title100 charsUTF-8 / Grapheme countHard form validation cap at 100
Instagram Profile Bio150 charsGrapheme clustersHard cap upon saving profile
X / Twitter Post280 charsWeighted Unicode code points (CJK/Emoji = 2)Hard cap on standard accounts
SMS Segment (GSM-7 / UCS-2)160 / 70 chars7-bit GSM charset vs. 16-bit UCS-2Splits into multi-part 153/67-char segments

Frequently Asked Questions

Why does a single non-ASCII character reduce an SMS limit from 160 to 70 characters?

Standard 160-character SMS messages use 7-bit GSM-7 encoding (1,120 bits max). Including even one character outside the GSM-7 table (such as curly smart quotes or non-Latin letters) forces the carrier to switch the entire message to 16-bit UCS-2 encoding, reducing the single-segment cap to 70 code units.

How does FetchToolKit keep the browser responsive when counting 500,000 words?

Whenever the input payload exceeds 100 KB (102,400 UTF-8 bytes), TextCounterEngine automatically offloads Intl.Segmenter traversal and keyword frequency aggregation to a background Web Worker (text.worker.ts).

Related Local Utilities