TL;DR: By leveraging Astro 6’s partial hydration (React Islands) and moving global state to Nanostores, you can build a highly interactive portfolio that maintains a perfect 100/100 Lighthouse score and ships zero unnecessary JavaScript to the client.
The modern web is bloated. We have normalized shipping megabytes of JavaScript just to render static text and a few images.
When I set out to rebuild my portfolio, I wanted to prove that you don’t need a heavy Single Page Application (SPA) framework to achieve a premium, app-like feel.
The solution was a hybrid architecture: Astro 6 for the static foundation, and React Islands for isolated interactivity.
Table of Contents
- The Architecture: Static First, Interactive When Necessary
- State Management Across Islands
- The Result: Speed as a Feature
- Architecting for Scale and Resilience
- The Pragmatic Engineer’s Approach
The Architecture: Static First, Interactive When Necessary
Astro operates on a simple premise: ship HTML by default. If a component doesn’t need to be interactive, it shouldn’t execute JavaScript on the client.
In this portfolio, 90% of the UI—the Navbar, the Footer, the ProjectCards, and the content layout—are pure .astro components.
They render on the server at build time and become inert HTML.
However, certain elements demand interactivity:
-
The Command Palette (Cmd+K): Needs to handle complex keyboard events, search filtering, and modal state.
-
The Toast Notification System: Needs to react to global application events.
These are our “Islands.”
Implementing React Islands
Instead of wrapping the entire application in a React context provider, I isolated the interactive components using Astro’s client:* directives.
---
// MainLayout.astro
import { ToastProvider } from '@/components/react/ToastProvider';
import { CommandPalette } from '@/components/react/CommandPalette';
import Navbar from '@/components/astro/Navbar.astro';
---
<body class="bg-zinc-950 text-zinc-300">
<!-- Load immediately because toasts can happen anytime -->
<ToastProvider client:load />
<!-- Defer loading until the main thread is free -->
<CommandPalette client:idle searchData={searchData} />
<!-- Zero JS shipped to the client for the Navbar -->
<Navbar />
<slot />
</body>
Notice the client:idle directive on the CommandPalette. This tells Astro: “Download and execute the React code for this component only after the initial page load has finished.” This ensures the First Contentful Paint (FCP) remains blistering fast.
State Management Across Islands
The biggest challenge with an Islands architecture is state management. Because your React components are isolated instances, they don’t share a common React tree.
You can’t just throw a Context.Provider at the top level.
This is where Nanostores comes in.
Nanostores is an agnostic state manager designed specifically for frameworks like Astro.
It allows you to define stores outside of React, which can then be read by any component—React, Vue, Svelte, or vanilla JavaScript.
// src/store.ts
import { atom } from 'nanostores';
export const isSearchOpen = atom<boolean>(false);
I can now trigger this state from a vanilla JavaScript button inside my static .astro Navbar:
<!-- Inside Navbar.astro -->
<button id="search-trigger">Search</button>
<script>
import { isSearchOpen } from '@/store';
document.getElementById('search-trigger')?.addEventListener('click', () => {
isSearchOpen.set(true);
});
</script>
And my React Island listens to that exact same store:
// Inside CommandPalette.tsx
import { useStore } from '@nanostores/react';
import { isSearchOpen } from '@/store';
export function CommandPalette({ searchData }) {
const $isOpen = useStore(isSearchOpen);
if (!$isOpen) return null;
return <div className="fixed inset-0 z-50">{/* Search UI */}</div>;
}
The Result: Speed as a Feature
This architecture isn’t just about satisfying Lighthouse metrics (though getting a 100/100 is nice).
It’s about respecting the user’s hardware and network constraints.
By strictly separating static content from interactive logic, the site feels instantaneous.
It’s a reminder that performance isn’t just an optimization step you do at the end of a project; it’s a fundamental architectural decision you make at the beginning.
Architecting for Scale and Resilience
The beauty of this approach is that it scales predictably. You aren’t constantly fighting the browser’s main thread to render static content.
As your portfolio expands—perhaps adding detailed project case studies or complex data visualizations—you can selectively hydrate only those specific islands.
This predictability is crucial for maintaining velocity. You know exactly where your state lives, you know exactly when JavaScript is executed, and you have complete control over the loading sequence.
There’s no magic, just solid engineering principles applied systematically.
The Pragmatic Engineer’s Approach
We often get caught up in the allure of complex ecosystems. But the most effective architects understand that every dependency is a liability.
By stripping away the unnecessary and focusing on a static-first methodology, you reduce technical debt from day one.
This portfolio isn’t just a collection of links; it’s a demonstration of a disciplined, zero-BS approach to software development.
It prioritizes the user’s experience above all else, delivering content instantly and interactivity seamlessly.
This is how modern web architecture should be approached: deliberately, efficiently, and with a ruthless focus on performance.