DEV Community

Lucky Guy
Lucky Guy

Posted on

How I Built a 100% Client-Side Text Repeater (And Why Your Data Never Leaves the Browser

A few weeks ago a friend asked me for a tool that could take a line like "I Love You ❤️" and repeat it 10,000 times so he could paste it into a WhatsApp message. Easy, right? Just string.repeat(10000).

But then he asked the follow-up that changed how I think about "simple" tools:

"Please make sure it doesn't store my text anywhere. I don't want my private messages on your server."

That's when I realised a text repeater isn't really about repeating text at all. It's about privacy. And the only way to genuinely guarantee it is to never send the data anywhere in the first place.

So I built TextBolt — a text repeater that runs 100% in the browser. No backend, no database, no analytics pinging home with your words. Let me walk you through the interesting bits.

The core: repeating without a server

The heart of the tool is embarrassingly simple. There's no API endpoint — the heavy lifting happens in the browser:

const repeated = input.repeat(count);
Enter fullscreen mode Exit fullscreen mode

That's it. String.prototype.repeat() is one of the most underrated methods in JavaScript. It's much faster than a manual for loop because the engine implements it in native code:

// Slow-ish (still fine for small counts)
let out = "";
for (let i = 0; i < 100000; i++) out += input;

// Fast, native
const out = input.repeat(100000);
Enter fullscreen mode Exit fullscreen mode

For a 13-character string repeated 100,000 times, that's a ~1.3 MB string. repeat() handles it in milliseconds.

The parts that were actually hard

Repeating text is trivial. Everything around it is where the real engineering lives:

1. Handling 100,000 items without freezing the page

A 1.3 MB string is fine in memory, but the moment you try to render it to the DOM naively, you freeze the tab. Two things saved me:

  • Virtualised output — I don't render 100,000 DOM nodes. I render the final repeated string into a single <pre>/<textarea> and let the browser handle scrolling. Creating 100k individual elements is the mistake; a single text node isn't.
  • Deferred rendering — heavy generation happens inside requestAnimationFrame (or a microtask) so the input field stays responsive while the string is built.
async function generate(input, count, separator) {
  // Yield to the event loop so the UI never freezes
  await new Promise(r => requestAnimationFrame(r));

  const body = input.repeat(count);
  const result = input
    .repeat(count)
    .split(input)
    .join(separator); // or a smarter join
  // then write to a single text node
}
Enter fullscreen mode Exit fullscreen mode

2. Separators, prefixes and suffixes

A repeat tool is useless if you can only produce aaaaaa. Users want:

  • a a a a a (space separator)
  • a,a,a,a (comma separator)
  • 1. a, 2. a, 3. a (auto-numbering)
  • --a--a--a (prefix/suffix on each line)

The trick is to generate the final string once, not to build and append pieces in a loop. For the numbered variant:

function numbered(input, count, start = 1, format = (n) => `${n}. `) {
  const lines = new Array(count);
  for (let i = 0; i < count; i++) {
    lines[i] = format(start + i) + input;
  }
  return lines.join("\n");
}
Enter fullscreen mode Exit fullscreen mode

Pre-allocating the array (new Array(count)) avoids the repeated resizing that a push-in-a-loop incurs.

3. Copy, download, and share

Two APIs do most of the work:

  • navigator.clipboard.writeText() for one-click copy.
  • Blob + object URLs for .txt downloads.
function download(filename, content) {
  const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
}
Enter fullscreen mode Exit fullscreen mode

For the WhatsApp share button it's just a wa.me deep link — the URL length limits how much you can pass, so that's a fun constraint to work around for long strings.

Why client-side is the whole point

The nicest part is that this architecture makes privacy a feature, not a promise. Because the text never hits my server, there's nothing for me (or an attacker, or a subpoena) to hand over. There's no request body to intercept. The "we don't store your data" line isn't marketing — it's a consequence of the architecture.

That's worth remembering: if you can push computation to the client, you don't need to be trusted with the data at all. You've removed the attack surface entirely.

What I'd do differently

  • Web Worker for the extreme cases. At 100k+ with heavy prefixes, even repeat() plus string building can jank. Moving it to a Web Worker would keep the main thread silky-smooth.
  • Streaming into the textarea in chunks instead of one giant write, for the same reason.
  • Better memory hygiene — clearing the big string (result = "") when the user changes input, so the browser doesn't hold onto multi-megabyte strings.

Try it, fork the idea

If you want to poke at the real thing, the live tool is at https://textbolt.net/ — you can repeat anything up to 100,000 times, try the fancy-text and blank-text generators, and it's all running in your browser. I built it as a standalone tool plus a few sibling tools (a character counter, a fancy-text generator).

The whole takeaway for your own projects: before you spin up a backend, ask if you actually need one. For a surprising number of "simple" utilities, the browser already has everything you need — and shipping zero servers is the best privacy story you can tell.

If you build your own version, I'd genuinely love to see it. Drop a link in the comments.

Top comments (0)