Article notes
Notes, guides, and personal discoveries from things I build, enjoy, and learn.
Astro ships zero JavaScript by default. Alpine adds interactivity with a small, focused footprint. Together they are a strong fit for content-heavy sites, but only if the patterns stay disciplined.
The failure mode I see most often is treating Astro like a React project: reaching for client-side state for things that do not need it, hydrating components when a static output would have done the job, and ending up with a bundle that undermines the performance you chose Astro for in the first place.
The patterns below are the ones that have kept this site fast while still supporting the interactive details that matter.
Default to no JavaScript and work backward
Every Astro component renders to static HTML unless you explicitly ask for client-side behavior. That default is worth protecting.
Before adding x-data or a client: directive, the question I ask is: does this interaction require client-side state, or does it just look like it does?
A surprising amount of UI that appears interactive is actually just presentation logic: showing and hiding elements based on a condition that is already known at request time, applying a class when a value meets a threshold, or rendering one variant of a component versus another. That logic belongs in the template, not in JavaScript.
When client-side behavior genuinely is needed, Alpine is the right tool for everything that does not require a component tree. The x-data, x-show, and x-on directives cover most interactive patterns without shipping a full framework.
Keep x-data close to the element that uses it
Alpine scopes reactive state to the element where x-data is declared. That is a feature, not a limitation.
The temptation is to hoist shared state to a wrapper element high in the tree so multiple components can access it. That works for small cases but makes the page harder to reason about as it grows — you end up with invisible state affecting visible behavior across large parts of the layout.
The pattern I prefer: each interactive unit owns its state. A dropdown owns its open/closed state. A form field owns its error state. An accordion item owns its expanded state. If two elements genuinely need to share state, I look for whether that state belongs in a URL parameter, a cookie, or a server-rendered prop first, before lifting it into a shared Alpine scope.
Use Astro transitions as progressive enhancement
View Transitions in Astro can make navigation feel significantly more polished, but they add JavaScript to every page transition. On content sites, that is worth thinking about carefully.
The pattern I use:
- Enable
<ViewTransitions />globally for pages where the transition adds real perceived value (moving between post and listing pages, for example) - Use
transition:nameon specific elements that should animate — header, hero image, or a shared UI element - Always test with
prefers-reduced-motionin the browser to confirm the transitions degrade gracefully
The motion-reduce path matters because transitions that ignore user preferences damage accessibility without looking broken in a standard audit.
Islands architecture: hydrate behavior, not content
Astro’s island model lets you hydrate specific components without shipping JavaScript for the rest of the page. The question is which directive to use.
These are the choices I make most often:
client:idlefor secondary interactive elements — a newsletter form below the fold, a related-posts carouselclient:visiblefor anything the user needs to scroll to before interacting withclient:onlywhen a component genuinely cannot render on the server (third-party widgets, browser-only APIs)- No directive at all for components that render static output, even if they look interactive in Figma
client:load should be rare on a content site. If something is above the fold and interactive immediately on page load, it probably warrants client:load. For most other cases the deferred options serve the user just as well with a smaller performance cost.
Lazy loading images with Astro’s Image component
Content sites are usually image-heavy. Every image loaded eagerly is a resource competing with the content the user is actually trying to read.
Astro’s built-in <Image> component handles format conversion, size optimization, and lazy loading by default. The pattern I follow:
- Use
<Image>for all content images, cover photos, and project screenshots - Add
loading="eager"andfetchpriority="high"only on the largest contentful image in the viewport — the one that affects LCP - Use
widthandheightexplicitly on every image to prevent layout shift
The combination of WebP output, correct sizing, and deferred loading for below-the-fold images tends to move Lighthouse performance scores significantly without any architectural change.
Minimize global CSS and lean on Tailwind’s output
Tailwind scans the template files at build time and generates only the classes that are used. That output is usually very small. The risk is accumulating custom utilities, base resets, and global styles that override or duplicate what Tailwind already provides.
On content-heavy sites I try to keep the global stylesheet limited to:
- Design token definitions under
@theme - Typography base styles for prose content where Tailwind’s
proseclass is not enough - Custom utilities that would otherwise require component-level repetition
Everything else should be a Tailwind class directly in the template. Global CSS that is not in @theme or a @utility block is worth a second look before it ships.
The performance targets worth tracking
Astro and Alpine together make it achievable to hit Lighthouse ≥ 90 on mobile consistently, but the specific numbers worth watching are:
- LCP (Largest Contentful Paint) under 2.5 seconds on a simulated mid-range mobile device
- CLS (Cumulative Layout Shift) under 0.1 — mostly a function of explicit image dimensions and font loading strategy
- TBT (Total Blocking Time) as close to zero as possible — the main risk is third-party scripts and over-eager hydration
Checking these scores after adding a new interactive section or changing the hydration strategy on a key component catches regressions before they compound.
For the accessibility side of what Astro pages need, the post on Playwright accessibility checks covers how to automate that verification alongside the performance work.
Comments
Powered by GitHub Discussions — sign in with GitHub to join the conversation, orreply on GitHub.


