Back to blog
Astro Web Dev Performance

Building a Scalable Portfolio with Astro

Building a Scalable Portfolio with Astro

Building a Scalable Portfolio with Astro

When I decided to rebuild my personal portfolio, I had a classic “modern web” dilemma. I wanted the speed of a static site (no database queries, no server wait times), the interactivity of a Single Page Application (SPA) for certain components, and the SEO benefits of server-rendered content. Oh, and I didn’t want to spend my weekend wrestling with Webpack configs.

Enter Astro

After three months in production, my portfolio scores a almost perfect 90’s on Lighthouse, loads in under 300ms on 4G, and has room to scale to thousands of pages. Here is exactly how I built it.

The “Islands” Architecture: Why Astro Wins

Traditional frameworks (React, Vue, Svelte) send entire JavaScript bundles to the client. If you have a static header and a non-interactive blog post, React still ships its runtime.

Astro flips the script with Islands Architecture. The page is static HTML (zero JavaScript). Interactive components (a dark mode toggle, a canvas animation, a search bar) are “islands” hydrated individually.

Result: If you disable JavaScript on this blog post, it still renders perfectly. The menu just won’t open. That is performance by default.

Step 1: Laying the Foundation

I started with the strictest setup possible.

npm create astro@latest -- --template basics --typescript strict --git yes

I chose strict TypeScript and the basics template. No bloat. Folder structure for scale:

src/
├── components/    # (Button.astro, Card.astro, SEO.astro)
├── layouts/       # (BaseLayout.astro, ProjectLayout.astro)
├── pages/         # (index.astro, projects/[slug].astro)
├── content/       # (projects/, blog/) <- Astro Content Collections
└── styles/        # (global.css)

Step 2: Content Collections for Future-Proofing

For a portfolio to scale, your content cannot be hardcoded in JSX. Astro’s Content Collections changed my life. Instead of React components importing markdown files, I defined a schema. src/content/config.ts:

import { defineCollection, z } from 'astro:content';
const projects = defineCollection({
  schema: z.object({
    title: z.string(),
    pubDate: z.date(),
    tech: z.array(z.string()),
    featured: z.boolean().default(false),
    liveUrl: z.string().url().optional(),
  }),
});
export const collections = { projects };

Now, when I add a new project markdown file, Astro validates the frontmatter automatically. TypeScript gives me autocomplete for entry.data.title. Zero runtime errors.

Step 3: The “Hybrid” Rendering Trick

My portfolio has two types of pages:

  1. Static: Homepage, About, Contact. (Built at deploy time)
  2. Dynamic: Individual project pages. (Scalable, but still static) By default, Astro is static. But what about a “Recent Views” counter? That needs a server. I enabled Hybrid mode in astro.config.mjs:
import { defineConfig } from 'astro/config';
export default defineConfig({
  output: 'hybrid', // SSR for some, static for others
});

Now, my “Contact” form uses an Astro endpoint (/api/contact.ts) that runs server-side. The blog remains static. Best of both worlds without paying for a backend.

Step 4: Optimizing Images for Scale

Nothing kills a portfolio’s speed like unoptimized 5MB PNGs. Astro’s built-in <Image /> component is non-negotiable.

Before (bad):

<img src="/screenshot.png" />

After (good):

---
import { Image } from '@astrojs/image/components';
---
<Image
  src={entry.data.heroImage}
  alt="Project dashboard"
  widths={[600, 1200, 1800]}
  sizes="(max-width: 800px) 100vw, 800px"
/>

This generates multiple resolutions, lazy loads, and outputs WebP automatically. My image weight dropped from 4.2MB to 89KB average.

Step 5: The Scalability Test (100 Projects)

To test scale, I generated 100 dummy project pages using a Node script that created markdown files. The build time comparison:

  • Gatsby (GraphQL): 4m 12s
  • Next.js (Static Export): 2m 18s
  • Astro: 11 seconds Why? Astro doesn’t bootstrap a virtual DOM during build. It renders straight to strings. Pro Tips I Learned the Hard Way
  1. Use View Transitions for free Astro 3.0+ includes built-in view transitions. Add it to your layout:
---
import { ViewTransitions } from 'astro:transitions';
---
<html>
  <head>
    <ViewTransitions />
  </head>
</html>

Now page navigations are SPA-fast, but you still get static HTML. It feels magic.

  1. Deploy to Github Pages You can see the tutorial on how to deploy to Github Pages.

  2. Don’t over-island Only add client:load to components that truly need JavaScript on initial paint. Use client:visible for fold components or client:idle for non-critical widgets. The Final Verdict Six months ago, my React portfolio shipped 247kB of JavaScript to render a static bio and three images.

    My Astro portfolio ships 8.7kB (mostly analytics and a dark mode toggle).

    The developer experience is unmatched: · Bring your own animation or use Vanta JS · No hydration mismatches because the server and client use the same component syntax. · Scales to thousands of pages without slowing down your build pipeline.