Skip to content
Go back

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

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:

Astro/SolidJS Stack:

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:

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:

When Astro/SolidJS is the Better Choice

Astro + SolidJS is ideal for:

Next.js/React remains better for:

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:

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:

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:

SolidJS Islands:

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:

Real-World Impact: On a typical content page with a calculator, the bundle breakdown looked like this:

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:

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:

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:

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:

Bundle Size Reduction:

Time to Interactive:

Concrete Numbers:

SEO Improvements

Developer Experience

Astro:

SolidJS:

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:

Stick with Next.js/React when:

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:

For Whom is Astro + SolidJS Suitable?

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:


Share this post on:

Next Post
Navigating the 2026 Horizon: Why Code, Conscience, and Culture Must Converge