Give feedback as an early user and get the docs plus a full website free. Code FRIEND: valid on one-time purchases only.

Optimizing Page Speed

Astro ships your pages as pre-rendered static HTML with zero JavaScript by default, so this starter is fast before you do anything. These are the levers that move your Lighthouse Performance score the most.

0 KB JS by defaultWebP + AVIF images100 Lighthouse
Step 1

Ship as little JavaScript as possible

Biggest win

Every .astro component renders to plain HTML at build time and sends no client-side JavaScript. You only opt into hydration where you genuinely need interactivity, using a client:* directive, and even then, only that one component ships JS (an island), not the whole page.

src/pages/index.astro
---
import Counter from "../components/Counter.jsx";
---
<!-- Static HTML: no JavaScript shipped -->
<Hero />

<!-- Hydrated only once it scrolls into view -->
<Counter client:visible />

The less you hydrate, the higher your Lighthouse Performance score, toggle to compare:

100
Performance
100
Accessibility
100
Best Practices
100
SEO

This Astro starter (zero JS)

Illustrative scores, but the gap is real: less JavaScript on the wire means a faster Largest Contentful Paint and a higher Performance score.

Step 2

Serve optimized, responsive images

Fewer bytes

Images are usually the heaviest thing on a page, so this is where the Performance score is won or lost. Astro optimizes any image you store in src/assets/ and render with the built-in <Image /> or <Picture /> components, files dropped in public/ are served untouched.

The starter sets layout: 'constrained' as the default in astro.config.mjs, so every image is responsive with no per-image work:

astro.config.mjs
export default defineConfig({
  image: {
    layout: "constrained",
  },
});
  • srcset and sizes are generated automatically from each image's dimensions, so the browser downloads a right-sized file instead of a full-resolution one, a big mobile win.
  • width / height are optional for images in src/ (Astro infers them) and reserve the space up front, so the layout never jumps, that protects your CLS.

Reach for <Picture /> when you want modern formats with a fallback, it emits a real <picture> element so the browser picks the smallest format it supports:

src/pages/index.astro
---
import { Picture } from "astro:assets";
import heroImage from "@assets/images/hero.jpg";
---

<Picture
  src={heroImage}
  alt="Description"
  width={400}
  formats={['avif', 'webp']}
  priority
  pictureAttributes={{ class: "cs-picture" }}
/>

Key properties

  • formats: output formats, e.g. ['avif', 'webp']. AVIF and WebP are far smaller than JPG/PNG, so fewer bytes reach the browser.
  • priority: automatically sets the optimal loading, decoding, and fetchpriority for above-the-fold images (use it on your LCP image).
  • width / height: the dimensions to render at.
  • layout: defaults to 'constrained' (responsive); other options are 'fixed' and 'full-width'.

Art-direct with <CSPicture />

Many CodeStitch blocks use a <picture> with multiple srcset sources to swap a different crop, or a different image, between mobile and desktop. The kit's custom <CSPicture /> (in src/components/TemplateComponents) replicates that with getImage(), serving a smaller asset to phones and converting your .jpg files to .webp, so less data on the device that needs it most.

src/pages/index.astro
---
// Import the component and all the images you want to use with it
import CSPicture from "@components/TemplateComponents/CSPicture.astro";
import mobileImage from "@assets/images/construction-m.jpg";
import desktopImage from "@assets/images/cabinets2.jpg";
import fallbackImage from "@assets/images/cabinets2.jpg";
---

<CSPicture
  mobileImgUrl={mobileImage}
  mobileMediaWidth="600px"
  desktopImgUrl={desktopImage}
  desktopMediaWidth="601px"
  fallbackImgUrl={fallbackImage}
  alt=""
/>

It accepts three images, mobile, desktop, and a fallback, which can be different sizes, crops, or completely different assets, plus optional mobileMediaWidth / desktopMediaWidth to tune the breakpoint per usage.

Not every native <picture> from a CodeStitch block is swapped for Astro's component, it's your call. The stock markup already performs well but means resizing and reformatting assets by hand; Astro's <Picture /> has to be written in manually, yet it processes and optimizes assets for you. Read more in the Astro images docs.

Step 3

Preload your hero (LCP) image

The big image above the fold is almost always your Largest Contentful Paint element, the metric that weighs most on the Performance score. Pass a heroImage to BaseLayout and the kit optimizes it for social sharing (1200×600), adds it to your Open Graph tags, and, via the Meta component, preloads it with fetchpriority="high" so it loads before anything else.

src/pages/index.astro
---
import heroImage from "@assets/images/hero.jpg";
import { getImage } from "astro:assets";
const optimizedImage = await getImage({ src: heroImage, format: "webp" });
---

<BaseLayout heroImage={optimizedImage}>
  <Hero />
</BaseLayout>

…which produces this in the rendered <head>:

Rendered HTML
<link
  rel="preload"
  as="image"
  href="/optimized-hero.webp"
  fetchpriority="high"
/>

Preload these

  • Hero / banner images visible immediately on load.
  • Critical brand assets (logos, etc.).
  • Above-the-fold content.

Don't preload

  • Below-the-fold images, lazy-load them instead.
  • Many images, keep it to 1–2 critical resources per page.
  • Small icons or decorative images.
Step 4

Self-host your fonts

Loading fonts from Google's servers adds a render-blocking, third-party round-trip on every visit. This starter avoids it by self-hosting Roboto, the font files live in public/assets/fonts/ and are declared with @font-face in src/styles/root.css:

src/styles/root.css
/* roboto-regular - latin */
@font-face {
  font-style: normal;
  font-family: "Roboto";
  font-weight: 400;
  font-display: swap;
  src: local(""),
    url("/assets/fonts/roboto-v29-latin-regular.woff2") format("woff2"),
    url("/assets/fonts/roboto-v29-latin-regular.woff") format("woff");
}

Base elements then use it, falling back to a system font while it loads:

src/styles/root.css
body,
html {
  font-family: "Roboto", Arial, sans-serif;
}
  • No third-party request, fonts ship from your own domain, so there's no extra DNS lookup and connection to Google blocking the render (and nothing handed off for privacy).
  • .woff2 first with a .woff fallback, woff2 is the smallest modern format and is supported almost everywhere.
  • font-display: swap shows your text immediately in the fallback font, then swaps in Roboto once it arrives, no invisible text (FOIT).
  • Only the weights you actually use ship, 400, 700, and 900: three files, not the whole Roboto family.

To swap in a different font, drop its .woff2 / .woff files into public/assets/fonts/, add a matching @font-face block, and update the font-family: keeping the weight list lean.

Measure, don't guess

After each change, audit the deployed URL with Google PageSpeed Insights or the Lighthouse tab in Chrome DevTools. Test the production build, the dev server is unoptimized and will always look slower.

Light pages, happy users.