iSamuelDev

Astro Performance Tips That Actually Move the Needle

iSamuel · · 12 min read
Share: Tweet WhatsApp
Astro Performance Tips That Actually Move the Needle

Astro Performance Tips That Actually Move the Needle

Astro starts you off with a serious advantage. Zero JavaScript by default, static HTML output, selective hydration — the foundation is already solid. But “already fast” and “as fast as possible” are two different things.

This post is about closing that gap — with real numbers, not just advice.


Lab Scores vs. Field Data: Know What You’re Actually Measuring

Before touching a single config file, this distinction matters more than any individual optimization.

Lighthouse is a lab tool. It runs in a controlled environment, on a fixed connection speed, against a single page load. It’s useful for spotting obvious problems. But it’s not what Google uses to rank your site.

Google’s Core Web Vitals rankings are based on field data — real measurements from real users collected through the Chrome User Experience Report (CrUX). A developer on a fast MacBook with a wired connection is not your average visitor. Field data captures the person on a mid-range Android phone on a 4G connection in the middle of a commute.

The gap between lab and field can be surprisingly wide:

MetricLab Score (Lighthouse)Field Score (CrUX)Why They Differ
LCP1.2s3.1sReal devices are slower; CDN variance
CLS0.020.11Fonts and late-loaded ads cause real-world shift
INP60ms210msLab uses idle CPU; field captures busy main threads

You can check your site’s actual field data for free through Google Search Console under Core Web Vitals, or via PageSpeed Insights which shows both lab and field results side by side. Optimize for field, use lab to diagnose.


1. Only Hydrate What Actually Needs It

Island architecture is Astro’s biggest performance lever, and it’s easy to misuse.

Every time you reach for client:load, ask yourself: does this component genuinely need JavaScript to work on page load? Most of the time the answer is no — or at least not immediately.

On a documentation site migrated from a React SPA to Astro, switching all non-critical interactive components from client:load to client:visible or client:idle reduced Total Blocking Time from 480ms to 55ms and brought the mobile Lighthouse performance score from 61 to 89. The content didn’t change. Only the hydration strategy did.

DirectiveWhen It HydratesBest For
client:loadImmediately on page loadAbove-the-fold interactive elements
client:idleWhen the browser is idleWidgets not needed right away
client:visibleWhen the component scrolls into viewComments, share buttons, embeds
client:mediaWhen a CSS media query matchesSidebar menus, responsive nav
client:onlyClient-side render only, no SSRComponents that break without a browser

A search bar in the header warrants client:load. A comment section at the bottom of a post is exactly what client:visible is for. Deferring hydration on things users haven’t seen yet is essentially free performance — and the TBT improvement it creates directly feeds your INP field score.


2. Stop Importing Entire Libraries

This one shows up constantly. You install a utility library, import it at the top, and use one function from it. The entire library ships in your bundle.

A real example: one Astro project was importing all of date-fns (roughly 35KB gzipped) to format a single date string. Switching to a named import from the specific module path brought that down to under 2KB — a 94% reduction for that chunk alone.

Instead of:

import _ from 'lodash';
const slugified = _.kebabCase(title);

Do this:

import kebabCase from 'lodash/kebabCase';

Same outcome, fraction of the weight. Tree-shaking helps, but explicit named imports from module paths are more reliable — some bundler configurations don’t tree-shake as aggressively as you’d expect, and you won’t notice until you look at the bundle analyzer output.


3. Use Astro’s Image Component — Every Time

<img> tags work, but they give you nothing for free. The built-in <Image /> component from astro:assets handles the heavy lifting automatically:

  • Converts to WebP (or AVIF if you configure it)
  • Generates responsive srcset values
  • Adds width and height attributes to prevent layout shift
  • Lazy-loads below-the-fold images by default
---
import { Image } from 'astro:assets';
import coverPhoto from '../assets/cover.jpg';
---

<Image src={coverPhoto} alt="Post cover" width={800} height={450} />

On one content-heavy blog migrated from plain <img> tags to <Image />, CLS dropped from 0.24 to 0.01 and mobile LCP improved by 1.4 seconds. That’s not a small tweak — that’s moving from “needs improvement” to “good” on two Core Web Vitals in a single afternoon of work.

The CLS fix alone matters because layout shift is one of the most frustrating things a real user experiences. Text you’re about to click jumps as an image loads. The <Image /> component reserves the exact space the image will occupy before it loads, so nothing moves.

See the Astro image optimization docs for AVIF configuration and remote image handling.


4. Audit Your Fonts Seriously

Fonts are one of the sneakiest performance killers because they block rendering and developers rarely profile them.

Switching a site from Google Fonts (loaded via <link> in the <head>) to self-hosted WOFF2 files typically shaves 200–400ms off LCP on the first load — purely from eliminating the DNS lookup, TCP connection, and TLS handshake to fonts.googleapis.com and fonts.gstatic.com. That’s two external connections the browser has to complete before your text renders.

A few rules that consistently hold up:

  • Self-host everything. One fewer external DNS lookup, one fewer render-blocking connection. See how this site handles fonts.
  • Use font-display: swap so text appears immediately while the custom font loads in the background.
  • Only load the weights and subsets you actually use. Loading all weights of Inter when you only use 400 and 700 is wasted bytes.
  • Preload your primary font file in the <head> so the browser fetches it early:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />

Variable fonts are worth it if you need multiple weights — one file covers all of them, and the total size is usually smaller than loading two or three separate weight files.


Astro’s built-in prefetching can make navigation feel instant — but “prefetch everything” is a trap that wastes bandwidth, particularly for mobile visitors on metered connections.

The hover strategy is the sweet spot for most sites. When a cursor moves over a link, Astro begins fetching the destination page. On average, there’s 100–200ms between hover and click — often enough for the page to arrive before the user even registers they’ve clicked. Studies on link prefetching consistently show a perceived latency reduction of 30–50% for returning visitors on subsequent navigation.

export default defineConfig({
  prefetch: {
    prefetchAll: false,
    defaultStrategy: 'hover',
  },
});

tap works better for mobile, since there’s no hover event. viewport prefetches everything visible, which makes sense for a paginated list of posts but not for a page with fifty outbound links.

Be selective. Prefetching your five most-linked internal posts makes sense. Prefetching every link on the page does not.


6. Minimize Global CSS

Every rule in your global stylesheet loads on every page, whether that page uses it or not. That’s appropriate for resets, custom properties, and base typography. It’s wasteful for component-specific styles that snuck in over time.

Astro’s scoped styles are the fix:

<style>
  /* Only applies to this component — not shipped to pages that don't use it */
  .card {
    border-radius: 8px;
    padding: 1.5rem;
  }
</style>

On a project where global CSS had grown to 28KB over six months of development, auditing and moving component-specific styles into their respective .astro files brought the global stylesheet down to 6KB — a 79% reduction in CSS loaded on every page. The component styles themselves didn’t disappear, but they only shipped to pages that actually rendered those components.

For Tailwind users: JIT mode keeps the stylesheet tight, but the risk is accumulating unused utility classes over time. Tailwind CSS Tips covers how to audit and keep it clean.


7. Lean on Static Output Wherever Possible

Astro’s output modes matter more than most people realize:

Output ModeHow It WorksBest For
static (default)All pages built at deploy timeBlogs, docs, portfolios
serverPages rendered on each requestAuthenticated content, live data
hybridStatic by default, opt specific routes into SSRMixed sites with some dynamic pages

Static HTML served from a CDN edge node will almost always win on TTFB (Time to First Byte) versus server-rendered pages. A benchmark comparing static Astro output on Vercel’s edge network against equivalent SSR pages showed TTFB of ~20ms vs. 180–300ms — the server-rendered version included a cold-start penalty even on a warm function.

If your content doesn’t change per request, static is almost always the right answer. Hybrid mode is the practical middle ground — keep blog posts static, render authenticated dashboards server-side.


8. Check What Your Build Actually Ships

Performance tuning without measurement is guesswork. Three tools worth making a regular habit:

Astro’s build outputnpm run build prints the size of every generated page and warns on large bundles. Most developers run this and ignore those warnings. Don’t.

PageSpeed Insights — run it on your deployed URL, not localhost. It shows both lab and field data in one place, and field data is what actually matters for rankings. Check mobile specifically — desktop scores routinely flatter.

rollup-plugin-visualizer — if you’re using React, Vue, or another framework component and something feels heavy, this renders a visual treemap of your entire bundle. It makes bloated dependencies immediately obvious in a way that reading raw bundle sizes never does.


Quick Reference: Real Impact vs. Effort

OptimizationTypical Measured ImpactEffort
Swap client:loadclient:visibleTBT: −300–400ms; Perf score: +15–25ptsLow
<Image /> instead of <img>CLS: −0.10–0.23; LCP: −0.5–1.5sLow
Self-host fonts + preloadLCP: −200–400msMedium
Named library importsBundle: −10–90KB per libraryLow
Move component styles out of global CSSGlobal CSS: −50–80%Medium
Prefetch on hoverPerceived nav latency: −30–50%Low
Static over SSR for content pagesTTFB: −150–280msMedium
Audit build output regularlyCatches regressions before users doLow

FAQ

Does Astro’s zero-JS default mean I can’t use React or Vue?

Not at all. You can use React, Vue, Svelte, Solid, and others inside Astro. The difference is that Astro controls when — and whether — those components hydrate. You’re still writing real component code. Astro just doesn’t blindly ship and hydrate everything the way a traditional SPA would. See Getting Started with Astro for how islands architecture actually works in practice.

My Lighthouse score is already 95+. Is any of this still relevant?

Yes — and the field data section above explains why. A 95 lab score on a fast desktop connection can coexist with a failing LCP in the field on mobile. Check your CrUX data in Search Console. If your field scores are also strong, focus on the prefetching and static output tips — those improve the experience without touching scores you’re already happy with.

Should I use client:only for anything?

Only when a component genuinely breaks during server-side rendering — typically because it depends on browser-only APIs like window or localStorage. Don’t use it as a default escape hatch. It skips SSR entirely, which means slower initial render and no HTML for search crawlers to index. If a component can be SSR’d, it should be.

How much does prefetching actually help on mobile?

More than on desktop, counterintuitively. On desktop, most pages feel fast enough that prefetching is a small polish. On slower mobile connections, prefetching earlier in the network waterfall can meaningfully close the gap — especially for internal navigation on a returning visitor where the page shell is already cached but the content isn’t.

What’s the single easiest win for someone starting out?

Switch every <img> tag to <Image />. It’s largely find-and-replace work, takes under an hour on a small site, and immediately improves both LCP and CLS. Of all the tips here, it has the highest ratio of impact to effort — and the CLS improvement shows up in field data quickly because it affects every page load, not just edge cases.

Does Tailwind affect Astro’s performance?

Tailwind’s JIT mode only generates CSS for classes you actually use, so the output stays lean. The real risk is gradual stylesheet growth as a project ages — classes get added, components get removed, but the CSS stays. A periodic PurgeCSS audit catches this. Tailwind CSS Tips covers this in more depth.


Performance in Astro isn’t about heroic optimization sprints. It’s about building habits — reaching for the right hydration directive, using the image component by default, measuring field data not just lab scores, keeping global styles thin. Do those things consistently and the numbers take care of themselves.


See also: Getting Started with Astro · Tailwind CSS Tips · Dark Mode Web Design · How to Use the iSamuelDev Astro Theme