DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • A Tool to Ease Your Transition From Oracle PL/SQLs to Couchbase JavaScript UDFs
  • Stop Hand-Rolling Chat UIs: Streaming LLM Tokens Into React Native Without the Jank
  • Deploying an Enterprise LLM Chatbot on Databricks With RAG, MLflow, Vector Search, and Model Serving
  • The Embedding Model You Choose Matters More Than Your LLM

Trending

  • Chat with Your Oracle Database: SQLcl MCP + GitHub Copilot
  • Microservices Architecture in Production: 7 Engineering Decisions That Determine Success or Failure
  • A Complete Guide to Creating Vector Embeddings for Your Entire Codebase
  • Reliability Challenges in Multi-Cloud Environments: Why Two Clouds Are Often Harder Than One
  1. DZone
  2. Coding
  3. JavaScript
  4. Text Analysis Without a Backend: Replacing an LLM Call With Intl.Segmenter and 60 Lines of JavaScript

Text Analysis Without a Backend: Replacing an LLM Call With Intl.Segmenter and 60 Lines of JavaScript

Build deterministic browser-based text tools instead of LLM APIs to reduce cost, latency, privacy risks, and nondeterministic results.

By 
Kevin Brown user avatar
Kevin Brown
·
Aug. 24, 26 · Analysis
Likes (0)
Comment
Save
Tweet
Share
82 Views

Join the DZone community and get the full member experience.

Join For Free

Last spring, I had six small text features to build: flag filler phrases in a draft, score sentence-length variation, format a citation, check a document against a rubric. My first design put all six behind an API route that called a model. It worked in an afternoon.

Then I priced it. Anthropic lists Claude Fable 5 at $10 per million input tokens and $50 per million output. A 700-word draft plus instructions runs about 1,500 input tokens, and users hit the button five or six times per session while they edit. The bill is survivable. The rest of the tradeoff is not.

Every keystroke a user typed would leave their machine and land in someone else's logs. Every click added 900ms of round trip to something that should feel like a spellchecker. And two runs over identical input returned different advice, which turns "did my edit help?" into an unanswerable question.

I rewrote all six as deterministic browser code. No API route, no server, no network. This is what that took, and where the approach breaks.

What a Heuristic Actually Catches

The honest framing is that heuristics and models solve different problems, and half the features people route to an LLM belong in the first category.

A model is worth paying for when the task needs world knowledge or judgment: Is this argument coherent, does this paragraph follow from the last one, is this claim supported? A regular expression cannot do any of that.

But "does this text contain the phrase in order to" is a lookup. "How much do sentence lengths vary" is arithmetic. "Should of be capitalized in this title" is a rule from a style manual, written down, unchanged since 2019. Sending those to a probabilistic system buys you latency and nondeterminism in exchange for nothing.

The six tools I run in production all fall in the second category. They ship as static pages with inline scripts, no build-time secrets, and no runtime dependencies.

Sentence Segmentation Without a Regex You Will Regret

Every metric below needs sentence boundaries, so this is the piece to get right first.

Splitting on /[.!?]+\s+/ collapses under real prose. Run it over four ordinary lines and watch:

Code language: Text

Plain Text
 
IN   : The file cost $3.50. It shipped on Jan. 5 anyway. naive: ["The file cost $3.50", "It shipped on Jan", "5 anyway."] 
IN   : He said "stop." Then he left. naive: ["He said \"stop.\" Then he left."]


One false split, one missed split, and the abbreviation list you are about to write will never end.

The browser ships an ICU-backed segmenter instead:

Code language: JavaScript

JavaScript
 
const SEG = new Intl.Segmenter('en', { granularity: 'sentence' }); 
const raw = (text) =>  [...SEG.segment(text)].map((s) => s.segment.trim()).filter(Boolean);


ICU gets both of those cases right, along with 9 a.m., decimals and section numbers like 2.1. It has one failure I hit in production, and it is worth knowing before you ship: it breaks after title abbreviations.

Code language: Text

Plain Text
 
IN  : She met Dr. Chen last week. The draft grew by 3.5 pages.
ICU : ["She met Dr.", "Chen last week.", "The draft grew by 3.5 pages."]


The repair is a merge pass over the output rather than a rewrite of the splitter. If a segment ends in a known title, glue the next one onto it:

Code language: JavaScript

JavaScript
 
const TITLE_END = /(^|\s)(Dr|Mr|Mrs|Ms|Prof|Sr|Jr|St|vs|Fig|No)\.$/i;

function sentences(text) {
  return raw(text).reduce((out, part) => {
    const prev = out[out.length - 1];
    if (prev && TITLE_END.test(prev)) out[out.length - 1] = `${prev} ${part}`;
    else out.push(part);
    return out;
  }, []);
}


Verified against the cases above:

Code language: Text

Plain Text
 
["Dr. Chen wrote 3.5 pages.", "She revised twice."]
["She met Dr. Chen last week.", "The draft grew by 3.5 pages."]
["The file cost $3.50.", "It shipped on Jan. 5 anyway."]
["We deployed at 9 a.m.", "Nobody noticed."]
["He said \"stop.\"", "Then he left."]
["Prof. Ada Lovelace vs. Mr. Babbage.", "Round one."]


That is a twelve-entry list against the open-ended one the naive regex demands, because ICU already covers the numeric and punctuation cases that make abbreviation lists grow.

Intl.Segmenter landed in Chrome 87, Safari 14.1 and Firefox 125, so a 2026 audience has it. It also does granularity: 'word', which matters the moment a user writes in Thai or Japanese, where whitespace tokenization returns one enormous token. Guard it if you support older embedded webviews:

Code language: JavaScript

JavaScript
 
const hasSegmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl;


Phrase Matching That Does Not Fire on Substrings

The naive filler checker uses indexOf, then reports "just" inside "adjustment" and loses the user's trust in the first thirty seconds.

Build one alternation with word boundaries, compile it once, and keep the phrase list in data rather than code:

Code language: JavaScript

JavaScript
 
const FILLERS = [
  'in order to',
  'it is important to note',
  'at the end of the day',
  'due to the fact that',
  'a wide variety of',
  'needless to say',
];

const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const FILLER_RE = new RegExp(
  '\\b(' + FILLERS.map(escapeRe).join('|') + ')\\b',
  'gi'
);

function findFillers(text) {
  return [...text.matchAll(FILLER_RE)].map((m) => ({
    phrase: m[0],
    index: m.index,
  }));
}


Two details that cost me a rewrite. Compile the RegExp outside the function, because a global-flagged regex carries lastIndex state and rebuilding it per call hides that bug instead of fixing it. And use matchAll rather than a while (re.exec()) loop, which is where that state bites.

The phrase list is the whole product here. Mine came from marking up 200 real drafts by hand, not from asking a model what filler looks like.

Measuring Variation, and the Trap Next to It

Uniform sentence length reads as flat prose. The metric is standard deviation over word counts:

Code language: JavaScript

JavaScript
 
function rhythm(text) {
  const lens = sentences(text).map((s) => s.split(/\s+/).length);
  if (lens.length < 2) return null;

  const mean = lens.reduce((a, b) => a + b, 0) / lens.length;
  const variance =
    lens.reduce((a, n) => a + (n - mean) ** 2, 0) / lens.length;

  return { mean, sd: Math.sqrt(variance), count: lens.length };
}


Low standard deviation is a useful writing signal. It is also, and this is where teams get into trouble, one of the two features commercial AI-text detectors lean on, alongside token-level perplexity.

Do not ship it as one. A peer-reviewed study in Patterns tested seven commercial detectors and found they misclassified more than half of TOEFL essays written by non-native English speakers as machine-generated, while scoring near-perfect on native-speaker samples (full text). Steady sentence patterns are what a second-language writer produces under pressure. If your product tells that user their own writing looks synthetic, you have built a discrimination engine with a progress bar on it.

Report the number as rhythm. Let the writer decide.

Make "No Network" a Test, Not a Promise

Claiming a tool runs locally is easy. Proving it survives the next dependency bump is the engineering.

Two layers. Content Security Policy on the tool pages:

Code language: HTML

HTML
 
<meta data-fr-http-equiv="Content-Security-Policy"
      content="default-src 'self'; connect-src 'none'; img-src 'self' data:;">


connect-src 'none' kills fetch, XMLHttpRequest, WebSocket and sendBeacon. If you run first-party analytics on the same origin, drop to connect-src 'self' and lean harder on the second layer.

That second layer is a Playwright spec that fails the build if anything leaves the origin:

Code language: JavaScript

JavaScript
 
test('clarity checker makes no offsite requests', async ({ page }) => {
  const offsite = [];

  page.on('request', (req) => {
    if (new URL(req.url()).origin !== BASE) offsite.push(req.url());
  });

  await page.goto(`${BASE}/tools/clarity-checker/`);
  await page.fill('#draft', 'In order to be clear, it is important to note this.');
  await page.click('#analyze');

  expect(offsite).toEqual([]);
});


This caught a real regression for me: a font subset I added later pulled from a CDN, which meant the browser advertised the visitor's IP and user agent to a third party on a page whose whole selling point was that nothing left the device. The CSP would have blocked the request in a browser that enforced it. The test told me before a user did.

The Comparison, With Numbers


LLM API route Browser heuristic
First response 600–1,200 ms under 5 ms
Marginal cost ~$0.001 per run zero
Same input, same output no yes
User text leaves device yes no
Works offline no yes
Handles novel phrasing yes no
Judges argument quality yes no
Ships without a backend no yes


The last row decided it for me. Six static pages on a CDN have no runtime to patch, no key to rotate, and no bill that scales with traffic.

When to Call the Model Anyway

I still reach for one, on three conditions.

The task needs judgment rather than lookup. Restructuring an argument, catching a claim the writer never supported, spotting that paragraph four repeats paragraph two. No word list gets there.

The user asked for it explicitly, with the data boundary stated in plain language on the button. Silent exfiltration dressed as a feature is how teams end up in a compliance review.

And the output gets checked. For anything structured, constrain the response with a schema and validate it before it touches your UI, because a model that returns prose where your parser expects an object will do it on a Friday.

Everything else stayed in the browser. Six features, roughly 400 lines of JavaScript total, zero infrastructure, and a p99 that is a rounding error. The default in 2026 is to reach for an API key first. Check whether the problem is a lookup before you do.

JavaScript large language model

Opinions expressed by DZone contributors are their own.

Related

  • A Tool to Ease Your Transition From Oracle PL/SQLs to Couchbase JavaScript UDFs
  • Stop Hand-Rolling Chat UIs: Streaming LLM Tokens Into React Native Without the Jank
  • Deploying an Enterprise LLM Chatbot on Databricks With RAG, MLflow, Vector Search, and Model Serving
  • The Embedding Model You Choose Matters More Than Your LLM

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook