---
type: "Blog post"
title: "How Astro keeps a marketing site fast"
description: "Most marketing sites ship an entire framework to render text that never changes. Here is what Astro does instead, and the handful of settings that still matter once you are past the defaults."
resource: "https://new.cuongthach.com/blog/astro-performance/"
tags:
  - "blog"
  - "en"
  - "Engineering"
  - "astro"
  - "performance"
  - "islands"
status: stable
generated:
  by: "process:cuongthach-okf-export"
  at: "2026-06-12T00:00:00Z"
---
# How Astro keeps a marketing site fast

A marketing page is mostly text, a few images, and two or three things that
actually need to react to a click. The usual stack sends a runtime, a router,
and a hydration pass for all of it, then spends the next six months trying to
claw the milliseconds back.

Astro starts from the opposite default. Every component renders to HTML at build
time and ships **zero JavaScript** unless you ask for some. You are not
optimizing your way down to a fast page. You are deciding, one component at a
time, what deserves to be interactive.

## Components render on the server and stop there

An `.astro` component looks like a page and behaves like a function. The
frontmatter runs once, at build time, and the template becomes plain markup.
There is no client bundle for this file, because there is nothing left to run.

```astro
---
const posts = await fetch("https://api.example.com/posts/").then((res) =>
  res.json(),
);
---

<ul class="post-list">
  {posts.map((post) => (
    <li>
      <a href={post.href}>{post.title}</a>
    </li>
  ))}
</ul>
```

That `await` at the top is doing real work during the build, not in a visitor's
browser. By the time the page is requested, the list is already HTML sitting in
a file.

## Islands are opt in, one directive at a time

When a component genuinely needs state, you mark it with a `client:*` directive.
Only that component and its dependencies reach the browser. Everything around it
stays static.

```astro
---
---

{/* Rendered to HTML at build time. Ships nothing. */}

{/* Hydrates only once it scrolls into view. */}
```

The directive is also the scheduling decision:

- **`client:load`** hydrates immediately. Reserve it for something above the
  fold that breaks without JavaScript.
- **`client:visible`** waits for the component to scroll into view. This is the
  right answer far more often than people expect.
- **`client:idle`** waits for the main thread to go quiet, which keeps it out of
  the way of first paint.
- **`client:media`** hydrates only when a media query matches, so a desktop-only
  widget costs a phone nothing.

> The fastest component is the one that never hydrates. The second fastest is
> the one that hydrates after the visitor has already seen the page.

## Four settings worth turning on

The defaults are good, but a marketing site benefits from a few explicit
choices. Prefetching is the one people notice: links are fetched before the
click lands, so navigation feels instant without a client-side router.

```js

  site: "https://example.com/",
  compressHTML: true,
  trailingSlash: "always",
  prefetch: {
    prefetchAll: true,
    defaultStrategy: "viewport",
  },
  integrations: [sitemap()],
});
```

### Images are part of the build, not an afterthought

The image pipeline converts, resizes, and generates a srcset during the build.
The only thing it needs from you is the display size, because that is the one
thing it cannot infer from the file.

```astro
---
---

<Image
  src={hero}
  alt="Product dashboard"
  widths={[640, 1024, 1536]}
  sizes="(min-width: 64rem) 1024px, 100vw"
  loading="eager"
  fetchpriority="high"
/>
```

Skip `widths` and `sizes` and the browser cheerfully downloads a 2400 pixel
original to paint it at 380. It is the single most common reason an otherwise
clean Astro site scores badly.

## What the build actually produces

The useful part is that you can check. If a page has no islands, there is no
JavaScript directory to inspect, and the absence is the whole point.

```bash
$ npm run build

09:41:02 [build] 14 page(s) built in 1.83s
09:41:02 [build] Complete!

$ ls dist/_astro/*.js
ls: cannot access 'dist/_astro/*.js': No such file or directory
```

## Where the effort goes instead

None of this removes the work of building a good site. It moves it. You spend
the time on layout, copy, and the two or three interactions that earn their
bytes, rather than on auditing a bundle you did not choose to ship. That is a
much better trade, and it holds up as the site grows.
