What a global UI script costs your SvelteKit app
Aug 12, 2026 SvelteKit
UI libraries come in two shapes, and the shape turns out to matter more than the feature list.
A global script is a prebuilt .js file you drop into a <script> tag. It scans the page for magic class names and wires up the behaviour it finds. Preline works this way, as did Bootstrap before it.
A component library is a set of components you import, so your bundler compiles only the ones you actually use. Bits UI, Melt UI and Radix all work this way.
This site ran on Preline and now runs on Bits UI. We built and measured both versions, so the numbers below are real rather than a rule of thumb.
Table of Contents
The headline
Same site, same dependencies, only the UI library changed. Every asset the home page loads, gzipped:
| Preline | Bits UI | Difference | |
|---|---|---|---|
| JavaScript | 149,927 | 79,998 | −46.6% |
| CSS | 32,071 | 21,466 | −33.1% |
| HTML | 9,063 | 9,391 | +3.6% |
| Total | 191,061 | 110,855 | −42.0% |
There is one accordion on that page.
Why a global script is expensive
The Preline runtime is a single file, and every page loads all of it:
<!-- src/app.html -->
<script src="/dist/js/preline.js"></script> That file is 362 KB, or 78.7 KB gzipped. Inside it are dropdowns, tooltips, carousels, file upload widgets and combo boxes. A page with one accordion still downloads and parses the carousel code, because a global script has no way of knowing what any particular page needs.
Worse, your tooling will not warn you about it. The file sits in static/, gets copied to the build output and is served as-is, so it never passes through the bundler at all. Chunk sizes look healthy and bundle analyzers stay quiet while every visitor pays 78.7 KB.
The flat fee hurts most where the browser is already doing a lot of work. A single page application ships its whole app as JavaScript, so a global UI script lands on top of a bundle that is large to begin with.
Bits UI has no runtime file. You import the components you want, and only those get compiled in:
<script>
import { Accordion } from 'bits-ui';
</script>
<Accordion.Root type="single">
<Accordion.Item value="one">
<Accordion.Header>
<Accordion.Trigger>Title One</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>Content One</Accordion.Content>
</Accordion.Item>
</Accordion.Root> The accordion component in our docs is built this way.
None of which makes Bits UI free. It added 8.8 KB gzipped to the home page bundle and 20.7 KB across all routes. The difference is that it charges for what you use instead of charging every page a flat 78.7 KB.
The part we did not expect
The CSS dropped by 10.6 KB gzipped, and for a while we could not explain it. Preline’s contribution to the stylesheet is a file called variants.css, which is 1,612 bytes long and consists mostly of variant definitions:
@custom-variant hs-accordion-active {
/* ... */
} Definitions like that generate no CSS by themselves. So we tested the thing directly: starting from the Bits UI build, changing no code at all, we copied preline.js back into static/dist/js/ and rebuilt.
| CSS (raw) | CSS (gzipped) | |
|---|---|---|
Without preline.js in static/ | 138,162 | 21,466 |
With preline.js in static/ | 276,313 | 32,117 |
One file that nothing imported, and the stylesheet doubled.
The explanation is that Tailwind v4 finds your class names by scanning the project, rather than reading a list of content paths from a config file. And preline.js is 362 KB of minified JavaScript packed with strings like "max-h-40" and "opacity-0". Tailwind read those as class names and dutifully generated CSS for all of them, on a site that uses none of them.
Counting both halves, that one <script> tag cost:
| Raw | Gzipped | |
|---|---|---|
preline.js | 362 KB | 78.7 KB |
| Dead CSS it caused Tailwind to generate | ~138 KB | ~10.7 KB |
| Total, on every page | ~500 KB | ~89 KB |
This can happen to you without Preline
Any large JavaScript file sitting in your project can do the same thing, whether it is a vendored analytics script or some bundle nobody got round to deleting. If Tailwind scans it and it contains class-like strings, you are shipping CSS for it.
Build your app and look at what came out:
pnpm build
ls -l .svelte-kit/output/client/_app/immutable/assets/*.css If that number looks too big for the amount of styling on your site, exclude the file in src/app.css:
@import 'tailwindcss';
/* Stop Tailwind reading class names out of a vendored bundle */
@source not "../static/dist/js/preline.js"; Rebuild and compare. Here that single line was worth 138 KB of raw CSS.
What it costs
Builds got slower, which makes sense once the bundler has a real component library to compile instead of a prebuilt file it can ignore. Median of three cold production builds:
| Preline | Bits UI | |
|---|---|---|
pnpm build | 9.2s | 11.6s |
That is 26% slower, so this is a trade and not a free win. You spend build time to save the bytes your visitors download. For a prerendered site built in CI that is usually worth taking, though if you rebuild constantly on an old laptop you might weigh it differently.
Measure your own app
Our numbers describe our site, and yours will differ. Two commands will tell you by how much.
For the total JavaScript your build produces, gzipped:
pnpm build
find .svelte-kit/output/client -name '*.js'
-exec sh -c 'gzip -9c "$1" | wc -c' _ {} ;
| awk '{s+=$1} END {print s " bytes gzipped"}' That command deliberately includes everything in static/, since a global script is exactly the sort of thing that hides there.
More useful than a project total is what a single page actually pulls down. Open DevTools, switch to the Network tab, tick Disable cache and reload. The transfer size sits at the bottom, and you can filter by JS and CSS separately.
What these numbers do not tell you
We measured payload, and payload is where the difference is unambiguous. Load time is another matter. Testing over localhost means no network latency and no bandwidth ceiling, so half a megabyte arrives more or less instantly. Our timings even showed the smaller build finishing a few milliseconds slower than the larger one, which is a good sign that we were measuring noise.
Payload matters most on slow connections and cheap phones, which is precisely what a laptop serving itself files cannot show you. For real numbers, throttle the network in DevTools or collect field data from production.
We also did not measure how either library feels to click, so nothing here speaks to runtime performance.
What to do about it
Prefer a library that ships what you use. A global script charges a flat fee to every page, and because the file never passes through your bundler, nothing in your build output will point at the cost.
While you are in there, check what Tailwind is scanning. A file that nothing imports can still cost you a hundred kilobytes of CSS.
The full benchmark, with bundle strategy comparisons and the raw per-page measurements, is in preline-vs-bitsui-bench.md at the root of this repository.