Astro ships no JavaScript to the browser until something asks for it, and that default is most of why the framework gets chosen. Then a video embed arrives carrying a player bundle, and the page that was 0 kB of script is suddenly not. The cost is real. Most of it is avoidable. Load the player when someone wants to watch, rather than when the page loads. The part that remains buys something a plain video tag can't give you.
TL;DR
Adaptive playback needs JavaScript in every browser, and Firefox can't play HLS at all without it, so some script is unavoidable for anyone who actually watches. The trick is that most visitors don't. Render a thumbnail straight from images.fastpix.com/{PLAYBACK_ID}/thumbnail.jpg with a play button, and put the player behind a dynamic import() in a <script> tag so a visitor who never clicks downloads none of it. Skip client directives entirely where the only interactivity is starting the video, since an island drags its framework runtime along. Use enable-lazy-loading instead when you expect most visitors to watch.
Astro's zero-JavaScript default and a video embed
Astro renders components to HTML at build time and sends no client-side JavaScript unless a client: directive asks for it. That's the model.
A video player breaks the model on purpose, because adaptive playback requires JavaScript. Firefox has no native HLS at all, and even where the browser can play a manifest on its own, rendition switching and analytics need a library feeding Media Source Extensions. Nothing declarative can do that.
So the question isn't whether to ship script. It's when, and for whom.
| Approach | JavaScript on page load | Adaptive playback |
|---|---|---|
| <video> with an MP4 | None | No, one fixed bitrate |
| Player loaded on page load | Full player bundle | Yes |
| Poster image, player on interaction | None until a click | Yes, once clicked |
What you need before you start
- An Astro project, version 7.
- A playback ID for a public video, which is safe to put in client-side markup.
- No adapter, if the playback ID is known at build time. Static pages can embed video.
An Astro video embed that costs one image request
FastPix serves a still frame for any playback ID straight from the image host, with no upload and no API call:
https://images.fastpix.com/{PLAYBACK_ID}/thumbnail.jpg?time=4&width=1280time picks the frame in seconds, width and height size it, and fit_mode takes preserve, stretch, crop, intellicrop or pad. The image is also available as .png or .webp.
That means the visible part of a video embed costs one image request and zero JavaScript. For a page with several videos below the fold, that's the whole page as far as most visitors are concerned, because most of them won't press play.
Load the Astro video player on click
The pattern is a poster with a play affordance, and the player import deferred to the click:
---
const { playbackId, title } = Astro.props;
const poster = `https://images.fastpix.com/${playbackId}/thumbnail.jpg?time=4&width=1280`;
---
<div class="embed" data-playback-id={playbackId}>
<img src={poster} alt={title} width="1280" height="720" loading="lazy" />
<button type="button" class="play" aria-label={`Play ${title}`}>Play</button>
</div>
<script>
document.querySelectorAll(".embed").forEach((embed) => {
embed.querySelector(".play")?.addEventListener("click", async () => {
await import("@fastpix/fp-player");
const player = document.createElement("fastpix-player");
player.setAttribute("playback-id", embed.dataset.playbackId);
player.setAttribute("stream-type", "on-demand");
player.setAttribute("auto-play", "");
embed.replaceChildren(player);
});
});
</script>The <script> tag is what Astro bundles and runs in the browser, and it's tiny because the player itself is behind a dynamic import(). A visitor who never clicks downloads none of it.
loading="lazy" on the poster is worth adding for the same reason. Below-fold posters wait for the scroll rather than competing with the hero for connections. The thumbnail parameters are documented in create thumbnails from a video.
You can put a real adaptive stream behind this first. The free plan covers 10 videos and 100K streaming minutes a month with no credit card.
Which Astro client directive a video embed needs
If the embed is a React or Svelte component rather than an Astro one, the client directive decides when its JavaScript loads, and the default choice is usually wrong for video.
client:load hydrates immediately, which is what you don't want. client:visible hydrates when the component scrolls into view, which is better and still loads the player for people who scroll past. client:idle waits for the main thread to be quiet, which is fine for something below the fold.
For a video embed, none of those is as cheap as the plain Astro version above, because an island brings its framework runtime along with it. React on an Astro page to render a play button is a lot of machinery for one click handler.
The rule of thumb: if the component's only interactivity is "start the video", write it as an Astro component with a <script> tag and skip the island entirely.
When enable-lazy-loading beats click-to-load
enable-lazy-loading defers the player's initialisation until it's near the viewport, which is the right tool when the player element is already in the markup and you accept the script cost:
<fastpix-player
playback-id={playbackId}
stream-type="on-demand"
enable-lazy-loading
></fastpix-player>
<script>
import "@fastpix/fp-player";
</script>If the playback ID has to be fetched per request rather than baked in, that route needs an adapter and prerender = false. This is simpler than the click-to-load pattern and more expensive, because a static import downloads the player bundle for every visitor whether they scroll to it or not. The attribute only saves the initialisation work. Use it when you expect most visitors to watch, and use click-to-load when you expect most of them not to.
Note that the player import sits in a <script> tag rather than in the frontmatter. Astro only runs <script> in the browser, and the element registers itself against customElements, which would put the player bundle in every server-rendered page.
What the player bundle buys over a plain MP4
It's worth saying what the alternative costs, because "just use an MP4" is the obvious response to all of this.
A single MP4 in a <video> tag ships no JavaScript and serves one bitrate to everybody. A phone on a weak connection downloads the same file as a desktop on fibre, so either the phone stalls or the desktop gets a soft-looking video, and you pick which by choosing the bitrate.
An adaptive stream removes that choice by letting the player negotiate. It also gives you captions, multiple audio tracks, quality selection and playback analytics, none of which a bare video tag offers. Whether that's worth the script is a real decision, and it's a different decision for a marketing hero than for a course platform.
For the plain HTML video techniques on the other side of that decision, guide to lazy loading HTML videos for your website covers them.
Ship a video embed that costs nothing until it plays
Render the thumbnail from images.fastpix.com/{PLAYBACK_ID}/thumbnail.jpg, put a play button beside it, and load the player behind a dynamic import in a <script> tag. The page keeps its zero-JavaScript profile for every visitor who scrolls past. The free plan covers 10 videos and 100K streaming minutes a month, no credit card, which is enough to build the embed and measure it against a plain video tag.
Frequently Asked Questions (FAQs)
How do I embed a video in Astro without shipping JavaScript?
Render a thumbnail image with a play button in an Astro component, and load the player behind a dynamic import() inside a <script> tag on click. A visitor who never presses play downloads no player code. The thumbnail comes from the FastPix image host for any playback ID with no API call.
Does a video embed break Astro's zero-JavaScript default?
Only if you load the player on page load. Firefox has no native HLS at all, and adaptive switching and analytics need a library everywhere, so some script is unavoidable for anyone who actually watches. Deferring it to a click keeps the default intact for everyone who does not.
Which client directive should I use for a video component in Astro?
Ideally none. If the only interactivity is starting the video, write it as an Astro component with a <script> tag, since an island brings its framework runtime along with it. If it must be an island, client:visible beats client:load, and both still load the player for people who scroll past.
What is the difference between enable-lazy-loading and click-to-load?
enable-lazy-loading defers the player's initialization until the element is near the viewport. It does not defer the download: a static import pulls the player script, and hls.js with it, for every visitor. Click-to-load shows only an image and imports nothing until someone presses play. Use the attribute when most visitors watch, and click-to-load when most do not.
Can I embed video on a static Astro page without an adapter?
Yes, if the playback ID is known at build time and the video is public. The player takes the ID and talks to the CDN directly, so no server is involved. You need an adapter for minting signed upload URLs, receiving webhooks, and signing tokens for private or DRM-protected playback.






