Engineering 3 min read

How Astro keeps a marketing site fast

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.

Theo Park
Theo Park Staff engineer

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.

---
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.

---
import PriceTable from "@/components/PriceTable.astro";
import Newsletter from "@/components/Newsletter.tsx";
---

{/* Rendered to HTML at build time. Ships nothing. */}
<PriceTable plans={plans} />

{/* Hydrates only once it scrolls into view. */}
<Newsletter client:visible />

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.

import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";

export default defineConfig({
  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.

---
import { Image } from "astro:assets";
import hero from "@/assets/hero.jpg";
---

<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.

$ 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.

  • astro
  • performance
  • islands
Share
Theo Park

Theo Park

Staff engineer

Theo spent six years on developer tooling, and now works on rendering performance and the build pipeline.