We dropped GSAP from this site and got 31kB gzipped back. It was buying one effect, a staggered word reveal on the hero headline, which turned out to be about thirty lines of DOM work and a single CSS transition. Homepage JavaScript went from roughly 46kB to 14.6kB.
Measured on 8 September 2026. The widget has grown since (the redesign added host isolation and a generated palette); the live figure is on the features page. The argument below is about the 31kB we removed, which has not changed.
The DFYe homepage sells a chat widget on the grounds that it is 7.1kB gzipped and does not slow your site down. It would be embarrassing to make that argument on a page carrying 31kB of animation library, so we measured what ours was actually costing.
What the build said
dist/_astro/index.js 70.85 kB │ gzip: 27.88 kB ← gsap core
dist/_astro/SplitText.js 7.34 kB │ gzip: 3.47 kB
dist/_astro/lenis.js 18.71 kB │ gzip: 5.45 kB
GSAP core plus SplitText: 31.35kB gzipped. Lenis, the smooth-scroll library: 5.45kB.
The honest question is what each one was buying.
Lenis was buying smooth scroll, which is a genuinely hard thing to do well, is the single biggest reason a site feels expensive, and would take far more than 5kB to reimplement badly. It stayed.
GSAP was buying one effect: the words of the hero headline rising into place with a stagger.
What that effect actually is
Walk the headline’s DOM, wrap each word in a span with overflow: hidden, wrap the word
itself in a second span, push the inner span down by 108%, then release them all with a
staggered transition delay.
That is the whole thing:
const wrapWords = (node) => {
for (const child of Array.from(node.childNodes)) {
if (child.nodeType === Node.TEXT_NODE) {
const frag = document.createDocumentFragment();
for (const part of (child.textContent ?? '').split(/(\s+)/)) {
if (!part) continue;
if (/^\s+$/.test(part)) { frag.appendChild(document.createTextNode(part)); continue; }
const outer = document.createElement('span');
outer.className = 'split-line'; // inline-block, overflow hidden
const inner = document.createElement('span');
inner.className = 'split-word';
inner.textContent = part;
outer.appendChild(inner);
frag.appendChild(outer);
}
node.replaceChild(frag, child);
} else if (child.nodeType === Node.ELEMENT_NODE && child.tagName !== 'BR') {
wrapWords(child); // so <em> and <br> survive
}
}
};
Then set the initial state, and release on the second animation frame so the browser has actually painted the start position before the transition begins:
words.forEach((w, i) => {
w.style.transform = 'translateY(108%)';
w.style.opacity = '0';
w.style.transition =
`transform .9s cubic-bezier(.22,.7,.2,1) ${i * 28}ms, opacity .7s ease ${i * 28}ms`;
});
requestAnimationFrame(() =>
requestAnimationFrame(() =>
words.forEach((w) => { w.style.transform = ''; w.style.opacity = '1'; })));
The two details that are easy to get wrong
The wrapper must be inline-block, not block. SplitText’s own line wrapper is a block
element because it wraps a line. Ours wraps a word, and a block wrapper puts every word
on its own line. That bug is invisible in code review and obvious in a screenshot, which is
a decent argument for screenshotting your own work before you ship it.
You need two animation frames, not one. With one, the browser can batch the initial and final styles into the same layout pass and the transition never runs. The element simply appears in its final position.
What it cost and what it saved
Thirty lines, one CSS rule, and about twenty minutes including the inline-block bug.
Homepage JavaScript before: ~46kB gzipped. After: ~14.6kB gzipped, of which 7.1kB is the product itself and 5.45kB is Lenis.
When should you keep GSAP instead?
GSAP is superb, it is free for commercial use since April 2025 including the plugins that used to be paid, and if you are building timeline-driven scroll choreography you should use it rather than hand-rolling a worse version.
The argument is narrower: we imported a 31kB dependency for one effect, and we only found out because we looked at the build output. Most projects never look. If your bundle has a library in it that is doing one job, it is worth ten minutes to find out how big that job actually is.
Answers to the usual questions
How much does GSAP actually cost in bundle size?
On this build, 27.88kB gzipped for the core plus 3.47kB for SplitText, so 31.35kB in total. That figure comes from the Vite build output rather than from a documentation page, which matters: tree shaking and your own import pattern change it, so the number on someone else’s blog is not your number. Run your build, read the gzip column, and you have the real figure for your site.
Can you replace SplitText with plain JavaScript?
For a word-level reveal, yes, in about thirty lines. Walk the DOM, wrap each word in a clipping span and an inner span, set a transform and a staggered transition delay, then release. SplitText does far more than this, including line-level splitting that reflows correctly when the container resizes, so the answer changes the moment you need lines rather than words, or need the split to survive a font swap.
Why does the word wrapper need to be inline-block?
Because it wraps a word, not a line. SplitText’s own line wrapper is a block element, and copying that puts every single word on its own line. The bug is invisible in code review, since the CSS is correct in isolation and the JavaScript is correct in isolation, and it is completely obvious in a screenshot. That is a decent argument for looking at your own work in a browser before shipping it.
Why do you need two animation frames instead of one?
With one, the browser can batch the initial and final styles into the same layout pass and the transition never runs: the element simply appears in its final position. Waiting two frames guarantees the start position is painted before the change to the end position is applied. It is the same class of problem as reading offsetHeight to force a reflow, handled by waiting rather than by measuring.
Is this an argument against using GSAP?
No. GSAP is excellent and has been free for commercial use, including its former Club plugins, since April 2025. The argument is narrower: we imported a 31kB dependency for one effect and only found out because we read the build output. Most projects never look. If you are building timeline-driven scroll choreography, reach for GSAP rather than hand-rolling a worse version of it.
The same habit applied to a market instead of a bundle produced the WordPress chat plugin scan, and applied to the product category itself it produced what an AI chatbot for a website actually is. The size budget this protects is described on the features page, and you can watch the result on the live demo.