{"id":266,"date":"2026-05-22T13:02:05","date_gmt":"2026-05-22T13:02:05","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/astro-vs-nextjs-2026-when-i-reach-for-each-on-content-sites\/"},"modified":"2026-05-22T13:02:05","modified_gmt":"2026-05-22T13:02:05","slug":"astro-vs-nextjs-2026-when-i-reach-for-each-on-content-sites","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/astro-vs-nextjs-2026-when-i-reach-for-each-on-content-sites\/","title":{"rendered":"Astro vs Next.js in 2026: When I Reach For Each on Content Sites"},"content":{"rendered":"<p>Confession: I built the same marketing site twice last year. Once in Next.js because I&rsquo;d been living in App Router land for months and reached for what I knew. Then again in Astro three weeks later, because the first build kept shipping a 280KB JS bundle to render four cards and a hero image. Same content. Same designer. Same client. The Astro version came in at 14KB of JS on the home page and stopped being a thing I had to defend in performance reviews.<\/p>\n<p>I&rsquo;m not here to tell you Astro wins. It doesn&rsquo;t, for the work I usually do. But the decision rule isn&rsquo;t &ldquo;which framework is better,&rdquo; it&rsquo;s &ldquo;which one matches the shape of the site I&rsquo;m building.&rdquo; So here&rsquo;s how I actually pick now, with the receipts.<\/p>\n<h2 id=\"what-each-one-is-actually-doing\">What each one is actually doing<\/h2>\n<p>Both ship HTML. That&rsquo;s where the similarity stops.<\/p>\n<p>Next.js, with the App Router, treats every page as a React tree. The server renders it, ships HTML, then hydrates the whole thing on the client so React can take over. With Server Components and Partial Prerendering you can shrink the hydrated portion, but the mental model is still React-first: you&rsquo;re writing components that may eventually run in the browser.<\/p>\n<p>Astro flips the default. Pages are static HTML by default and ship zero JS unless you opt in. When you do need interactivity, you wrap a component in a client directive (<code>client:load<\/code>, <code>client:visible<\/code>, <code>client:idle<\/code>) and only that component hydrates. The rest stays as cold HTML. The Astro docs call this <a href=\"https:\/\/docs.astro.build\/en\/concepts\/islands\/\" rel=\"nofollow noopener\" target=\"_blank\">islands architecture<\/a> and it&rsquo;s the whole point of the framework.<\/p>\n<p>Here&rsquo;s the practical difference. A pricing page with a billing toggle, a region selector, and a small cost calculator. In Next.js, even with <code>\"use server\"<\/code> directives and aggressive RSC discipline, you&rsquo;re shipping React&rsquo;s runtime so those three components can hydrate. In Astro, those three become islands, each can be written in a different framework if you want, and the surrounding marketing copy ships as plain HTML.<\/p>\n<h2 id=\"a-real-code-comparison\">A real code comparison<\/h2>\n<p>Same component: a billing-period toggle that swaps prices.<\/p>\n<p>Next.js App Router version:<\/p>\n<pre><code class=\"language-tsx\">\/\/ app\/pricing\/PriceToggle.tsx\n&quot;use client&quot;;\n\nimport { useState } from &quot;react&quot;;\n\nexport default function PriceToggle({ monthly, yearly }: { monthly: number; yearly: number }) {\n  const [annual, setAnnual] = useState(false);\n  const price = annual ? yearly : monthly;\n  return (\n    &lt;div&gt;\n      &lt;button onClick={() =&gt; setAnnual(!annual)}&gt;\n        {annual ? &quot;Billed annually&quot; : &quot;Billed monthly&quot;}\n      &lt;\/button&gt;\n      &lt;p className=&quot;text-3xl&quot;&gt;${price}\/mo&lt;\/p&gt;\n    &lt;\/div&gt;\n  );\n}\n<\/code><\/pre>\n<p>That <code>\"use client\"<\/code> pulls in the React runtime for the whole route. You can structure around it, but the cost is real. I&rsquo;ve seen people forget the directive and then wonder why their interactive component just renders static HTML.<\/p>\n<p>Astro version, with the React renderer installed:<\/p>\n<pre><code class=\"language-astro\">---\n\/\/ src\/pages\/pricing.astro\nimport PriceToggle from '..\/components\/PriceToggle.tsx';\n---\n\n&lt;section&gt;\n  &lt;h1&gt;Pricing&lt;\/h1&gt;\n  &lt;p&gt;Plain HTML, ships as text.&lt;\/p&gt;\n  &lt;PriceToggle client:visible monthly={29} yearly={290} \/&gt;\n&lt;\/section&gt;\n<\/code><\/pre>\n<p>The surrounding <code>&lt;section&gt;<\/code> is HTML. The <code>PriceToggle<\/code> is a React component, but <code>client:visible<\/code> means it only hydrates when it scrolls into view. If a visitor never scrolls there, the React runtime never loads. That&rsquo;s the part that genuinely changed my mind on Astro for content sites.<\/p>\n<h2 id=\"where-nextjs-still-wins\">Where Next.js still wins<\/h2>\n<p>I keep reaching for Next.js when the product is an app, not a site. If half the routes need auth, server actions, optimistic UI, and a real database, Astro&rsquo;s content-first model fights you. Next.js&rsquo;s <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/data-fetching\/server-actions-and-mutations\" rel=\"nofollow noopener\" target=\"_blank\">Server Actions<\/a> and the way mutations flow through the React tree are still the most ergonomic story I&rsquo;ve used for full-stack TypeScript work.<\/p>\n<p>I also lean Next.js when I need streaming for a slow-render page. Next.js can stream Suspense boundaries from the edge, and with PPR the static shell renders instantly while the personalized bits load. Astro has server islands now, which is similar, but the Next.js story is more mature and I&rsquo;ve shipped it more times. I wrote about this in <a href=\"https:\/\/abrarqasim.com\/blog\/nextjs-partial-prerendering-six-months-in-production\/\" rel=\"noopener\">Next.js partial prerendering, six months in production<\/a>. That pattern is hard to replicate cleanly in Astro today.<\/p>\n<p>The team factor matters too. If you&rsquo;ve got React developers and the cognitive cost of a second framework is real, Astro&rsquo;s component syntax is close to JSX but it isn&rsquo;t JSX. The moment you have someone debugging why <code>useState<\/code> doesn&rsquo;t work in a <code>.astro<\/code> file, you&rsquo;ve lost the win.<\/p>\n<p>Finally, I pick Next.js for anything where the routing graph is the product. Dashboards, internal tools, multi-tenant apps. Astro&rsquo;s file-based routing is fine for content but starts to feel thin compared to App Router&rsquo;s parallel and intercepted routes.<\/p>\n<h2 id=\"where-astro-actually-shines\">Where Astro actually shines<\/h2>\n<p>Content sites. I can&rsquo;t stress this enough. If the site is mostly text and images, with a handful of interactive bits, Astro&rsquo;s bundle story is so far ahead it isn&rsquo;t a fair fight. The home page I ported went from ~280KB of JS to ~14KB. The Lighthouse perf score went from 78 to 99. The build time dropped from 90 seconds to 12.<\/p>\n<p>Documentation. <a href=\"https:\/\/docs.astro.build\/en\/guides\/content-collections\/\" rel=\"nofollow noopener\" target=\"_blank\">Content collections<\/a> give you typed frontmatter, schema validation via Zod, and a query API that feels like it&rsquo;s been designed by someone who&rsquo;s actually maintained a docs site. I rewrote a 200-page docs section in two days because the typing caught half the broken cross-links during build, not in production.<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/content\/config.ts\nimport { defineCollection, z } from 'astro:content';\n\nconst docs = defineCollection({\n  type: 'content',\n  schema: z.object({\n    title: z.string(),\n    updated: z.date(),\n    deprecated: z.boolean().default(false),\n  }),\n});\n\nexport const collections = { docs };\n<\/code><\/pre>\n<p>That schema runs at build time. If a doc page is missing <code>updated<\/code>, the build fails before it ships. I&rsquo;ve spent way too many hours debugging &ldquo;why is the date showing as undefined&rdquo; in MDX-on-Next setups to not appreciate that.<\/p>\n<p>Multi-framework projects. Astro lets you mount React, Vue, Solid, Preact, and Svelte components on the same page. I had a client whose old marketing site was Vue and whose new dashboard was React. Astro let me ship both during the migration without rewriting either. Try that with Next.js and you&rsquo;re shipping two runtimes.<\/p>\n<h2 id=\"speed-numbers-with-caveats\">Speed numbers, with caveats<\/h2>\n<p>I keep my own benchmarks because public ones are usually rigged for whichever framework paid for them. On a real four-page marketing site (about 8,000 words of content, two interactive components):<\/p>\n<p>Next.js 15 App Router with PPR: 247KB of JS shipped, LCP 1.8s on a throttled 4G connection, Lighthouse perf score 87.<\/p>\n<p>Astro 5 with the same React components as islands: 14KB of JS shipped, LCP 0.9s on the same connection, Lighthouse perf score 99.<\/p>\n<p>Caveats: I&rsquo;m comparing two builds I wrote, on the same hosting (Cloudflare Pages), with the same designer&rsquo;s assets. Your numbers will be different. But the gap is large enough that I trust the direction even if the exact figures move.<\/p>\n<p>The Next.js team is closing this gap fast. PPR plus the React Compiler will eat a chunk of it. But &ldquo;eat a chunk of it&rdquo; is not &ldquo;match a framework that ships zero JS by default.&rdquo;<\/p>\n<h2 id=\"my-actual-decision-rule\">My actual decision rule<\/h2>\n<p>Before I start a project, I ask three questions.<\/p>\n<p>Is more than 60% of the site read-only content? If yes, lean Astro. If no, lean Next.js.<\/p>\n<p>Does the team already know React deeply, and is the second-framework tax painful? If yes, even a content-heavy site can be Next.js. Velocity matters more than the bundle.<\/p>\n<p>Is there a complex routing graph or a real-time streaming requirement? If yes, Next.js. Astro will fight you on this one.<\/p>\n<p>That&rsquo;s it. I&rsquo;ve stopped having Strong Opinions about which framework is correct. I use both, on different projects, in the same month. The work I do across <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">client builds and side projects<\/a> splits roughly 70\/30 Next.js to Astro right now, which feels about right for the mix of apps vs. content sites I take on.<\/p>\n<h2 id=\"what-to-try-this-week\">What to try this week<\/h2>\n<p>If you&rsquo;ve never used Astro: take one of your existing Next.js content pages, like an About page or a blog post template, and rebuild it in Astro. Time the build. Check the bundle. You&rsquo;ll either be converted or confirm that your case is genuinely app-shaped. Either answer is useful.<\/p>\n<p>If you&rsquo;re an Astro convert wondering when Next.js earns its place back: try building anything with real auth and mutations. The first time you reach for <code>useState<\/code> in an Astro file and realize you can&rsquo;t, you&rsquo;ll feel the limit.<\/p>\n<p>The good news in 2026 is that you don&rsquo;t have to pick one. Pick the one that matches the shape of this project. The framework war is mostly over, and the cost of being wrong has dropped to about a weekend.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I shipped the same marketing site in Next.js and then in Astro, then kept score. Here&#8217;s when I actually reach for each, with bundle numbers and code.<\/p>\n","protected":false},"author":2,"featured_media":265,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I shipped the same marketing site in Next.js and then in Astro, then kept score. Here's when I actually reach for each, with bundle numbers and code.","rank_math_focus_keyword":"astro vs next js","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[138,35],"tags":[309,311,38,44,61,19,41,310],"class_list":["post-266","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-frontend","category-web-development","tag-astro","tag-content-sites","tag-frontend","tag-javascript","tag-nextjs","tag-performance","tag-react","tag-ssg"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/266","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=266"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/266\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/265"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=266"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=266"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=266"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}