Cutting a 625KB homepage down to 477KB without changing a single pixel
How I found 168 redundant internal links in our server-rendered HTML, moved the duplication to the client, and kept the animation identical.
Our tool site has 160 tools. The homepage lists them in four places: a hero marquee, a "popular tools" ticker, the category explorer, and the footer.
That's fine for a human. It is not fine for a crawler.
When I finally dumped the server-rendered HTML and counted, the numbers were embarrassing:
/tools/ links in <html>: 432
unique tools: 160
most repeated tool: 6 times
HTML size: 625 KB
Four sections × 160 tools should be ~264 links. We were serving 168 more than that.
Where the 168 came from
The hero marquee and the popular ticker are infinite scrollers. A CSS marquee needs the content duplicated so it can loop seamlessly — so the server was rendering every item twice:
// every column rendered twice, on the server
{[...column, ...column].map((tool) => (
<li key={tool.slug}><Link href={toolPath(tool.slug)}>…</Link></li>
))}
Hero: 320 links (160 real + 160 clones).
Ticker: 16 links (8 real + 8 clones).
The clones exist purely for the animation. A crawler doesn't care about the animation. It just sees a page that mentions the same 160 URLs four times over, with two of those mentions being noise.
So: render the real items on the server, add the clones in the browser.
Moving the clone to useEffect
const tickerRef = useRef<HTMLUListElement>(null);
useEffect(() => {
const el = tickerRef.current;
if (!el) return;
const source = Array.from(el.children) as HTMLElement[];
for (const li of source) {
const clone = li.cloneNode(true) as HTMLElement;
clone.setAttribute("aria-hidden", "true");
clone.setAttribute("data-ticker-clone", "");
clone.querySelectorAll("a").forEach((a) => a.setAttribute("tabindex", "-1"));
el.appendChild(clone);
}
return () => {
el.querySelectorAll("[data-ticker-clone]").forEach((n) => n.remove());
};
}, []);
And the render side becomes the obvious thing:
{items.map((tool) => (
<li key={tool.slug}><Link href={toolPath(tool.slug)}>…</Link></li>
))}
Server sends 8. Browser appends 8. Animation unchanged.
The part I got wrong first
My first version cloned the whole <ul> and inserted it as a sibling:
el.parentElement?.insertBefore(clone, el.nextSibling); // ← wrong
Which produced two separate scrollers side by side. The animation ran, but the layout was wrong — immediately visible, obviously broken. A screenshot check caught it in under a minute, but it's the kind of thing you don't notice if you only look at numbers.
The fix: clone the <li> elements into the same <ul>. Then translateX(-50%) on one element does what it was designed to do — it scrolls exactly one duplicated track.
const source = Array.from(el.children);
for (const li of source) el.appendChild(li.cloneNode(true));
Two lessons, and they're the same lesson: verify with your eyes, and clone the innermost node rather than the container.
Accessibility, not just SEO
Cloning <li> elements duplicates real <a> tags into the tab order. Screen readers and keyboard users would hit the same tool twice.
Every clone gets:
clone.setAttribute("aria-hidden", "true");
clone.querySelectorAll("a").forEach((a) => a.setAttribute("tabindex", "-1"));
Visually identical, semantically half the size.
The result
before after
HTML size 625 KB 477 KB (-24%)
/tools/ links 432 264 (-39%)
most repeated 6x 4x
unique tools 160 160 (unchanged)
Every unique tool is still linked from the homepage. Nothing became unreachable. The only links that disappeared are the ones that were never meant to exist for a reader.
What I deliberately did not remove
A crawler seeing the same URL in the hero, in the explorer, and in the footer is not a problem — those are genuine, useful internal links. Internal linking from a category page to its tools is one of the cheapest SEO wins there is.
The rule I used:
Remove duplication that exists for an animation. Keep duplication that exists for navigation.
That's the whole decision. The audit took about twenty minutes; the change was under thirty lines across two components.
One small thing I'd do differently
I'd write the "is this a real link or a clone?" audit as a permanent test instead of a one-off grep:
// expect: link count == sum of real sections, and clone markers == 0 in SSR output
it("sends no animation clones to the server", async () => {
const html = await render(<HomePage />);
expect(html).not.toContain("data-ticker-clone");
});
We shipped the fix without that test. It'll work fine until someone refactors the marquee and quietly reintroduces it.
The site is freetoolsy.com — 160 browser-based tools, 159 of which never touch a server.
If you run a content-heavy homepage and have done something similar, I'm curious what number you started from.
Top comments (0)