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.
Ship as little JavaScript as possible
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.
---
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:
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.
Serve optimized, responsive images
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:
export default defineConfig({
image: {
layout: "constrained",
},
});srcsetandsizesare 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/heightare optional for images insrc/(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:
---
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 optimalloading,decoding, andfetchpriorityfor 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.
---
// 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.
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.
---
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>:
<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.
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:
/* 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:
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).
.woff2first with a.wofffallback, woff2 is the smallest modern format and is supported almost everywhere.font-display: swapshows 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, and900: 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.
