← Zurück
Astro

How I switched from Next.js/React to Astro/SolidJS in a few simple steps

Olaf Mirel
Free Classic wooden desk with writing materials, vintage clock, and a leather bag. Stock Photo

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 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

MetricNext.js/ReactAstro/SolidJSImprovement
Initial Bundle (Static Page)~150KB~0KB100% reduction
Interactive Component~150KB~10KB93% reduction
Lighthouse Performance70-8095++15-25 points
Time to Interactive~3s~0.5s83% faster
Core Web VitalsGoodExcellentSignificant 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.

Free Classic wooden desk with writing materials, vintage clock, and a leather bag. Stock Photo
Photo by Pixabay

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:

  • classNameclass
  • children<slot />
  • Fragments (<> </>) are not needed in Astro
  • key attribute is not needed (React-specific)

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.

Free Classic wooden desk with writing materials, vintage clock, and a leather bag. Stock Photo

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:

  1. No Runtime Library: SolidJS compiles to minimal JavaScript. There’s no runtime to load.
  2. No VDOM: Direct DOM updates without virtual DOM overhead
  3. Signals: Granular reactivity instead of component re-renders
  4. Bundle Size: ~5-10KB instead of ~45KB+ for React
  5. Performance: Fastest runtime available (as of early 2026)

Bundle Size Comparison:

Component TypeReact BundleSolidJS BundleReduction
Simple Form~150KB~10KB93%
Chart Component~200KB~15KB92%
Calculator~180KB~12KB93%

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:

MetricReact IslandsSolidJS IslandsImprovement
Bundle Size~150KB~10KB93% smaller
Time to Interactive~2.5s~0.4s84% faster
Lighthouse Performance75-8595++10-20 points
First Contentful Paint~1.2s~0.3s75% 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:

  • useStatecreateSignal
  • useForm (react-hook-form) → createForm (Modular Forms)
  • {condition && <Component />}<Show when={condition()}>
  • valuevalue() (signals are functions)
  • classNameclass

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

Free Classic wooden desk with writing materials, vintage clock, and a leather bag. Stock Photo
During my migration, I encountered several common issues. Here are the solutions that worked for me:

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

  1. React is Overkill for Content Websites: The bundle size problem is real and significant. For content-focused sites, React’s overhead isn’t justified.

  2. 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.

  3. Islands Architecture Maximizes Performance: Only loading JavaScript for interactive parts is the key to achieving 95+ Lighthouse scores.

  4. 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:

Themen