Astro's client:only Directive: Keeping Heavy Components Off the Critical Path
Astro's client:only directive keeps heavy interactive components out of the initial JavaScript bundle by skipping server-side rendering entirely. Learn when to use it, how to provide fallbacks, and the SEO trade-offs you need to weigh.
09 Jan 2026, 11:58 UTC

The Problem: Interactive Components Bloat Initial JavaScript
Astro ships zero JavaScript by default, but real projects inevitably need interactive islands—maps, charts, complex forms, or third-party widgets. When you drop a heavy React or Vue component into an .astro page, Astro hydrates it by default, pulling its entire dependency tree into the initial bundle. On content-heavy sites (docs, blogs, marketing pages) that extra 100–300 KB can push Time-to-Interactive past the 3-second threshold, hurting both Core Web Vitals and conversion.
How client:only Changes the Equation
The client:only directive tells Astro: "Don't render this component on the server at all. Emit a lightweight placeholder and hydrate it entirely in the browser." Unlike client:load or client:visible, which still server-render HTML then hydrate, client:only skips SSR completely for that component. The server sends a <div data-astro-cid-...></div> (or your custom fallback) and the component's JavaScript loads only when the browser executes the hydration script.
This means the component's framework runtime (React, Vue, Svelte) and its dependencies never enter the critical-path bundle. They arrive later, often after the user has already started reading the page.
Worked Example: Embedding a Leaflet Map
Suppose you have a MapView.astro component that wraps Leaflet and adds custom markers. The Leaflet CSS + JS plus your wrapper weighs ~120 KB gzipped. You only need the map on a "Contact" page, and it sits below the fold.
--- // src/pages/contact.astro
import MapView from '@/components/MapView';
---
<main>
<h1>Visit Us</h1>
<p>Our office is located in downtown Portland.</p>
<section id="map-container">
<MapView client:only="react" />
</section>
</main>
Key points in this snippet:
client:only="react"specifies the renderer (Astro needs to know which framework's hydration script to inject).- No server-rendered map markup appears in the initial HTML—view source shows only an empty placeholder
div. - The map's JavaScript loads as a separate chunk (
MapView.[hash].js) after the page's primary content is interactive.
To provide a fallback for users without JavaScript (or crawlers), add a fallback prop or slot:
<MapView client:only="react" fallback="<img src='/static/map-fallback.png' alt='Office location' />" />
Trade-offs and Limitations
- SEO and crawlers: Content inside a
client:onlycomponent is invisible to search-engine bots that don't execute JavaScript. If the map contains address text you want indexed, duplicate that text in server-rendered HTML above the component. - No server-side data: The component cannot access
Astro.propsthat depend on server-only secrets or database calls at render time. Pass required data as JSON-serializable props, or fetch it client-side inside the component. - Layout shift: The placeholder has zero height by default. Reserve space with CSS (
aspect-ratioormin-height) to avoid Cumulative Layout Shift when the map hydrates. - Over-fragmentation: Applying
client:onlyto dozens of tiny components creates many small JS chunks, increasing request overhead. Reserve it for genuinely heavy islands.
Verifying the Impact
- Run
astro build && astro preview. - Open DevTools → Network tab, filter "JS". Reload the page.
- Confirm the map chunk (
MapView.[hash].js) loads after the main page JS and only when the placeholder enters the viewport (or immediately, depending on your hydration strategy). - Compare the initial JS total before and after adding
client:only. A 30–50% reduction on content pages is typical.
When to Reach for client:only
Use client:only when:
- The component is heavy (framework runtime + large deps).
- The component sits below the fold or behind a user interaction.
- The component's content doesn't need to be indexed.
- You can supply all required data via props or client-side fetch.
For everything else—navigation, hero sections, comment forms—stick with client:load or client:visible so the first paint includes meaningful HTML.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.