Moving My Portfolio from Next.js to Astro

Moving My Portfolio from Next.js to Astro
Photo by Cash Macanaya / Unsplash

My portfolio isn’t a typical website. It’s packed with Three.js scenes, scroll-driven animations with Framer Motion, video backgrounds, and interactive branding tools. Every page is a visual experience first, a website second.

For over a year, it ran on Next.js. And for a while, that worked. But after real user testing, the feedback was clear , the site was slow. Some users said their browsers nearly froze. That’s when I decided to rethink everything.

What Worked with Next.js

I don’t want to paint Next.js as the villain here. It gave me a lot.

The developer experience was genuinely great. Setting up the foundation , routing, layouts, API routes , felt smooth. Deployment to Vercel was effortless. The community is massive, so almost every problem I hit had a solution on GitHub or Stack Overflow.

React Server Components gave me a clean mental model for separating server and client logic. And the ecosystem of packages around it meant I could move fast during the initial build.

Where It Started Breaking Down

The cracks appeared gradually.

MDX was a constant fight. Working with MDX in the App Router had rough edges. Syntax highlighting for code blocks didn’t work out of the box , I spent days getting it right. The documentation was confusing at times, and some packages simply didn’t support the App Router yet.

Content management was painful. I built a custom object-based system for my projects , each project was a TypeScript file exporting metadata and a React component. It worked, but it was brittle. There was no schema validation, no type safety on frontmatter, and adding a new project meant wiring up imports manually. I wanted collections, not a pile of files.

The bundle was massive. Three.js, Framer Motion, React Three Fiber, Radix UI components , it all shipped to the client. Every page loaded the full React runtime plus all my animation libraries, even for pages that were mostly static text and images.

Real users noticed. After sharing the site with designers, recruiters, and friends, the feedback was consistent: it looks incredible but it’s painfully slow. One person on a mid-range laptop said their browser almost froze on the homepage. That was the turning point.

Why Astro

I could have optimized within Next.js , lazy loading, dynamic imports, bundle splitting. But I wanted to rethink the architecture entirely. The core question was: why am I shipping a full React app when 80% of my pages are static content with a few interactive islands?

Astro answered that question perfectly. Its philosophy is simple , ship zero JavaScript by default, add it only where interaction demands it.

For a portfolio with heavy 3D scenes surrounded by static layouts, sidebars, and content pages, this was exactly right.

The Migration

Content Collections Changed Everything

The biggest immediate win was Astro’s content collections. My messy TypeScript project files became clean MDX files with typed frontmatter validated by Zod schemas at build time.

I set up two collections , one for branding projects, one for UI projects , each with their own schema. Adding a new project now means creating a single MDX file. No imports to wire up, no routing to configure, no maps to update. The collection handles everything.

The blog was even simpler. Frontmatter with title, date, category, and tags. Astro generates the pages, handles the slugs, and gives me typed data everywhere.

Islands Architecture for Heavy Components

This was the real performance unlock. In Next.js, my entire page was a React tree. In Astro, only the parts that need interactivity are React.

My Three.js scenes use client:load , they need to hydrate immediately for the hero experience. Scroll-driven animations that are below the fold use client:visible , they don’t load until the user scrolls to them. Utility components like the color picker in my branding toolbar use client:idle , they load after the browser is done with critical work.

Everything else , navigation, sidebars, footers, project layouts, content rendering , is pure Astro. Zero JavaScript. Static HTML and CSS.

Aggressive Asset Optimization

Moving to Astro forced me to think about every asset.

I converted all images from PNG and JPEG to WebP, cutting file sizes by 30-40%. I compressed my hero videos with FFmpeg, creating separate desktop and mobile versions. Desktop gets a 1280px WebP video, mobile gets a 640px version. A poster image extracted from the first frame shows instantly while the video buffers.

For my Three.js models, I ran them through gltf-transform with Draco compression. The abstract shape on my homepage went from over 2MB to under 100KB.

I also moved from MeshTransmissionMaterial (which renders the scene twice for refraction) to meshPhysicalMaterial on my complex wireframe meshes. Only the simpler geometry keeps the expensive transmission material. That alone roughly doubled the frame rate.

Fonts and Design Details

Astro 6’s built-in Fonts API made font management clean. Instead of manually setting up font-face declarations and preloading, I declared my fonts in the Astro config and the framework handled optimization, subsetting, and self-hosting automatically.

Small thing, but it removed an entire category of performance issues I used to debug.

View Transitions

Astro’s ClientRouter gave me smooth page transitions without building a single-page app. Pages still generate as static HTML, but navigation feels instant with morphing animations between shared elements.

I use transition:name to animate project screenshots from grid thumbnails to full-screen views. The image smoothly expands from its position in the grid to fill the viewport, then morphs back when you close it. All handled by the browser’s View Transitions API , no JavaScript animation library needed.

Dark Mode That Actually Works

This was surprisingly tricky. With the ClientRouter, scripts don’t re-run on navigation, so naive dark mode implementations break when you navigate between pages.

The solution was a window.theme IIFE in the document head that runs before paint, combined with an astro:after-swap listener that re-applies the theme after every page transition. One global API, no state management library, no flash of wrong theme.

The Results

The numbers speak for themselves.

JavaScript shipped to the client dropped dramatically. Pages that were previously full React apps now ship zero JS unless they contain Three.js or scroll animations.

First Contentful Paint improved significantly because the browser renders static HTML immediately instead of waiting for React to hydrate.

The Three.js scenes feel smoother because they’re not competing with the rest of the page for resources. When a canvas is the only JavaScript on the page, it gets the full attention of the browser’s runtime.

Users stopped complaining about freezing. The same homepage that nearly crashed a mid-range laptop now loads in seconds with smooth 3D animations.

What I’d Tell Someone Considering the Same Move

Don’t migrate everything at once. Start with the static pages , blog, project listings, layouts. Keep your React components as-is and render them as islands. You can optimize them later.

Content collections are worth the effort. If you have any kind of repeating content , projects, blog posts, case studies , model them as collections from day one. The schema validation alone saves hours of debugging.

Be honest about what needs JavaScript. Most of my “interactive” components turned out to be static content with a hover effect. Only Three.js scenes and scroll-driven animations truly needed React.

Optimize assets before optimizing code. Converting images to WebP and compressing videos had a bigger impact than any code-level optimization. A 50KB image loads fast no matter what framework serves it.

Ship it. The pursuit of perfect performance is endless. Get it fast enough that users don’t notice, then move on to making the work itself better.


The site still has Three.js, Framer Motion, and heavy scroll animations. It’s still a visual experience first. But now the foundation is honest , static where it can be, dynamic where it must be. That’s all Astro asks for, and it turns out that’s all a portfolio needs.