Reading time: 16 min read

Astro vs Next.js for Sitecore XM Cloud: We built the same site twice

We built one Sitecore XM Cloud site on Astro and again on Next.js App Router, both on the official Content SDK. This post compares integration files, shipped JavaScript, edit-mode wiring, response timing, and developer experience with measured numbers, and says which stack fits which kind of project.

Portrait photo of Sohrab Saboori, article author

What actually differed, with numbers

We built the same site twice — once on Astro with @sitecore-content-sdk/core, once on Next.js App Router with @sitecore-content-sdk/nextjs. Same SitecoreAI tenant (the platform formerly sold as XM Cloud), same content tree, same routes, same components. This post is what we learned.

Up front: neither is a wrong choice. They optimize for different things. The useful framing is "what kind of site are you building, what kind of team is building it", not "which framework is better."

What we measured against. Both repos pin Content SDK 1.4.1 so the two sides match. The Astro app runs Astro 6.3 with React 19; the Next.js app runs Next.js 15.5 with React 19. Sitecore has shipped Content SDK 2.x since: 2.0 moved the Next.js package to Next.js 16 and Node.js 24 in March 2026 (Content SDK 2.0 changelog), and 2.2 added on-demand static revalidation in June 2026 (Content SDK 2.2 changelog). We call out below where that changes the picture. How every number was produced is in the Notes on method section at the end.

TL;DR

 AstroNext.js App Router
Files for Sitecore integration†829
Client/server boundaryOne directive at the mount site (client:load)'use client' at the top of every interactive file
JS on our homepage (gzipped, measured)61 KB — mostly the React runtime our header island pulls in242 KB — React + router + component map + boundaries
Server response (local, warm, measured)~190 ms to first byte and to full page~30 ms to first byte (streamed shell), ~250 ms to full page
Vendor support and ecosystemOfficial framework-agnostic docs with Astro examples; a community adapter; no first-party packageFirst-class: SDK package, CLI, and Sitecore's reference template
Best forMarketing / content sites, multi-framework teamsApp-heavy sites, React shops, larger teams

† Counting files that exist to wire Sitecore in (client setup, routing, editing endpoints, component wiring, config) excluding the site's own UI components and demo pages. For Astro: the client, the catch-all, two editing endpoints, three shared components, and the config. For Next.js: five root wrappers, middleware, three layout/page files, four API routes, two Sitecore configs, two lib files, two content-sdk components, three BYOC files, two i18n files, and five generated .sitecore/ maps. The Astro repo is public if you want to check our count; the Next.js project is internal, so the rule above is what we applied.

If your site is mostly content with sprinkles of interactivity, Astro wins on simplicity and shipped bytes. If your site is mostly app, with auth, dashboards, forms, real-time, complex client state, Next.js wins on ecosystem, tooling, and team familiarity.

How routing differs

Astro uses a single file:

src/pages/[...path].astro

Inside, one await client.getPage(routePath, { site, locale }) resolves the Sitecore route. Site and language come from query strings, falling back to defaults hard-coded in our catch-all. No middleware required for basic operation.

Next.js App Router uses a directory tree:

src/app/[site]/[locale]/[[...path]]/page.tsx
src/middleware.ts
src/app/[site]/layout.tsx
src/app/layout.tsx

The routing parameters live in the URL pattern itself — [site] and [locale] are explicit segments. A middleware chain handles redirects, multisite resolution, and personalization gating before the page component runs.

The tradeoff: Astro's single file is easier to read and reason about; Next.js's segmented structure is easier to extend if you genuinely have many tenants × many locales and need URL-level segmentation. For a one-site-one-locale launch, Astro is half the moving parts.

How the client/server boundary feels

This is the biggest day-to-day difference between the two.

Next.js App Router asks you to declare boundaries at the file level. The 'use client' directive goes at the top of a file and marks everything it exports as a client entry point (Next.js use client reference):

// Providers.tsx
'use client';
import { SitecoreProvider } from '@sitecore-content-sdk/nextjs';
export function Providers({ children }) { return <SitecoreProvider>{children}</SitecoreProvider>; }
// Bootstrap.tsx
'use client';
import { useEffect } from 'react';
useEffect(() => { initializeCloudSdk(); }, []);

Every file that uses state, effects, or browser APIs needs 'use client'. The mental model is "the whole file is one side of the wall." This is a real constraint that shapes architecture: you end up with Provider files, Bootstrap files, and a Layout file just to handle the seams.

Astro declares the boundary at the call site. These are the actual mount lines from our repo:

<!-- HeaderST.astro: the one island on the homepage -->
<HeaderSTReact client:load />
<!-- demo-frameworks.astro: three frameworks on one page -->
<ReactSiteList client:load />
<VueSiteList client:load />
<SvelteSiteList client:load />

The component itself is a regular .tsx/.vue/.svelte file with no special markers. The page decides what hydrates and when: swap client:load for client:visible or client:idle to defer hydration, or client:only to skip server rendering entirely (Astro template directives). Components have no concept of "I'm a client component" baked in.

Why it matters: Next.js's model forces you to think about server/client every time you create a file. Astro's model lets you write the component first and decide later. Neither is "right" — the Next.js model is stricter and easier to verify; the Astro model is faster to author.

Component composition

Astro (trimmed from PlaceholderRenderer.astro; the real file also recurses into partial-design placeholders):

const components = { HeroST, HeaderST, SignupBanner, Video };
{renderings.map((r) => {
  const Comp = components[r.componentName];
  return Comp
    ? <Comp fields={r.fields} params={r.params} />
    : <MissingComponent name={r.componentName} />;
})}

A plain object, a plain mapping. One file. You can read it in 30 seconds.

Next.js:

.sitecore/component-map.ts         # server-side map: 16 project components + 3 SDK built-ins
.sitecore/component-map.client.ts  # client-safe subset: 3 project components + the same built-ins
.sitecore/import-map.ts            # generated, auto-rebuilt
.sitecore/import-map.client.ts     # generated, auto-rebuilt

The Sitecore CLI scans your components folder and writes both maps automatically. Components opt into client mode with componentType: 'client'. This is better for large projects (no manual map maintenance, type safety) but is more moving parts for a small one.

The tradeoff: Astro's manual map is fine for ~10 components and easy to debug. Next.js's generated maps scale better to ~100 components but are harder to grep through and depend on the CLI watcher running.

Edit mode

Both frameworks land in roughly the same place, with very different shapes.

Next.js uses SDK-provided route handlers. This is the complete code for both endpoints in our repo, on Content SDK 1.4:

// app/api/editing/config/route.ts
import { createEditingConfigRouteHandler } from '@sitecore-content-sdk/nextjs/route-handler';
import components from '.sitecore/component-map';
import metadata from '.sitecore/metadata.json';
export const { GET, OPTIONS } = createEditingConfigRouteHandler({ components, metadata });
// app/api/editing/render/route.ts
import { createEditingRenderRouteHandlers } from '@sitecore-content-sdk/nextjs/route-handler';
export const { GET, OPTIONS } = createEditingRenderRouteHandlers({});

Six statements, hidden complexity, "it just works" if your sitecore.config is set up right. Combined with draftMode() in the page (Next.js draftMode reference), the SDK handles the entire handshake.

Astro (covered in Part 2) wires the same flow by hand:

  • A /api/editing/config.ts with explicit CORS + secret checks
  • A /api/editing/render.ts that proxies back to your own catch-all
  • The catch-all itself branches on sc_mode=edit and calls client.getPreview

The tradeoff: Next.js gives you six lines of code and a black box; Astro gives you about 160 lines of code and no black box. If you ever need to debug a CORS issue or a CSP issue, the Astro version tells you exactly where the problem is. If you never need to debug it, the Next.js version is just less code.

Bundle, HTML, and response time

Same homepage (hero, feature banners, video, signup form) from production builds of both apps on one machine, against the same XM Cloud environment. Sizes are gzip bytes on the wire; timings are medians of 10 warm requests over the loopback interface. Details in the Notes on method section at the end.

 AstroNext.js App Router
HTML (gzipped, on the wire)4.2 KB24.6 KB streamed (15.1 KB if gzipped in one pass)
CSS (gzipped)5.5 KB6.5 KB
JS shipped (gzipped)61 KB (4 files)242 KB (15 chunks)
JS shipped (uncompressed)192 KB785 KB
Edge layout call per request (SDK log)~180–250 ms~180–250 ms
First byte (warm, median)~190 ms~30 ms
Full HTML response (warm, median)~190 ms~250 ms

Four things worth spelling out, because the raw table is easy to misread.

Astro's 61 KB is almost entirely the React runtime. Our header is a React island (client:load), and the first React island on a page costs you the React hydration client — 57 KB of the total. The island component itself is 0.9 KB. If the header were a plain .astro component the page would ship no JavaScript at all, and a Svelte island would cost a fraction of that 57 KB. Astro strips client-side JavaScript from every component unless you mark it as an island (Astro islands). So the accurate framing isn't "Astro ships 8 KB", it's "Astro ships nothing by default, and you pay per framework runtime you opt into."

Next.js's 242 KB is the framework being a framework. React runtime, the App Router client (routing, RSC plumbing), the client-side component map, and the error/not-found boundaries the router loads up front. None of it is waste exactly, it's what makes client navigation, streaming, and error recovery work, but it ships whether this particular page needs it or not. The larger HTML (25 KB vs 4 KB on the wire) is the same story: the RSC payload is embedded in the page. Part of that gap is streaming itself: gzipped in one pass the Next.js document is 15 KB, but a streamed response is compressed chunk by chunk and compresses worse.

Neither app caches the layout response at the server; both pay the Edge round-trip on every request. Our first draft claimed Next.js was fast because its Data Cache stored the GraphQL response on disk. It doesn't, in this setup. Next.js 15 does not cache fetch calls unless you opt in (Next.js caching guide), the SDK's GraphQL client sets no cache options, and the route is not prerendered: the page reads draftMode() and request params, the build's prerender manifest has no entry for it, and the response carries Cache-Control: no-store. The SDK log showed the same ~200 ms call to Experience Edge in both apps. Next.js even made it twice per request, once for the page and once for generateMetadata; the two overlap, so most of the second call hides behind the first.

The first-byte gap is streaming, not caching. The Next.js route has a loading.tsx, which wraps the page in a Suspense boundary and lets the server send the layout shell before the page data arrives (Next.js loading.js reference). The browser sees bytes in ~30 ms and the content about 200 ms later. Our Astro catch-all awaits getPage at the top of the file and sends nothing until that resolves; the rendered HTML follows within a few milliseconds, so first byte and full page arrive together at ~190 ms. Full-document time was close, and Astro's actually finished sooner in our runs (~190 ms vs ~250 ms), a gap we didn't try to attribute further. The user-visible difference is a skeleton appearing about 160 ms sooner on Next.js. That is how we wrote the Astro template, not a limit of the framework: Astro's server islands (server:defer) let you send the rest of the page first and fill a slow section in afterwards (Astro server islands); we just didn't use them.

Developer experience

Dev server and hot reload. Astro's dev server starts fast and HMR feels instant. Next.js takes a few seconds to boot; HMR is fast, but cache invalidation in the App Router can be confusing when data looks stale in development.

Production build. Our Astro project builds in about 22 seconds. The Next.js project spends 45 seconds in compilation alone, before linting, type checking, and the Sitecore component-map generation that runs first. Both figures are from our machine in August 2026.

Learning curve. Astro's component model is small, and an experienced front-end developer picks it up quickly. A React shop has no learning curve at all on Next.js, and component authors can keep working the way they already do.

Ecosystem and official coverage. Astro's ecosystem is smaller and there are fewer Stack Overflow answers for Sitecore-specific problems. Sitecore's framework-agnostic docs use Astro and Go for their examples and cover setup, content rendering, and visual editing — thinner than the Next.js docs, but official. Next.js has a far larger ecosystem, and Sitecore's main starter template, CLI, and most first-party guidance target it (Content SDK documentation).

The DX question reduces to "how much novelty cost can your team absorb?" In our experience, once the Sitecore integration is in place, Astro needs less framework-specific ceremony for a conventional content site. A team mid-flight on Next.js features should not switch.

When to pick which

Where Next.js's weight pays off. The moment your page becomes app-like (many interactive widgets, complex client state, lots of suspense) Next.js's "everything is React" model stops being a tax and starts being a feature. The framework's investment in Server Components, partial pre-rendering, and route-level caching pays off when you have a lot of dynamism.

Caching after Content SDK 2.2. The Cache Components starter caches the layout response in Next.js 16 and invalidates it when Sitecore publishes (Content SDK 2.2 changelog). With that in place the Next.js side stops paying the Edge round-trip on warm requests, which is a real architectural advantage that Astro has to build for itself with Cache-Control headers and a CDN. We walked through that model in From ISR to Cache Components.

Pick Astro if:

  • The site is mostly content (marketing, documentation, blog, landing pages)
  • You want minimal JS on the wire by default
  • You have multiple frameworks in your design system or vendor stack
  • The team is comfortable owning a smaller dependency surface
  • You're starting fresh and don't have an existing Next.js codebase

Pick Next.js if:

  • The site has heavy app-like behavior (dashboards, auth flows, real-time, complex forms)
  • Your team is React-only and that's not changing
  • You want first-party Sitecore support, sample repos, and starter templates to just work
  • You expect to use partial pre-rendering, server components, or other recent Next.js features
  • You have an existing Next.js codebase and don't want a split stack

There are also legitimate "do both" scenarios: marketing site on Astro, customer app on Next.js, shared Sitecore content tree underneath. The CMS doesn't care; only the rendering layer needs to know.

Options that didn't exist when we started. Sitecore has published framework-agnostic docs with Astro examples for project setup, content rendering, and visual editing. EXDST ships a community Astro adapter for the Content SDK (@exdst-sitecore-content-sdk/astro) if you want the Next.js-style "the SDK does the wiring" experience on Astro, with the usual third-party maintenance trade-off. And Sitecore's own SDK now reaches past Next.js: Content SDK for Angular 1.0 went GA on September 10, 2026. None of this changes the comparison above, but "Next.js or nothing" is no longer the framing.

What's the same

It's worth being explicit about what doesn't change between the two:

  • The Sitecore content model, Pages editing experience, GraphQL queries, layout service responses, and editor flow are all identical.
  • The Content SDK speaks the same protocol from both sides.
  • Your authors, your content tree, your publish flow, your CDN cache strategy — same.

This means the choice is reversible. If you build on Astro and outgrow it, the porting cost is mostly the rendering layer. Your content investment is safe either way.

Wrap-up

This is the end of the series. We've covered the full Astro + Sitecore stack: initial setup (Part 1), the editing flow that makes XM Cloud Pages work (Part 2), and finally the comparison against Next.js (this post).

The aim has been to give you enough end-to-end detail that you can pick the right tool for your next Sitecore project without guessing. If you build something on top of any of this, we'd love to see it.

Source code. The Astro build is open source at github.com/rikaweb/astro-sitecore-content-sdk, and issues and discussions are open there. The Next.js App Router build is an internal Fishtank project against the same XM Cloud instance, which is why its file count and timings can't be checked independently. This post is Part 3 of a series: Part 1 covers the Astro setup, and Part 2 covers how Sitecore Pages edit mode works.

Notes on method

Builds and servers. Both apps were built for production in August 2026 and every number above was taken from those same builds on September 10, 2026. Astro was built with the Node adapter and served with astro preview; Next.js with next build and next start. Both ran on one Windows machine and talked to the same XM Cloud environment. The Astro repo is public; the Next.js project is internal, so its file count and timings can't be independently reproduced.

Sizes. A script fetched each homepage with Accept-Encoding: gzip, then fetched every script and stylesheet the HTML references — for Astro that includes the component-url and renderer-url attributes on each "astro-island"and the modules those files import. JavaScript and CSS sizes are the sum of compressed bytes. Astro's Node preview server doesn't compress, so its assets were gzipped at level 6 to match. Next.js's polyfill chunk is excluded because its script tag carries noModule and modern browsers skip it.

Timings. Medians of 10 warm requests per URL with curl, reading time_starttransfer for first byte and time_total for the full document. Both servers ran with the Content SDK's debug logger enabled (DEBUG=content-sdk:http,content-sdk:layout), which prints every GraphQL request to Experience Edge and its duration; that is where the "Edge layout call per request" row comes from, and it is how we found that both apps fetch on every request.

The IPv6 trap. Our first Astro timings were about 400 ms, twice what the SDK log said the Edge call cost. The gap was the client, not the server. The Astro preview server had bound the IPv6 loopback address only, while Next.js bound both families:

$ netstat -ano | grep LISTENING | grep ":4321 "
  TCP    [::1]:4321             [::]:0                 LISTENING       21960
$ netstat -ano | grep LISTENING | grep ":61256 "
  TCP    0.0.0.0:61256          0.0.0.0:0              LISTENING       47124
  TCP    [::]:61256             [::]:0                 LISTENING       47124

curl resolves localhost to both addresses and tries whichever the resolver lists first — on this machine, 127.0.0.1. Nothing answers there, so it waits out its happy-eyeballs timer, 200 ms by default (curl manual), before trying [::1]. The connect time shows it directly, and the first-byte time inherits the delay:

$ curl -s -o /dev/null -w "connect %{time_connect}s  ttfb %{time_starttransfer}s
" http://localhost:4321/
connect 0.206778s  ttfb 0.402680s
$ curl -s -o /dev/null -w "connect %{time_connect}s  ttfb %{time_starttransfer}s
" "http://[::1]:4321/"
connect 0.000940s  ttfb 0.180619s
$ curl -4 -s -o /dev/null http://localhost:4321/; echo "exit $?"
exit 7

Verbose output shows the two attempts:

$ curl -v -s -o /dev/null http://localhost:4321/ 2>&1 | grep -E "Trying|Connected"
*   Trying 127.0.0.1:4321...
*   Trying [::1]:4321...
* Connected to localhost (::1) port 4321 (#0)

Next.js was unaffected because it listens on :: and 0.0.0.0, so whichever address curl tried first connected in about a millisecond. The fix for benchmarking is any of: address the server as [::1] or 127.0.0.1 instead of localhost, force one family with curl -4 or curl -6, or bind the server to both families; for the Astro Node adapter that means setting HOST=0.0.0.0 at runtime (Astro Node adapter). On Windows PowerShell, replace /dev/null with NUL and grep with findstr. Browsers and Lighthouse resolve localhost too and have their own fallback timers, so a suspiciously round floor of a few hundred milliseconds in any local benchmark is the first thing to rule out.

Caveats. One page, one machine, one network. The build times in the developer-experience section are from August and were not re-measured. Byte counts will shift with dependency updates; the shape of the comparison should not.

Sources

  1. Astro + Sitecore Content SDK starter (source code)
  2. Part 1: Connecting Sitecore XM Cloud to Astro — Fishtank
  3. Sitecore Content SDK documentation
  4. Introduction to framework-agnostic Sitecore development — Sitecore documentation
  5. Content SDK v2.0 released — Sitecore changelog, March 19, 2026
  6. Content SDK 2.2 released with on-demand static revalidation — Sitecore changelog, June 30, 2026
  7. Content SDK for Angular 1.0 released — Sitecore changelog, September 10, 2026
  8. EXDST Astro Content SDK adapter (community)
  9. Islands architecture — Astro documentation
  10. Template directives reference — Astro documentation
  11. use client directive — Next.js documentation
  12. loading.js file convention — Next.js documentation
  13. Caching and revalidating (previous model) — Next.js documentation
  14. draftMode function — Next.js documentation
  15. From ISR to Cache Components — Fishtank
  16. Server islands — Astro documentation
  17. curl manual: --happy-eyeballs-timeout-ms
  18. Node adapter — Astro documentation