When I started building content-focused websites with Next.js and React, I quickly realized that I was shipping way too much JavaScript for what I actually needed. My Lighthouse performance scores were stuck in the 70-80 range, and users on mobile devices were experiencing slow load times. After extensive research and testing, I made the decision to migrate to Astro with SolidJS for interactive components—and the results exceeded my expectations.
In this article, I’ll share my complete migration journey from Next.js/React to Astro/SolidJS, including the problems I encountered, the solutions I implemented, and the performance improvements I achieved. By the end, you’ll understand why Astro + SolidJS is the optimal performance configuration for content websites in 2026.
Table of contents
Open Table of contents
- Why Astro + SolidJS? The Motivation
- The Migration in Detail: Next.js → Astro
- React → SolidJS: The Islands Migration
- Common Problems and Solutions
- Results and Learnings: Next.js/React → Astro/SolidJS
- Conclusion
Why Astro + SolidJS? The Motivation
The Performance Problem: Next.js/React vs. Astro/SolidJS
When building content-focused websites like blogs, documentation sites, or information portals, Next.js with React introduces significant overhead that isn’t necessary for most use cases. Here’s what I discovered:
Next.js/React Stack:
- Framework overhead: Next.js adds its own runtime and build system
- React Runtime: ~45KB gzipped, loaded even for minimal interactive components
- Full-Page Hydration: Every page component gets hydrated, even if it’s static
- Bundle Size: A simple calculator component with React Islands = ~150KB+ total bundle
Astro/SolidJS Stack:
- Zero JS by default: Astro ships 0KB JavaScript for static content
- SolidJS Islands: Only interactive components load JavaScript (~5-10KB)
- Islands Architecture: Only the parts that need interactivity are hydrated
- Bundle Size: Same calculator component with SolidJS = ~10KB total bundle
The React Bundle Size Problem
The main issue I encountered was React’s runtime overhead. Even when using React Islands in Astro, React still loads its entire runtime (~45KB gzipped) for every interactive component. This means:
- A simple form with validation: ~150KB bundle (React + react-hook-form + validation libraries)
- A chart component: ~200KB+ bundle (React + charting library)
- Multiple interactive components: The bundle size grows linearly
For content websites where most pages are static, this is complete overkill. Users on mobile devices or slow connections pay the price with longer load times and higher data usage.
Performance Comparison
| Metric | Next.js/React | Astro/SolidJS | Improvement |
|---|---|---|---|
| Initial Bundle (Static Page) | ~150KB | ~0KB | 100% reduction |
| Interactive Component | ~150KB | ~10KB | 93% reduction |
| Lighthouse Performance | 70-80 | 95+ | +15-25 points |
| Time to Interactive | ~3s | ~0.5s | 83% faster |
| Core Web Vitals | Good | Excellent | Significant improvement |
SEO Advantages
Astro’s static-first approach provides several SEO benefits:
- Complete SSG: All pages are generated as static HTML at build time
- No JavaScript Dependency: Search engines can index content without executing JavaScript
- Faster Load Times: Better Core Web Vitals lead to higher search rankings
- Simpler Sitemap Generation: Full control over all generated pages
When Astro/SolidJS is the Better Choice
Astro + SolidJS is ideal for:
- Content-focused websites (blogs, documentation, information portals)
- Websites with minimal interactivity (most pages are static)
- SEO-critical projects (maximum performance is important)
- Multilingual websites (simpler i18n through file-based routing)
Next.js/React remains better for:
- Highly dynamic web applications (dashboards, SaaS platforms)
- Real-time features (WebSockets, Server-Sent Events)
- Complex server logic (API routes with database connections)
The Migration in Detail: Next.js → Astro
Syntax Conversion: Next.js to Astro
Converting from Next.js to Astro requires several syntax changes, but they’re straightforward once you understand the patterns.
JSX to Astro Components
Next.js:
export default function Card({ title, children }) {
return (
<>
<div className="card">
<h2 className="card-title">{title}</h2>
<div className="card-content">{children}</div>
</div>
</>
);
}
Astro:
---
interface Props {
title: string;
}
const { title } = Astro.props;
---
<div class="card">
<h2 class="card-title">{title}</h2>
<div class="card-content">
<slot />
</div>
</div>
Key changes:
className→classchildren→<slot />- Fragments (
<> </>) are not needed in Astro keyattribute is not needed (React-specific)
Links and Images
Next.js:
import Link from 'next/link';
import Image from 'next/image';
<Link href="/about">About</Link>
<Image src={myImage} alt="Description" width={800} height={600} />
Astro:
---
import { Image } from 'astro:assets';
import myImage from '../assets/image.jpg';
---
<a href="/about">About</a>
<Image src={myImage} alt="Description" width={800} />
Astro automatically optimizes links and images, so you don’t need special components.
Data Fetching
Next.js:
export async function getStaticProps() {
const posts = await fetchPosts();
return { props: { posts } };
}
export default function Page({ posts }) {
return <div>{/* render posts */}</div>;
}
Astro:
---
const posts = await fetchPosts();
---
<div>
{posts.map(post => (
<article>{post.title}</article>
))}
</div>
Data fetching happens directly in the frontmatter (the code fence between ---), making it simpler and more intuitive.
Content Collections Setup
Astro’s Content Collections provide type-safe content management with automatic validation.
Schema Definition (src/content/config.ts):
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
// Important: Use z.coerce.date() for date fields
// YAML interprets "2025-12-13" as a Date object, not a string
date: z.coerce.date(),
lastModified: z.coerce.date().optional(),
author: z.string().optional(),
tags: z.array(z.string()).optional(),
draft: z.boolean().optional().default(false),
}),
});
export const collections = {
blog: blogCollection,
};
Important Note: Always use z.coerce.date() instead of z.string().transform() for date fields, because YAML automatically converts date strings to Date objects.
Using Content Collections:
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? !data.draft : true;
});
---
{posts.map(post => (
<article>
<h2>{post.data.title}</h2>
<p>{post.data.description}</p>
</article>
))}
i18n Strategy
For multilingual websites, Astro’s file-based routing makes i18n straightforward.
Locale Configuration:
// src/i18n/config.ts
export const locales = ['de', 'en', 'uk', 'ru', 'ar', 'fa', 'es', 'tr'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'de';
Localized Routes:
- Default locale (German):
/(no prefix) - Other locales:
/en/,/uk/, etc. - RTL support: Automatic for Arabic (
ar) and Persian (fa)
Implementation:
---
// src/pages/[locale]/index.astro
import { locales, defaultLocale, type Locale } from '../../i18n/config';
export function getStaticPaths() {
return locales
.filter(l => l !== defaultLocale)
.map(locale => ({
params: { locale },
props: { locale }
}));
}
const { locale } = Astro.props as { locale: Locale };
---
<Layout locale={locale}>
<h1>Welcome</h1>
</Layout>
From React Islands to SolidJS Islands
Initially, I tried using React Islands in Astro, but I quickly ran into the bundle size problem I mentioned earlier. Here’s why I switched to SolidJS:
React Islands:
- Loads React runtime (~45KB) even for minimal components
- VDOM overhead for simple forms
- Hydration costs for small interactive elements
- Example: Calculator with React = ~150KB+ bundle
SolidJS Islands:
- No runtime library: Compiles to minimal JavaScript
- No VDOM: Direct DOM updates without overhead
- Signals: Granular reactivity instead of component re-renders
- Example: Same calculator with SolidJS = ~10KB bundle
React → SolidJS: The Islands Migration
The React Problem: Bundle Size at Islands
When I first migrated to Astro, I kept using React for interactive components. This seemed like a natural choice since I was already familiar with React. However, I quickly discovered that React’s runtime overhead was killing my performance gains.
The Problem:
- React runtime loads ~45KB gzipped, even for minimal Islands
- VDOM overhead for simple forms and interactions
- Hydration costs even for small interactive components
- Example: A calculator component with React Islands = ~150KB+ total bundle
- Performance impact on mobile devices was significant
Real-World Impact: On a typical content page with a calculator, the bundle breakdown looked like this:
- React runtime: ~45KB
- React DOM: ~130KB
- Form library (react-hook-form): ~15KB
- Validation library: ~10KB
- Total: ~200KB+ for a simple form
This was completely unacceptable for a content website where most pages are static.
Why SolidJS is the Better Choice
After researching alternatives, I discovered SolidJS and realized it was the perfect solution for my use case.
Key Advantages:
- No Runtime Library: SolidJS compiles to minimal JavaScript. There’s no runtime to load.
- No VDOM: Direct DOM updates without virtual DOM overhead
- Signals: Granular reactivity instead of component re-renders
- Bundle Size: ~5-10KB instead of ~45KB+ for React
- Performance: Fastest runtime available (as of early 2026)
Bundle Size Comparison:
| Component Type | React Bundle | SolidJS Bundle | Reduction |
|---|---|---|---|
| Simple Form | ~150KB | ~10KB | 93% |
| Chart Component | ~200KB | ~15KB | 92% |
| Calculator | ~180KB | ~12KB | 93% |
The Optimal Performance Configuration (Early 2026)
After extensive testing and benchmarking, I’ve determined that Astro + SolidJS is the optimal performance configuration for content websites with interactive elements in 2026.
The Stack:
- Astro: 0KB JS for static parts (header, footer, content)
- SolidJS Islands: Minimal bundle (~5-10KB) for interactive components
- Result: Best performance for content websites with interactive elements
Benchmark Results:
| Metric | React Islands | SolidJS Islands | Improvement |
|---|---|---|---|
| Bundle Size | ~150KB | ~10KB | 93% smaller |
| Time to Interactive | ~2.5s | ~0.4s | 84% faster |
| Lighthouse Performance | 75-85 | 95+ | +10-20 points |
| First Contentful Paint | ~1.2s | ~0.3s | 75% faster |
Lighthouse Scores:
- With React Islands: Performance 70-80
- With SolidJS Islands: Performance 95+
Practical Example: Calculator Migration from React to SolidJS
Let me show you a concrete example of migrating a calculator component from React to SolidJS.
Before: React Implementation
// React Calculator Component
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const schema = z.object({
amount: z.number().min(0),
rate: z.number().min(0).max(100),
});
export function Calculator() {
const [result, setResult] = useState(0);
const form = useForm({
resolver: zodResolver(schema),
});
const onSubmit = (data) => {
const calculated = data.amount * (data.rate / 100);
setResult(calculated);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<input {...form.register('amount', { valueAsNumber: true })} />
<input {...form.register('rate', { valueAsNumber: true })} />
<button type="submit">Calculate</button>
{result > 0 && <div>Result: {result}</div>}
</form>
);
}
Bundle Size: ~150KB (React + react-hook-form + zod)
After: SolidJS Implementation
// SolidJS Calculator Component
import { createForm, zodForm } from '@modular-forms/solid';
import { createSignal, Show } from 'solid-js';
import { z } from 'zod';
const schema = z.object({
amount: z.number().min(0),
rate: z.number().min(0).max(100),
});
export function Calculator() {
const [result, setResult] = createSignal(0);
const [form, { Form, Field }] = createForm({
validate: zodForm(schema),
});
const handleSubmit = (values) => {
const calculated = values.amount * (values.rate / 100);
setResult(calculated);
};
return (
<Form onSubmit={handleSubmit}>
<Field name="amount" type="number">
{(field, props) => (
<input {...props} value={field.value ?? ''} />
)}
</Field>
<Field name="rate" type="number">
{(field, props) => (
<input {...props} value={field.value ?? ''} />
)}
</Field>
<button type="submit">Calculate</button>
<Show when={result() > 0}>
<div>Result: {result()}</div>
</Show>
</Form>
);
}
Bundle Size: ~10KB (SolidJS + Modular Forms + zod)
Key Differences:
useState→createSignaluseForm(react-hook-form) →createForm(Modular Forms){condition && <Component />}→<Show when={condition()}>value→value()(signals are functions)className→class
Code Examples: React vs. SolidJS
State Management
React:
const [value, setValue] = useState(0);
useEffect(() => {
// Side effects
}, [dependencies]);
SolidJS:
const [value, setValue] = createSignal(0);
createEffect(() => {
// Side effects - automatically reactive
});
Conditional Rendering
React:
{condition && <Component />}
{condition ? <A /> : <B />}
SolidJS:
<Show when={condition()}>
<Component />
</Show>
<Show when={condition()} fallback={<B />}>
<A />
</Show>
Lists
React:
{array.map(item => <Item key={item.id} item={item} />)}
SolidJS:
<For each={array()}>
{(item) => <Item item={item} />}
</For>
Common Problems and Solutions
CSS Styles Not Loading
Problem: Styles weren’t being applied after migration.
Solution: Create a postcss.config.mjs file in the Astro project root:
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Then import your CSS in the layout:
---
import '../styles/globals.css';
---
React Components Can’t Be Imported Directly
Problem: React components (like CardText, CardTitle) can’t be used directly in .astro files.
Solution: Replace React components with native HTML elements:
<!-- Instead of React components -->
<h1 class="text-4xl font-bold">Title</h1>
<p class="text-lg">Description</p>
<!-- Instead of VerticalCol -->
<div class="space-y-8">
<!-- Content -->
</div>
For interactive components, use them as Islands with client directives:
---
import MyComponent from '../components/MyComponent';
---
<MyComponent client:load />
getContentFiles Doesn’t Work
Problem: getContentFiles is Next.js-specific and doesn’t work in Astro.
Solution: Use Astro’s Content Collections:
---
// ❌ Wrong - Next.js specific
import { getContentFiles } from '@assetfux/shared/utils/server';
const posts = getContentFiles('blog');
// ✅ Correct - Astro Content Collections
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? !data.draft : true;
});
---
Import Paths Don’t Work
Problem: Import paths like @ui/src/components don’t resolve.
Solution: Check your alias configuration in astro.config.mjs:
vite: {
resolve: {
alias: {
'@ui': '../../@ui/src', // Alias points to src
'@': './src',
},
},
},
Then use imports without /src:
// ❌ Wrong
import { Component } from '@ui/src/components';
// ✅ Correct
import { Component } from '@ui/components';
Date Schema Errors
Problem: InvalidContentEntryDataError: date: Expected type "string", received "date"
Solution: Use z.coerce.date() instead of z.string().transform():
// ❌ Wrong
date: z.string().transform((str) => new Date(str)),
// ✅ Correct
date: z.coerce.date(),
z.coerce.date() accepts both strings and Date objects, which is important because YAML automatically converts date strings to Date objects.
Results and Learnings: Next.js/React → Astro/SolidJS
Performance Improvements
The migration results exceeded my expectations:
Lighthouse Scores:
- Before: Next.js with React Islands (Performance 70-80)
- After: Astro + SolidJS Islands (Performance 95+)
- Improvement: +15-25 points
Bundle Size Reduction:
- Before: React Islands ~150KB per interactive component
- After: SolidJS Islands ~10KB per interactive component
- Reduction: 60-70% less JavaScript overall
Time to Interactive:
- Before: ~3 seconds
- After: ~0.5 seconds
- Improvement: 83% faster
Concrete Numbers:
- React Islands: ~150KB bundle for a calculator
- SolidJS Islands: ~10KB bundle for the same calculator
- 93% reduction in bundle size
SEO Improvements
- Complete SSG: All pages are static HTML without JavaScript dependency
- Better Core Web Vitals: LCP, FID, and CLS all improved significantly
- Faster Indexing: Search engines can index content immediately
- Better Rankings: Improved performance leads to higher search rankings
Developer Experience
Astro:
- Simpler setup and configuration
- More intuitive data fetching (directly in frontmatter)
- Better TypeScript support with Content Collections
- Easier deployment (pure static files)
SolidJS:
- Faster development with Signals (no dependency arrays to manage)
- Better performance during development (no VDOM overhead)
- Type-safe forms with Modular Forms + Zod
- Smaller mental model (signals are simpler than hooks)
Lessons Learned
-
React is Overkill for Content Websites: The bundle size problem is real and significant. For content-focused sites, React’s overhead isn’t justified.
-
SolidJS is the Optimal Choice for Islands (Early 2026): After extensive testing, SolidJS provides the best performance-to-bundle-size ratio for interactive components in Astro.
-
Islands Architecture Maximizes Performance: Only loading JavaScript for interactive parts is the key to achieving 95+ Lighthouse scores.
-
Next.js/React → Astro/SolidJS is the Right Migration for Content Websites: If you’re building content-focused sites, this migration path provides the best performance improvements.
Recommendations for Other Developers
Use Astro + SolidJS when:
- Building content-focused websites (blogs, portals, documentation)
- Performance is critical (SEO, mobile users, slow connections)
- Most pages are static with minimal interactivity
- Bundle size is a concern
Stick with Next.js/React when:
- Building complex web applications (dashboards, SaaS platforms)
- You need server-side features (API routes, database connections)
- Real-time features are required (WebSockets, Server-Sent Events)
- The application is highly dynamic
The Bottom Line: Astro + SolidJS = Best Performance Configuration for 2026 for content websites. If you’re building a blog, documentation site, or information portal, this stack will give you the best performance with the smallest bundle size.
Conclusion
Migrating from Next.js/React to Astro/SolidJS was one of the best decisions I made for my content websites. The performance improvements were dramatic, and the developer experience improved significantly.
Key Takeaways:
- Next.js/React → Astro/SolidJS: The right migration for content websites
- Astro eliminates JavaScript overhead for static content (0KB JS by default)
- SolidJS is the optimal choice for Islands (Stand Anfang 2026)
- React bundle size was the main problem (~150KB → ~10KB reduction)
- Performance improvement: 70-80 → 95+ Lighthouse Score
For Whom is Astro + SolidJS Suitable?
- Content-focused websites (blogs, portals, documentation)
- Websites with minimal interactivity
- SEO-critical projects
- When React bundle size is a problem
Outlook: Astro + SolidJS represents the state-of-the-art performance stack for 2026. Both frameworks are actively developed and continuously improved. While Next.js/React remains relevant for complex web applications, Astro/SolidJS is the clear winner for content websites where performance and bundle size matter.
If you’re considering a similar migration, I highly recommend it. The performance gains are worth the effort, and the developer experience is excellent. Start with a small project, measure the improvements, and scale from there.
Resources: