A real-time text analyzer needs one textarea, one input listener, and a few counting functions. The hard part is deciding what “character” and “word” mean, because JavaScript’s built-in string length does not match what a reader sees once emoji or non-Latin scripts are involved. This guide builds the analyzer with no libraries, explains which count to show and why, and shows how to measure responsiveness instead of assuming it. The result is a tool that updates as you type, with counts that are correct for the text you give it.
Decide what “character” means before you count
JavaScript strings are sequences of UTF-16 code units, so text.length counts code units. That number is useful for some purposes, but it is not the number of characters a person sees. Iterating with spread syntax, [...text], counts Unicode code points, which is closer but still splits some visible characters into several parts. Grapheme clusters are the unit that matches what a user perceives as one character, and Intl.Segmenter can produce them.
As an Amazon Associate I earn from qualifying purchases.
The example below uses a family emoji, which is a sequence of three person emoji joined by zero-width joiners, and a thumbs-up with a skin-tone modifier.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →| Method | What it counts | Family emoji 👨👩👧 | Thumbs-up with skin tone 👍🏽 | Use it for |
|---|---|---|---|---|
text.length |
UTF-16 code units | 8 | 4 | Limits tied to JavaScript string storage |
[...text].length |
Unicode code points | 5 | 2 | Counting Unicode scalar values |
Intl.Segmenter with granularity: "grapheme" |
User-perceived characters | 1 | 1 | A “characters” count shown to people |
For a counter labeled “Characters,” use grapheme segments. If you also want to show the raw length, label it explicitly as UTF-16 length so readers do not confuse the two numbers.
#1 Best Overall
Choose a word-counting method
The simplest word count splits on whitespace. That works for English-style text where spaces separate words, and it gives wrong answers for languages whose words are not separated by spaces, such as Chinese, Japanese, and Thai. It also miscounts punctuation attached to words, and it counts fragments such as an isolated dash as words.
A locale-aware approach uses Intl.Segmenter with granularity: "word". Each segment carries an isWordLike flag, and counting only the segments where that flag is true excludes spaces and punctuation. MDN describes Intl.Segmenter as enabling locale-sensitive text segmentation, which is the reason to pass the page’s language rather than a hard-coded value. (Intl.Segmenter on MDN)
Check browser support before writing the code
MDN labels Intl.Segmenter as Baseline 2024 and says it has been available across the latest devices and browser versions since April 2024, while warning that it may not work on older devices or browsers. Decide your target browsers first. If you need older support, keep a fallback path that uses whitespace splitting and code-point counting, and label the results as approximate in that mode.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesRank #2
- Check for support at startup:
typeof Intl !== "undefined" && typeof Intl.Segmenter === "function". - If the check fails, fall back to
text.trim().split(/s+/)for words and[...text].lengthfor characters. - Show a short note when the fallback is active so readers know the counts are less precise.
Full reference details are in MDN’s internationalization guide. (MDN internationalization guide)
Build the page
Start with a labeled textarea and a results area. The labels should name each count precisely, because the counting rules are part of what the tool is telling the reader.
<label for="source">Your text</label>
<textarea id="source" rows="12"></textarea>
<dl>
<dt>Words</dt><dd id="words">0</dd>
<dt>Characters (user-perceived)</dt><dd id="chars">0</dd>
<dt>UTF-16 length</dt><dd id="utf16">0</dd>
</dl>
Set the lang attribute on the html element so the segmenters can use the correct locale. Without it, the code falls back to English rules.
Count words and characters with Intl.Segmenter
Create each segmenter once, not on every keystroke. Constructing segmenters is more work than calling them, and reusing them keeps the update path lean.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
const input = document.getElementById("source");
const wordsOut = document.getElementById("words");
const charsOut = document.getElementById("chars");
const utf16Out = document.getElementById("utf16");
const locale = document.documentElement.lang || "en";
const wordSegmenter = new Intl.Segmenter(locale, { granularity: "word" });
const graphemeSegmenter = new Intl.Segmenter(locale, { granularity: "grapheme" });
function countWords(text) {
let count = 0;
for (const segment of wordSegmenter.segment(text)) {
if (segment.isWordLike) count++;
}
return count;
}
function countCharacters(text) {
let count = 0;
for (const _ of graphemeSegmenter.segment(text)) count++;
return count;
}
function analyze(text) {
wordsOut.textContent = countWords(text);
charsOut.textContent = countCharacters(text);
utf16Out.textContent = text.length;
}
Counting with a loop avoids building an array of segments just to read its length, which is a small but avoidable allocation on every update.
Connect the input event
The input event fires when the user changes the textarea’s content, so it is the right hook for typing. It does not fire when your script changes .value, so any code that sets the value programmatically must call the analysis function itself. (MDN input event reference)
Rank #4
input.addEventListener("input", () => analyze(input.value));
// Example: a "clear" button or a loaded sample
function loadSample(text) {
input.value = text;
analyze(input.value);
}
analyze(input.value);
Keep the interface responsive
For a small text box, calling analyze directly on every event is usually fine. When text grows or the page does more work per update, the risk is repeated DOM writes and layout work for every keystroke. A single pending animation frame avoids that: each event stores the latest text, and one callback runs before the next repaint.
let pendingText = null;
let frameId = 0;
function scheduleAnalysis(text) {
pendingText = text;
if (frameId) return;
frameId = requestAnimationFrame(() => {
frameId = 0;
analyze(pendingText);
});
}
input.addEventListener("input", () => scheduleAnalysis(input.value));
MDN describes requestAnimationFrame as scheduling a callback before the next repaint, generally at the display’s refresh rate. (requestAnimationFrame on MDN)
What this change does and does not do
- It reduces DOM updates to at most one per frame, no matter how many input events fire in that frame.
- It does not make the counting itself cheaper. If the analysis is expensive, the work still runs once per frame.
- It is not a timer. Browsers generally pause
requestAnimationFramein background tabs, so do not use it for work that must keep running while the page is hidden.
Ways to judge whether the update feels instant
MDN’s general performance guidance gives 50 ms as the budget for idle work, 16.7 ms for animation frames, and 50 to 200 ms as the range for responding to user input. The publication date of that guidance is not stated on the page. These are general targets, not measurements of this analyzer, and they do not guarantee how any particular person perceives a given update. (MDN Web performance guidance)
Best Value
Decide whether analysis belongs in a Web Worker
Move analysis to a Web Worker only when measurement shows that the main thread is blocked for long enough to affect typing or scrolling. Workers run scripts on a separate thread and communicate through messages, which adds copying and message overhead. MDN’s Web Workers guide covers the mechanics. (Using Web Workers on MDN) The sources used for this article do not establish an input-size threshold where a worker becomes worthwhile, so the decision should come from profiling your own text sizes on your target devices.
Measure before you claim speed
Timing a single call with performance.now() is enough to get a first number. Run it on a realistic input size and report the device, browser, input length, and whether you ran it once or several times.
const sample = "Your test paragraph goes here. ".repeat(2000);
const start = performance.now();
analyze(sample);
const elapsed = performance.now() - start;
console.log(`Analysis of ${sample.length} UTF-16 units took ${elapsed.toFixed(2)} ms`);
A single run is noisy. Repeat the measurement several times, discard the first run because of warm-up, and compare the median. Avoid calling the tool “lightning-fast” unless your own numbers, taken this way, support that wording.
Free tools Windows power users keep installed
One-click scans. No signup required.
Test the edge cases
Check each of these inputs before you ship. Each one exposes a different counting or update problem.
- Empty input and whitespace-only input, which should show zero words and zero characters.
- Short text and large text, such as several thousand paragraphs pasted at once.
- Mixed scripts in one paragraph, for example English with Japanese or Thai.
- Punctuation and dashes attached to words, such as “well-known” and “don’t”.
- Combining marks, such as “é” written as “e” followed by a combining acute accent.
- Emoji sequences with skin tones and zero-width joiners, such as the family and thumbs-up examples above.
- A scripted value change through
loadSample(), confirming the counts update without a user event. - Rapid typing and pasting, confirming the frame-based update shows the latest text.
If a case fails, compare the three counts side by side. The differences between UTF-16 length, code points, and graphemes usually explain the result.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




