Cutting 44% of My JavaScript by Deleting Three Dependencies
The public pages shipped over a megabyte of JavaScript. Three packages accounted for most of it, and all three were doing work CSS already does.
Table of contents
My public pages were shipping between 780 KB and 1.15 MB of JavaScript. For a portfolio and a blog. Pages that are, functionally, text.
Removing three dependencies cut that by 18–44% depending on the route. None of the removals cost a feature.
The three
A WebGL hero — 885 KB. three, @react-three/fiber and drei, fetched immediately after hydration, rendering an animated background inside the largest-contentful-paint viewport and running a permanent requestAnimationFrame loop for as long as the tab stayed open.
It was a gradient with movement. It is now a CSS gradient with movement. On a phone, the difference to the visitor is that the page arrives sooner and the battery lasts longer.
An animation library — ~190 KB, on every page. Eleven uses across the codebase. All eleven were a fade-in on scroll, a hover lift, or a progress bar. CSS transitions and @keyframes cover all three, and the browser runs them off the main thread.
A typewriter effect. Not a dependency, but the same category of mistake: a setTimeout chain re-rendering a React component once per character, forever, in a loop that never terminated. It is a CSS keyframe cycler now.
The pattern underneath
Each of these was a JavaScript solution to a problem the platform already solves. That is the whole diagnosis. Gradients, transitions, hover states, scroll-triggered reveals — the browser does all of this natively, and has for years.
The reason it happens is that reaching for a package is the fastest path at the moment you are writing the code. The cost does not appear until later, and it appears on someone else's device.
The one that was not a dependency
The scroll-reveal component was creating one IntersectionObserver per instance. With 165 instances site-wide, that is 165 observers, each holding a callback that set React state and triggered a re-render.
One shared observer that toggles a CSS class does the same job:
const observer = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
e.target.classList.add('is-visible')
observer.unobserve(e.target)
}
}
}, { rootMargin: '0px 0px -10% 0px' })
No state, no re-render, and it stops watching each element once it has fired.
How to find yours
Do not guess. Build with the bundle analyser and sort by size — the biggest offender is usually one you forgot was installed. Then for each large package, ask a narrow question: what does this actually do on the page?
If the answer is a visual effect, CSS probably does it. If the answer is a date format, Intl probably does it. If the answer is real application logic, keep it.
What I would not cut
Worth saying, because this kind of post invites the opposite mistake: I did not remove anything that was doing real work. The Supabase client stays. The analytics stay. Accessibility behaviour stays.
The target was never "less JavaScript." It was JavaScript that was not earning its transfer cost. Three packages, one megabyte, zero features lost — the ones left are there because something would break without them.
Get the AI Marketing Prompt Pack
30+ tested prompts for images, captions, video scripts, keywords, and full content systems, delivered instantly.
Browse all free guides →Want to implement this with guidance?
Santosh helps founders turn insights like this into real systems.
External Resources
Further Reading & Tools
Content Marketing Institute
Annual content marketing benchmarks — budgets, channels, and outcomes
HubSpot Marketing Blog
Data-driven marketing research, inbound strategy, and content guides
Semrush Blog
SEO, content, and digital marketing research and strategy guides
Marketing Week
UK's leading marketing news, strategy insight, and industry research