DEV Community

Cover image for Why I Rebuilt Text Repeater site as a 6KB Static Page
Sonny kk
Sonny kk

Posted on

Why I Rebuilt Text Repeater site as a 6KB Static Page

Last month I needed to send a friend a wall of repeated birthday text for a group chat — the kind of over-the-top gesture that's supposed to be funny, not a five-minute ordeal. I searched, clicked into the first result, and watched my phone struggle to render a page that, underneath the ad units and tracking pixels, was doing nothing more complicated than printing a string in a loop. I ended up building my own version instead, and it's now the live happy birthday 100 times generator I use whenever this comes up — a static page with no backend, no ads, and no reason to choke a browser.

Here's what went into making it actually fast at scale, not just fast in a demo.

The Loop That Almost Wasn't the Problem

Everyone's first pass at "repeat this string N times" looks the same:

let out = '';
for (let i = 0; i < count; i++) {
  out += phrase + '\n';
}
Enter fullscreen mode Exit fullscreen mode

At small counts this is invisible. The trouble starts because JavaScript strings are immutable — every += isn't an append, it's a full copy into a brand-new string. At 100 repeats you'll never notice. At 10,000, you're re-copying an ever-growing string thousands of times, and the garbage collector is stuck cleaning up every version you just threw away.

I replaced it with a single array build and one join, keeping separator logic in the mapping function instead of a running accumulator:

function repeatText(phrase, count, separator = '\n', numbered = false) {
  return Array.from({ length: count }, (_, i) =>
    numbered ? `${i + 1}. ${phrase}` : phrase
  ).join(separator);
}
Enter fullscreen mode Exit fullscreen mode

One allocation, one pass, done. This is the difference between the page hitching for a second on a mid-range phone and the text just appearing.

Formatting Options Without Overengineering Them

The tool supports four separator modes — line breaks, commas, sequential numbering, and full stops — plus two layout modes for how the output displays. I was tempted to build a small state machine for this. It didn't need one. A single object literal maps mode to separator, and the rest is a ternary in the join call above. Sometimes the simplest structure really is the right one; I've overengineered smaller problems than this before and regretted it.

Layout switching between a centered block and a multi-column grid is handled entirely in CSS:

.layout-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 6px;
}
Enter fullscreen mode Exit fullscreen mode

auto-fill with minmax means the grid recalculates its own column count at any screen width, so there's no resize listener and no JS doing layout math the browser already does natively and faster.

Getting Text Out of the Browser

Two export paths, both entirely client-side. Clipboard copy uses navigator.clipboard.writeText(), called synchronously in the click handler — wrap it behind another async call first and Safari in particular will silently refuse it, since it's no longer tied to a direct user gesture.

The .txt download is a Blob and an object URL, nothing else:

function downloadTxt(text, filename = 'happy-birthday-100-times.txt') {
  const url = URL.createObjectURL(new Blob([text], { type: 'text/plain' }));
  const link = Object.assign(document.createElement('a'), { href: url, download: filename });
  link.click();
  URL.revokeObjectURL(url);
}
Enter fullscreen mode Exit fullscreen mode

No server endpoint, no temp storage, nothing to clean up except the object URL itself, which gets revoked the instant the click fires.

What Actually Changed

The finished page loads in well under a second on a throttled connection, scores 100 on Lighthouse, and has zero backend cost because there isn't a backend. None of the pieces here are novel — array-based string building instead of concatenation, native CSS Grid instead of JS positioning, Blob URLs instead of a server round-trip. What made the difference wasn't a clever technique. It was just not skipping the boring ones.

If you've run into a nastier bottleneck building something similar, I'd like to hear about it.

Top comments (0)