A lightweight implementation of the Unicode Text Segmentation (UAX #29)
-
Spec compliant: Up-to-date Unicode data, verified by the official Unicode test suites and fuzzed with the native
Intl.Segmenter, and maintaining 100% test coverage. -
Excellent compatibility: It works well on older browsers, edge runtimes, React Native (Hermes) and QuickJS.
-
Zero-dependencies: It doesn't bloat
node_modulesor the network bandwidth. Like a small minimal snippet. -
Small bundle size: It effectively compresses the Unicode data and provides a bundler-friendly format.
-
Extremely efficient: It's carefully optimized for runtime performance, making it the fastest one in the ecosystem—outperforming even the built-in
Intl.Segmenter. Its flat lookup tables also keep the memory footprint far smaller than other libraries. -
TypeScript: It's fully type-checked, and provides type definitions and JSDoc.
-
ESM-first: It primarily supports ES modules, and still supports CommonJS.
Note
unicode-segmenter is now e18e recommendation!
Unicode® 17.0.0
Unicode® Standard Annex #29 - Revision 47 (2025-08-17)
Entries for Unicode text segmentation.
unicode-segmenter/grapheme: Segments and counts extended grapheme clustersunicode-segmenter/intl-adapter:Intl.Segmenteradapterunicode-segmenter/intl-polyfill:Intl.Segmenterpolyfill
And matchers for extra use cases.
unicode-segmenter/emoji: Matches single codepoint emojisunicode-segmenter/general: Matches single codepoint alphanumerics
Utilities for text segmentation by extended grapheme cluster rules.
You can split a string into graphemes by simply consuming the splitGraphemes() generator.
splitGraphemes() is a simple wrapper around graphemeSegments().
import { splitGraphemes } from 'unicode-segmenter/grapheme';
[...splitGraphemes('#️⃣*️⃣0️⃣1️⃣2️⃣')];
// 0: #️⃣
// 1: *️⃣
// 2: 0️⃣
// 3: 1️⃣
// 4: 2️⃣If you need the result as an array, use collectGraphemes() for convenience.
This is a fast version of [...splitGraphemes(str)], 2-4x faster.
import { collectGraphemes } from 'unicode-segmenter/grapheme';
collectGraphemes('#️⃣*️⃣0️⃣1️⃣2️⃣'); // => ["#️⃣", "*️⃣", "0️⃣", "1️⃣", "2️⃣"]However, it collects all grapheme clusters at once, so it's not good for large text or streamed input.
If you need only the count, use countGraphemes(), which is optimized to avoid allocations for segments.
Mostly 2-4x faster than the full segmenter and more GC friendly, so you can call it in a hot path.
import { countGraphemes } from 'unicode-segmenter/grapheme';
'👋 안녕!'.length;
// => 6
countGraphemes('👋 안녕!');
// => 5
'a̐éö̲'.length;
// => 7
countGraphemes('a̐éö̲');
// => 3You can retrieve all grapheme segments using the graphemeSegments() generator.
Each segment is returned as a GraphemeSegmentOutput object containing the segment (the substring), the starting index, and the full input string. This pattern is similar to the Intl.Segmenter API, so it can be a drop-in replacement.
import { graphemeSegments } from 'unicode-segmenter/grapheme';
[...graphemeSegments('a̐éö̲\r\n')];
// 0: { segment: 'a̐', index: 0, input: 'a̐éö̲\r\n' }
// 1: { segment: 'é', index: 2, input: 'a̐éö̲\r\n' }
// 2: { segment: 'ö̲', index: 4, input: 'a̐éö̲\r\n' }
// 3: { segment: '\r\n', index: 7, input: 'a̐éö̲\r\n' }graphemeSegments() exposes some knowledge identified in the middle of the process to support some useful cases.
For example, knowing the Grapheme_Cluster_Break category at the beginning and end of a segment can help approximately infer the applied boundary rule.
import { graphemeSegments, GraphemeCategory } from 'unicode-segmenter/grapheme';
function* matchEmoji(str) {
for (const { segment, _catBegin } of graphemeSegments(input)) {
// `_catBegin` identified as Extended_Pictographic means the segment is emoji
if (_catBegin === GraphemeCategory.Extended_Pictographic) {
yield segment;
}
}
}
[...matchEmoji('1🌷2🎁3💩4😜5👍')]
// 0: 🌷
// 1: 🎁
// 2: 💩
// 3: 😜
// 4: 👍Or build even more advanced one like an Unicode-aware TTY string width utility.
Intl.Segmenter API adapter (only granularity: "grapheme" available yet)
import { Segmenter } from 'unicode-segmenter/intl-adapter';
// Same API with the `Intl.Segmenter`
const segmenter = new Segmenter();Intl.Segmenter API polyfill (only granularity: "grapheme" available yet)
// Apply polyfill to the `globalThis.Intl` object.
import 'unicode-segmenter/intl-polyfill';
const segmenter = new Intl.Segmenter();Utilities for matching emoji-like characters.
import {
isEmojiPresentation, // match \p{Emoji_Presentation}
isExtendedPictographic, // match \p{Extended_Pictographic}
} from 'unicode-segmenter/emoji';
isEmojiPresentation('😍'.codePointAt(0));
// => true
isEmojiPresentation('♡'.codePointAt(0));
// => false
isExtendedPictographic('😍'.codePointAt(0));
// => true
isExtendedPictographic('♡'.codePointAt(0));
// => trueUtilities for matching alphanumeric characters.
import {
isLetter, // match \p{L}
isNumeric, // match \p{N}
isAlphabetic, // match \p{Alphabetic}
isAlphanumeric, // match [\p{N}\p{Alphabetic}]
} from 'unicode-segmenter/general';unicode-segmenter uses only fundamental features of ES2015, making it compatible with most browsers.
To ensure compatibility, the runtime should support:
If the runtime doesn't support these features, it can easily be fulfilled with tools like Babel.
Since Hermes doesn't support the Intl.Segmenter API yet, unicode-segmenter is a good alternative.
unicode-segmenter is compiled into small & efficient Hermes bytecode than other JavaScript libraries. See the benchmark for details.
unicode-segmenter aims to be lighter and faster than alternatives in the ecosystem while fully spec compliant. So the benchmark is tracking several libraries' performance, bundle size, and Unicode version compliance.
- graphemer@1.4.0
- grapheme-splitter@1.0.4
- @formatjs/intl-segmenter@12.1.0
- WebAssembly build of unicode-segmentation@1.12.0 with minimum bindings
- Built-in
Intl.SegmenterAPI
| Name | Unicode® | ESM? | Size | Size (min) | Size (min+gzip) | Size (min+br) | Size (min+zstd) |
|---|---|---|---|---|---|---|---|
unicode-segmenter/grapheme |
17.0.0 | ✔️ | 8,684 | 5,133 | 2,453 | 2,220 | 2,505 |
unicode-segmenter/grapheme (full*) |
17.0.0 | ✔️ | 10,494 | 5,777 | 2,732 | 2,435 | 2,796 |
graphemer |
15.0.0 | ✖️ | 410,435 | 95,104 | 15,752 | 10,660 | 15,911 |
grapheme-splitter |
10.0.0 | ✖️ | 122,254 | 23,682 | 7,852 | 4,802 | 6,753 |
@formatjs/intl-segmenter* |
17.0.0 | ✖️ | 268,301 | 176,759 | 45,988 | 31,701 | 45,370 |
unicode-segmentation* |
15.1.0 | - | 56,529 | 52,439 | 24,108 | 17,343 | 24,375 |
Intl.Segmenter* |
- | - | 0 | 0 | 0 | 0 | 0 |
unicode-segmenter/graphemeprovides count/split-only API which has much less overhead than fullSegmenterimplementation, which includes some redundant codes but tree-shakable.@formatjs/intl-segmenterhandles grapheme, word, and sentence, but it's not tree-shakable.unicode-segmentationsize contains only minimum WASM binary and its bindings to execute benchmarking. It will increases to expose more features.Intl.Segmenter's Unicode data depends on the host, and may not be up-to-date.Intl.Segmentermay not be available in some old browsers, edge runtimes, or embedded environments.
| Name | Bytecode size | Bytecode size (gzip)* |
|---|---|---|
unicode-segmenter/grapheme |
20,015 | 11,075 |
unicode-segmenter/grapheme (full) |
20,224 | 11,213 |
graphemer |
134,085 | 31,770 |
grapheme-splitter |
63,942 | 19,165 |
@formatjs/intl-segmenter |
329,547 | 136,751 |
- The installation size contains compressed assets.
Retained memory per library after segmenting the benchmark corpus, measured by yarn memory-stats:grapheme in isolated Node.js processes (median of 5, GC-stabilized deltas).
| Name | JS heap | External* |
|---|---|---|
unicode-segmenter/grapheme |
209 kB | 42 kB |
graphemer |
2.32 MB | - |
grapheme-splitter |
570 kB | - |
@formatjs/intl-segmenter |
1.95 MB | - |
unicode-segmentation* |
214 kB | - |
Intl.Segmenter* |
86.5 kB | - |
- "External" is the extra ArrayBuffer memory relative to the ~40 kB runtime baseline shared by every row;
unicode-segmenterkeeps its lookup tables in typed arrays, which live there instead of the JS heap. unicode-segmentation's WASM linear memory andIntl.Segmenter's ICU data are allocated by native code and invisible to both columns.
Here is a brief explanation, and you can see archived benchmark results.
Performance in Node.js/Bun/Deno: unicode-segmenter/grapheme has best-in-class performance.
- 5~48x faster than other JavaScript libraries.
- 3.5~8x faster than WASM binding of the Rust's unicode-segmentation.
- 1.3~2.5x faster than built-in
Intl.Segmenter.
Performance in Browsers: The performance in browser environments varies greatly due to differences in browser engines, which makes benchmarking inconsistent, but:
- Still significantly faster than other JavaScript libraries.
- Generally outperforms the built-in in the most browser environments, except the Firefox.
Performance in React Native: unicode-segmenter/grapheme is still faster than alternatives when compiled to Hermes bytecode. It's ~4x faster than graphemer and ~34x faster than grapheme-splitter.
Performance in QuickJS: unicode-segmenter/grapheme is the only usable library in terms of performance.
Instead of trusting these claims, you can try yarn perf:grapheme directly in your environment or build your own benchmark.
-
The Rust Unicode team (@unicode-rs):
The initial implementation was ported manually from unicode-segmentation library. -
Marijn Haverbeke (@marijnh):
Inspired a technique that can greatly compress Unicode data table from his library.