To see what’s in your Astro bundle, add rollup-plugin-visualizer to the Vite plugins in astro.config.mjs and run a build. It writes an HTML treemap showing every client-side JavaScript chunk and the modules inside it, sized by bytes. The biggest rectangles are where to start: usually a framework runtime pulled in by one small island, or a large library imported for one function.
Key Takeaways
- Only islands and processed
<script>tags produce client JavaScript. The treemap shows exactly which.rollup-plugin-visualizerplugs into Astro throughvite.plugins.- The usual culprits: an island that could be a
<script>, a second UI framework, a full library for one helper.- Compare gzip sizes, not raw sizes.
Quick check without tools
After npm run build, look at the client assets:
ls -lhS dist/_astro/*.js | head -20
Files are named after their source (Counter.Bx3k2.js, client.Dk92.js), so you can often spot the problem immediately. A client.*.js chunk is a framework runtime.
Treemap with rollup-plugin-visualizer
Install it:
npm install -D rollup-plugin-visualizer
Add it to Vite’s plugins:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
vite: {
plugins: [
visualizer({
filename: 'stats.html',
gzipSize: true,
brotliSize: true,
}),
],
},
});
Build, then open stats.html:
npm run build
open stats.html # macOS; use xdg-open on Linux
Astro runs Vite for both the server and client build. If you see server code (your pages, content) in the treemap, look for the stats file from the client build, or set a different filename per run. Add stats.html to .gitignore.
Turn on the gzip or brotli view in the report. Raw sizes overstate what users download.
What to look for
A framework runtime for one tiny island. A single React island ships React and React DOM. If that island is a toggle or a tab set, rewrite it as an Astro component with a <script>:
<button class="toggle" aria-expanded="false">Menu</button>
<nav class="menu" hidden>...</nav>
<script>
const btn = document.querySelector<HTMLButtonElement>('.toggle')!;
const menu = document.querySelector<HTMLElement>('.menu')!;
btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
menu.hidden = open;
});
</script>
Two frameworks. A React island and a Svelte island means two runtimes. Standardise on one for client components, or use a lighter one (Preact, Svelte, Solid) for all of them.
A whole library for one function. Common examples: a date library for one format call (use Intl.DateTimeFormat), a utility library for one helper (import the single function or write it), an icon package importing the full set.
Duplicates. Two versions of the same package, often from mismatched dependency ranges. npm ls <package> shows why each is installed.
Code that shouldn’t be on the client at all. Data fetching, Markdown parsing or validation inside an island. Move it to the Astro component’s frontmatter and pass the result as props.
Reduce what loads up front
Not all code needs to be cut; some just needs to load later:
- Change
client:loadtoclient:visibleorclient:idle. See lazy-loading islands. - Use dynamic
import()for rarely used features so they become separate chunks. - Remove the directive from components that don’t need interactivity.
Keep it from regressing
Re-run the treemap after adding a dependency or island. For a simple CI guard, fail the build if total client JS crosses a budget:
total=$(cat dist/_astro/*.js | gzip -c | wc -c)
echo "Client JS (gzip): $total bytes"
[ "$total" -lt 100000 ] || { echo "JS budget exceeded"; exit 1; }
Pick a budget from your current size, not an arbitrary number, and lower it as you improve.
When to use a framework island versus a plain script is covered in the Astro components guide. The wider set of performance checks is in the Astro performance guide. The plugin’s options are documented in the rollup-plugin-visualizer README.
FAQ
Why does my Astro page load React when I only used one component?
Any framework component with a client:* directive ships its framework runtime. Remove the directive if the component isn’t interactive, or rewrite small interactions as an Astro component with a <script>.
Does CSS count toward the bundle?
CSS is bundled separately and doesn’t appear in a JavaScript treemap. Check dist/_astro/*.css sizes for that.