Component libraries made my pages slow
I did not ditch component libraries for philosophical reasons. I ditched them because Lighthouse made me angry.
I had a client project that looked pretty harmless. Marketing site, some forms, a dashboard behind auth. Nothing exotic. I reached for the usual stack: React, a popular headless component library, and a utility-first CSS framework to glue it together.
The build shipped. Everyone was happy. Until we actually measured.
On a fast laptop, it felt fine. On a mid-range Android on 4G, it was sad. Time to interactive was embarrassing. Layout shift felt like a strobe light. The bundle analysis told the story: a truckload of CSS and JavaScript for UI primitives that were “lightweight” according to the homepage marketing.
That project is where I started backing away from component libraries and walking back toward vanilla CSS. Not for nostalgia. For performance.
The hidden cost of the “free” components
Most component libraries sell the same thing: speed of development. Drop in a Button, Modal, Tabs, whatever, and move on to the interesting part of your product. I bought that promise for years.
The problem is that every convenience comes attached to a bunch of invisible weight.
- Base styles that try to cover every use case.
- Theme layers for light, dark, high contrast, brand palettes.
- Layout helpers you never asked for but get anyway.
- Reset styles that clash with the other reset you already use.
- One-size-fits-all animation and interaction defaults.
All those little decisions are baked into CSS that you ship to every user, on every page, every time. Even if you only use 20 percent of the components.
On one project I pulled a CSS coverage report in Chrome. We were shipping roughly 280 KB of CSS, gzipped. Actual usage on a typical route: 18 percent. The rest was dead weight for components that never rendered.
You can call that convenient. I call it waste.
JS for what should be CSS
The other tax is JavaScript that tries to make styling smarter than it needs to be.
Design systems often lean on runtime theming, style props, and fancy responsive APIs. It feels powerful when you are building. It also means that things that could be solved with static CSS end up running in JavaScript on every render.
I profiled one of my own apps and saw a bunch of time spent doing style object merges and generating class names. Not big numbers individually. But dozens of components doing this on every state change adds up.
Toggle a sidebar. Update some text. The browser redraws and the JavaScript work spikes again. All for styles that hardly ever change.
Vanilla CSS with a small set of classes just sits there. The browser is ridiculously good at applying it. No JS bridge. No runtime work. That difference shows up on older devices fast.
Vanilla CSS is not the problem
A lot of developers treat "vanilla CSS" like it is some kind of pre-SaaS caveman tool. I think that attitude is lazy.
Browsers got extremely good at layout, typography, and theming. We have flexbox, grid, custom properties, container queries, prefers-reduced-motion, decent devtools. Most design systems I see could be built with a few hundred lines of real CSS and some discipline.
When I say I went back to vanilla CSS I mean:
- No heavy UI kit that paints over everything.
- No CSS-in-JS runtime on the client.
- No 50kb of reset and design tokens before I even write a line.
What I keep is boring:
- A small base stylesheet: typography, spacing scale, colors, layout primitives.
- A naming convention that I can remember when tired.
- Just enough build tooling to bundle and minify.
It feels like stepping out of a crowded coworking office into a quiet room. Suddenly you can hear the CPU again.
How I test my own bullshit
If I am going to rip out tooling, I want numbers, not vibes. So I started doing something simple on every project: I build two branches.
- Branch A: the "comfortable" way. Component library, utilities, maybe CSS-in-JS.
- Branch B: minimal CSS, hand-rolled layout and components.
Same features. Same pages. Same content. Then I measure both on a throttled Android profile.
The pattern has been boringly consistent:
- Initial CSS size is 2x to 4x smaller with vanilla.
- JS execution time drops, sometimes by a third.
- Largest Contentful Paint improves by a few hundred milliseconds.
- Cumulative Layout Shift becomes easier to tame because I control the DOM and the CSS, not some abstraction that fights me.
This is not about squeezing the last percent for a benchmark screenshot. It is about not wasting users' battery and patience for the sake of my own developer comfort.
“But my team moves slower without components”
This is the usual pushback. If you ditch the design system, everyone will reinvent the button and the app will look like a ransom note. I have seen that happen. It is not pretty.
The solution is not to outsource the entire UI to a library. The solution is to design a small, specific system for your project, and implement it with boring CSS.
What I do now on greenfield work:
- Spend one or two days just on tokens: colors, spacing, radius, shadow, typography.
- Design a handful of primitives: stack, cluster, sidebar, grid, button, input, card.
- Write the CSS for those by hand, using custom properties aggressively.
- Document this in a simple MDX or Storybook style page internally.
After that, the team still has a "component library". It just lives in our codebase, not in node_modules. It is smaller, faster, and fully under our control.
Does that take some time up front? Yes. Do we win that time back when we do not have to fight upstream library decisions for every edge case? Also yes.
The cascade is a feature, not a bug
Most component libraries exist to protect you from the cascade. For good reason. If you treat CSS like inline styling with extra steps, the cascade will punish you.
I stopped fighting it and started using it properly. That changed everything.
Now I rely on:
- Layered base styles that set gentle defaults.
- Single-responsibility utility classes that opt components into variations.
- Custom properties for themes, not runtime JS.
Want a dark theme? I flip a data attribute on <html>, redefine a handful of custom properties, and the entire UI updates. Zero JavaScript. No theme provider wrapped around the tree. No "context mismatch" bugs.
The cascade is free infrastructure. When you mask it behind a component API, you end up building a worse version in JavaScript.
Resetting my mental defaults
The hardest part was not the code. It was my own reflexes.
For a long time my default move was:
- Need a dialog? Install a dialog component.
- Need tabs? Install a tabs package or grab some headless primitives.
- Need a table? Surely there is a table library for that.
Now my default is different:
- Can native HTML do this?
- Can I get 80 percent of the way with a simple pattern and some CSS?
- Is this truly complex enough to justify another dependency?
Surprise: most of the time, the answer to the last question is no.
Dialogs? The native element is decent now. Tabs? It is a few buttons and some CSS. Tables? HTML already has a data table. You just need to style it and maybe sprinkle a tiny bit of behavior.
I am not against libraries for gnarly problems. Accessibility around complex widgets, for example, is hard. But I do not want a 40kb CSS payload for a few ARIA attributes and keyboard handlers.
Performance is a product feature
I build creative web experiences for a living. Fancy animations, micro-interactions, scroll effects, the fun stuff. That work gets a lot easier if you start from a lean baseline.
If the browser is already wading through a swamp of layout thrash and bloated CSS, your nice smooth animation will look like a slideshow on cheaper devices. You feel that especially hard on mobile Safari.
Going back to vanilla CSS cut my baseline cost so much that I can “afford” more interactivity where it actually matters. I get to spend my performance budget on visible delight, not on invisible plumbing.
That tradeoff is very simple in my head now:
- Less framework styling, more room for real features.
- Less abstraction, more direct control.
- Less magic, fewer surprises when I profile.
When I still reach for a library
I am not running a purity cult. I still reach for libraries, but the bar is higher.
I will usually accept a dependency if:
- It solves a real, complex problem that is hard to get right alone. Think date handling, not buttons.
- It can be tree-shaken aggressively and does not drag half the ecosystem with it.
- It has an escape hatch that lets me control the DOM and CSS output.
For UI, that typically means very focused, headless primitives that stay out of my styling. I bring my own CSS and layout. The library handles keyboard traps, focus management, and ARIA details.
If a UI toolkit insists on owning my visual design and ships a giant stylesheet by default, I am out.
Try it on your next feature, not your next project
You do not need to rewrite your whole app to feel this difference. Start smaller.
Pick one new feature and make a rule: no new component library for the UI, only vanilla CSS and the primitives you already have. Then actually measure.
- Run Lighthouse or WebPageTest before and after.
- Use the coverage tab in Chrome devtools to see unused CSS percentages.
- Profile JS execution around interactions that touch your UI widgets.
Commit numbers to a README or a Notion page. Treat it like a mini research project. If the performance win is tiny and the developer pain is huge, you can go back to your old habits with data in hand.
For me the data went the other way. The performance gains were big, the extra work dropped quickly as I built a tiny in-house system, and debugging got more straightforward.
So I stopped using component libraries as the default. Vanilla CSS is my first choice again. Not because I am nostalgic, but because shipping less code is the best optimization I know.
If you care about performance, you should at least make it compete.
Subscribe to my newsletter to get the latest updates and news
Member discussion