Type something to search...
How does Astro ship zero JavaScript by default?

How does Astro ship zero JavaScript by default?

Astro ships zero JavaScript by default because every component, whether it's an .astro file or a React, Vue or Svelte component, is rendered to HTML on the server and its code is discarded afterwards. Astro components have no client-side runtime at all, so their component scripts never leave the server. Framework components are run on the server too, but Astro doesn't send the framework or the component to the browser unless you add a client:* directive. The only JavaScript that reaches the browser is what you explicitly ask for: hydrated islands, <script> tags you write, and features that need it, such as the <ClientRouter /> or server islands.

"Zero by default" doesn't mean "zero forever". It means JavaScript is opt-in, per component, and you can see exactly where each byte comes from. In this article you'll learn how the rendering pipeline removes JavaScript, what a page with and without islands looks like in the build output, every way JavaScript gets added back, how to audit a build, and techniques for keeping pages light. All numbers come from builds with Astro 7.4.2.

Why Most Frameworks Ship JavaScript

In a traditional React or Vue framework, components describe UI that can change. To make that work in the browser, the framework re-runs the components on the client after the server-rendered HTML arrives (hydration). That requires the framework runtime, the component code and the data it used, even for parts of the page that will never change, such as article text or a footer.

Astro starts from the opposite assumption: most of a content page never changes after it's rendered. So it renders once, on the server, and sends the result.

How Astro Removes JavaScript

Astro Components Render Only on the Server

An .astro file compiles to a server-side render function. Its component script (the code between the --- fences) and template expressions run in Node.js during the build, or on each request for on-demand pages, and produce a string of HTML. There's nothing left to run in the browser. The component script article covers exactly when that code runs.

Framework Components Are Rendered to Static HTML

Framework components are treated the same way unless you say otherwise. Here's a page that uses a React counter with a useState hook and an onClick handler:

---
// src/pages/static.astro
import Base from '../layouts/Base.astro';
import Counter from '../components/Counter.jsx';

const posts = [
  { title: 'Hello Astro', slug: 'hello-astro' },
  { title: 'Islands explained', slug: 'islands' },
];
---
<Base title="Static page">
  <h1>Latest posts</h1>
  <ul>
    {posts.map((post) => <li><a href={`/blog/${post.slug}`}>{post.title}</a></li>)}
  </ul>
  <Counter start={5} />
</Base>

The build output for this page is 354 bytes of HTML:

<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Static page</title></head><body><h1>Latest posts</h1><ul><li><a href="/blog/hello-astro">Hello Astro</a></li><li><a href="/blog/islands">Islands explained</a></li></ul><button type="button">Count<!-- -->: <!-- -->5</button></body></html>

React ran on the server to produce the <button>, then Astro dropped it. The button won't count clicks, which is the point: you decide whether it should.

Opting In With a Client Directive

Change <Counter start={5} /> to <Counter start={5} client:load /> and Astro bundles the component, the React renderer and a small loader, and wraps the button in an <astro-island> element that hydrates it. In the same build, the files were:

FileSizeGzipped
client.*.js (React DOM renderer)212.8 KB65.7 KB
react.*.js (React)7.9 KB3.0 KB
Counter.*.js (the component)0.5 KB0.4 KB

Notice that the component itself is tiny; the cost is the framework. That's why the choice of directive and framework matters, and why Astro's island architecture loads each island independently, at a time you choose with client:idle, client:visible or client:media.

What Adds JavaScript Back

JavaScript appears in an Astro build only from a known list of sources. Knowing them makes auditing easy.

1. Hydrated Framework Components

Any component with client:load, client:idle, client:visible, client:media or client:only. Each brings its own code plus the shared framework renderer, which is sent once per page per framework.

2. Script Tags You Write

A <script> in a component is processed by default: Astro converts TypeScript, bundles imports, deduplicates it if the component is used more than once and inlines it if it's small. This theme toggle:

---
// src/components/ThemeToggle.astro
---
<button type="button" data-theme-toggle>Toggle theme</button>

<script>
  const buttons = document.querySelectorAll<HTMLButtonElement>('[data-theme-toggle]');
  buttons.forEach((button) => {
    button.addEventListener('click', () => {
      document.documentElement.classList.toggle('dark');
    });
  });
</script>

used twice on one page became a single 147-byte inline module:

<script type="module">document.querySelectorAll(`[data-theme-toggle]`).forEach(e=>{e.addEventListener(`click`,()=>{document.documentElement.classList.toggle(`dark`)})});</script>

Plain scripts like this are often the lightest way to add behaviour: no framework, no hydration, just the code you wrote. There's a dedicated guide on adding interactivity without a framework.

3. Inline and Unprocessed Scripts

<script is:inline>, or a script with any attribute other than src, is left exactly as written and repeated for every instance of the component. define:vars on a script also makes it inline. These are useful for third-party snippets and tiny bootstrapping code, such as setting a theme class before first paint, but they skip bundling and deduplication.

4. Built-In Features That Need the Browser

Some Astro features are client-side by nature:

  • ClientRouter: adding <ClientRouter /> from astro:transitions enables animated, client-side page navigation. In my build it added one module of 16.4 KB (5.7 KB gzipped).
  • Server islands: each server:defer component adds a small inline script that fetches the island's HTML and swaps it in.
  • Prefetching: when you enable the prefetch option, Astro adds a script that prefetches links.
  • The dev toolbar: present only in astro dev, never in production builds.

5. Integrations and Third-Party Code

Analytics, chat widgets, consent banners, embeds and some integrations add their own scripts. These are frequently the biggest contributors on real sites, so check them first when a page gets heavy.

Auditing a Build

Because JavaScript only comes from the sources above, a quick audit of dist/ tells you what you ship. After npx astro build, from the project root:

# How many JavaScript files are there in the build?
find dist -name '*.js' | wc -l

# Which pages contain script tags, and how many?
grep -c '<script' -r dist --include='*.html' | grep -v ':0$'

For a blog with an article layout, an RSS feed, a sitemap and one page that uses <ClientRouter />, the output was:

       1
dist/router/index.html:1

One JavaScript file in the whole site, on the one page that asked for it. Every article, the blog index and the home page had no scripts at all. With an SSR adapter, check dist/client/ instead, because dist/server/ holds server code that never reaches the browser.

In the browser, the Network panel filtered by JS shows what a page actually downloads, and the Astro dev toolbar can highlight islands on the page during development.

Techniques for Staying Close to Zero

Use HTML and CSS First

A surprising amount of interactivity needs no JavaScript at all. This page ships no scripts, yet has a working disclosure widget and a popover menu:

---
// src/pages/faq.astro
import Base from '../layouts/Base.astro';
---
<Base title="FAQ">
  <details>
    <summary>Does this need JavaScript?</summary>
    <p>No. The browser handles opening and closing.</p>
  </details>
  <button popovertarget="menu">Menu</button>
  <div id="menu" popover>
    <a href="/blog/">Blog</a>
  </div>
</Base>

Other native options include <dialog>, CSS :has() and :target, scroll snap for carousels, loading="lazy" for images and plain HTML forms that post to an endpoint or an Astro Action.

Choose the Lightest Tool for Each Island

  • A few event listeners: a processed <script> or a custom element.
  • A small interactive widget: Preact, Svelte or Solid typically have much smaller runtimes than React.
  • A complex, stateful component: your framework of choice, loaded as late as possible.

Load Islands as Late as Possible

Use client:visible for anything below the fold and client:idle for low-priority UI. Reserve client:load for controls that must work instantly. With client:visible, a component the visitor never scrolls to never downloads.

Keep Islands Small

Make the interactive part the island, not the whole section around it. A product page doesn't need to hydrate the description to make the "Add to basket" button work. Pass static content into an island through slots and keep props small, since they're serialised into the HTML. For more ideas, see how to reduce JavaScript in Astro.

What Zero JavaScript Doesn't Guarantee

Shipping no JavaScript removes the most expensive kind of resource, but it doesn't make a page fast on its own. A few things still deserve attention:

  • Images: an unoptimised hero image can outweigh a framework. Use <Image /> or <Picture /> with explicit widths so Astro resizes and converts them.
  • Fonts: several font files, loaded late, cause layout shift and slow text rendering. Load fewer weights and preload only the critical file.
  • CSS: Astro scopes and minifies styles, but a large global stylesheet or an entire CSS library imported for a few components still costs bytes.
  • Server response time: on-demand pages are only as fast as the data fetching in their component scripts. Prerender what you can, and cache the rest.

Zero JavaScript is the starting point that makes the rest easy, not a substitute for it.


FAQ: Zero JavaScript in Astro

No. It means interactivity is opt-in. You add it with client directives on framework components, script tags in Astro components, or features such as the ClientRouter, and only those pages and components ship JavaScript.

Without a client directive, Astro renders the component to HTML and doesn't send React to the browser, so event handlers never attach. Add client:load, client:idle or client:visible to hydrate it.

Yes. They're sent to the browser, but Astro bundles, minifies and deduplicates them, and inlines small ones into the HTML. They're often far lighter than hydrating a framework component for the same job.

No. Both compile to CSS at build time. Only libraries that run in the browser, such as UI component kits with their own scripts, add JavaScript.

It adds a script to every page that includes the ClientRouter component, usually through a shared layout. In a test build it was about 5.7 KB gzipped. Leave it out if you don't need client-side navigation.

Build the site and look in dist for .js files and script tags, or open the Network panel in your browser and filter by JS. With an adapter, only dist/client is sent to browsers.


Conclusion

Astro ships zero JavaScript by default because rendering happens on the server and nothing is hydrated unless you ask. Astro components never have client code, framework components become plain HTML without a client:* directive, and the build output contains only the scripts you opted into. In a real build, a whole blog shipped one JavaScript file, on the single page that used the ClientRouter.

When you do need behaviour, reach for HTML and CSS first, then a small script, then a lightweight island, and load it as late as you can. Audit dist/ from time to time, keep an eye on third-party scripts, and your pages will stay fast as the site grows.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper