The how and why migrating a legacy e-commerce frontend to Astro + Svelte
Islands architecture applied to e-commerce. What I'd do, how I'd do it, and the mess I'd expect along the way.
The other day I was browsing an online store from a major retailer. I click on a pair of shoes, and I wait.. The page takes 5 seconds to become interactive. I open devtools (dev reflex, can’t help it..) and what do I see? 2.01MB of compressed JavaScript. For a product page.
2MB for a pair of shoes
A product page is essentially a structured document with a few interaction points. 90% of the page is display: images, text, specifications and maybe some reviews.. Data you read, not data you manipulate.
The only places where something actually happens on the user side is the purchase area (picking a variant, adding to cart..), search, and maybe an image carousel. Everything else is rendering from a fixed catalog.
But an SPA framework assumes that everything needs a runtime. A title, a price, an add-to-cart button, everything goes through the same pipeline. Each page ships the full framework, and therefore the hydration of every component.
Result: a product page heavier and slower than gmail.
In practice it looks like this: a product page where every component goes through the runtime, even the ones that just display text.
How we got here
I’d guess there’s an e-commerce platform behind this that was already rendering HTML server-side. It probably wasn’t fast, but at least the browser received HTML and little JavaScript. Then someone wanted to “modernize” by building an SPA on top. The site didn’t get faster, it just got a full client-side rendering pipeline added to it. You’re paying the cost of a full application framework to render what is essentially a brochure.
Tracking scripts and everything else pile on top, but the problem isn’t the layers. It’s the foundation they’re piling on. If that foundation is already a full runtime that hydrates the entire page and ships code the page doesn’t even use, every addition starts from an already high baseline.
Going headless is the first step, but it’s not enough if the frontend remains an SPA. Everyone’s trying to reduce client-side JavaScript, but it’s retrofit on an app-first model, the runtime still travels to the client. What if we rethought the foundation?
Islands architecture
Islands architecture flips the problem. By default it’s static HTML, zero JavaScript. Interactivity is an explicit opt-in, only the components that need it hydrate. No global runtime, no client-side router, no hydration of the entire tree.
The tradeoff is state, each navigation reloads the page and client state disappears. But in e-commerce the states that matter (cart, session, store) already live on the server, each page reads them back.
Astro lets you pick the framework for the interactive parts. If you pick one that compiles without a runtime or virtual DOM, each island ships only its own code. SolidJS and Svelte come to mind. My personal preference is SolidJS (it’s what runs this site), but for an enterprise e-commerce migration Svelte is probably the better pick: larger ecosystem, more resources, easier to hire for.
I have some context with this approach. In 2020 I was contributing to ElderJS, a Svelte framework that did partial hydration before islands architecture existed as a term. I was involved in the plugin system, worked on reducing payload size and hydration footprint, and built elderjs-plugin-i18n. That’s what got me interested in the first place: understanding what partial hydration meant in practice, and why shipping less JavaScript was the right model for content sites.
The result on a product page: a fraction of the current JavaScript. In e-commerce this is directly tied to revenue. Every extra second of load time drops conversion. Core Web Vitals feed Google’s ranking signals, faster pages rank better, attract more organic traffic, convert more. Nobody notices a fast page. Everyone notices a slow one.
The model is there. Now how do you get there without breaking everything.
Migration plan
The migration should be incremental, we don’t want a big bang. The risk of breaking something is real, and the cost of a rollback is high if you have to roll back the entire site. Progressive migration means migrating one part at a time, validating it, then moving to the next.
The principle is simple: start with what doesn’t touch business sensitive parts. Lowest risk first, working your way up to the components that depend on backend work and purchase flows. Each phase validates the next.
One thing to know going in: during the migration, the site will be ugly under the hood. Two CSS systems on every page, components implemented twice.. This is normal. It’s the cost of not doing a big bang. The question isn’t if you’ll have this mess, it’s how long you stay in it.
The most fragile part is the seam between the two systems. Let’s say, a user adds a product from the legacy BuyBox, then the cart badge in the new layout needs to update. This flow crosses two frontends. No unit test can covers that, so E2E tests on critical flows that cross the boundary are the minimum safety net.
And if a migrated page regresses or breaks something, you need per-page rollback at the routing level. Send that one URL back to the legacy without touching the rest. The infrastructure for this is the first thing to put in place.
Phase 1 · Cohabitation
Before migrating anything, the two systems need to coexist. Same URL, same page but two frontends. The most straightforward way to do this is a reverse proxy that decides which system renders what.
Let’s say a user goes to store.com/product/shoes. The reverse proxy receives the request, routes to Astro for one part of the page, to the legacy for the other. The assembled page arrives at the browser as a whole, the user has no idea two systems participated.
CDN rewrite rules (Vercel, Cloudflare, Netlify) can handle the routing too, but a reverse proxy gives more control.
The proxy assembles the page from fragments, the header from Astro, the product content from the legacy, stitched together before the response reaches the browser. Nginx does this with SSI, CDNs with ESI.
This is also what makes rollback possible: change a routing rule and the page goes back to legacy. No redeployment, no downtime.
Once this is in place, the two systems can coexist and the migration can start. Now, what do we migrate first?
Phase 2 · Static markup
It’s time to introduce Astro in this project without breaking anything. Everything that doesn’t need JavaScript gets migrated first. Layout, content, display components. The goal is to move as much as possible to Astro’s server-rendered HTML before touching anything interactive.
Layout first
The layout is the best starting point: it’s identical on every page and once migrated, the whole site benefits. The interactive parts of the layout (e.g: search, cart badge) stay on legacy for now, they’ll move in the next phase with the other islands.
Page content
Then the page content. Start with the least interactive and least critical pages. The first deployments will surface issues you didn’t see with layout alone: routing gaps, design token inconsistencies, build pipeline surprises.. Better to discover those on pages that don’t directly cost revenue.
Things to watch
Shared design tokens (colors, typography, spacing) keep the migrated pages visually consistent with the legacy ones. Both systems consume the same source, even if the CSS implementation differs.
For SEO, URLs must not change. Measure Core Web Vitals before and after each migrated page. If LCP, CLS or INP regress, fix before moving on.
With the static markup in place, the page is lighter but not yet interactive. Time to bring in the islands.
Phase 3 · Interactive components
The static markup is migrated. Now it’s time to add interactivity back, but only where it’s needed. This is the core of the islands model: each interactive component becomes an island that hydrates independently.
Hydration directives
Astro gives you control over when each island becomes interactive. Not everything needs to load immediately.
client:load hydrates the component as soon as the page loads. For things the user needs right away: search, cart badge, burger menu..
client:visible waits until the component scrolls into view. For things below the fold: image carousel, reviews accordion, newsletter form.. Zero JavaScript cost until the user actually sees them.
client:idle waits until the browser has nothing better to do. For low-priority stuff: GDPR popup, analytics widgets..
Island boundaries
If two elements need to react to each other, they belong in the same island. The size selector changes the price and the stock? Same island. If they just need to read the same data independently, they can be separate islands querying the same source.
Not everything needs an island either. A link with analytics tracking doesn’t need hydration. A collapsible section can work with a <details> element. A <script> with event delegation handles a lot of lightweight interactions without shipping a framework. If it doesn’t have state, it probably doesn’t need an island.
SSR is still a valid option. A search results page doesn’t need an island, it’s a form that submits and the server returns a new page. The search bar with autocomplete needs one but the results page does not.
Every component is a tradeoff. Island, SSR, plain HTML, vanilla script.. Even with the islands model, it’s a decision to make for each part of the page.
Once the boundaries are defined, a new question comes up: how do these islands talk to each other?
Cross-system communication
This is probably the biggest mindset shift. In an SPA, state flows through the component tree. Everything is connected, everything is reactive. In islands, each island is isolated. Communication has to be explicit.
During the migration, the legacy still owns parts of the page. It doesn’t know about Svelte stores or reactive state. The lowest common denominator is a custom event on the document. The user adds a product to the cart from the legacy BuyBox, the cart badge in the Astro header needs to update. One dispatchEvent, one listener.
Once the legacy is gone and all islands are Astro, the options open up. Nanostores is built exactly for this: shared state between framework-agnostic islands with reactivity. A natural upgrade from custom events when island-to-island communication gets more complex.
But during migration, custom events are the right starting point. The legacy can fire one without any dependency.
At this point, everything is migrated except one thing. The component that actually makes money.
Phase 4 · The BuyBox
The BuyBox (price, variants, add to cart, shipping) is the component that converts. And probably the hardest piece to migrate.
Why it’s hard
Store-specific pricing is often tied to the legacy session system. The cart depends on the same session. Shipping options, stock availability, everything in the BuyBox talks to the backend in ways that were designed for the old frontend. Exposing all of that as clean APIs that Astro could consume server-side requires backend work that probably isn’t done.
If the two systems can’t share the same cart session, a user who adds a product on a migrated page and navigates to the legacy checkout loses their items. This is often the most blocking issue in headless migrations.
While migrating
The good news: the BuyBox can just stay on the legacy. The reverse proxy is already handling the cohabitation, the BuyBox works, the session is handled. No need to rush it.
Until now, we didn’t touch it and that was fine. The proxy handled the cohabitation, the legacy owned the BuyBox, no risk. But now we want to migrate it, and that means the backend needs to be ready.
When it does get migrated
Once the backend exposes the right APIs, the BuyBox becomes a client:load island.
Two options:
A server island that fetches the price on demand, personalized per user. The rest of the page is cached at the CDN, only the BuyBox is dynamic. Or astro:actions managing the cart server-side with an httpOnly cookie. Islands query it independently, mutations invalidate the cache.
The variant selector, the price, the stock, they all must live in the same island because they’re tightly coupled. One Svelte component for the entire BuyBox, one client:load. The content around it (description, specs, breadcrumbs) stays static.
Once the BuyBox is migrated, the legacy frontend has no reason to exist. Time to clean up.
Phase 5 · Cleanup
The last legacy component is migrated. Legacy JS bundle, CSS, patch stylesheets, proxy rules.. Everything that was only there for the cohabitation can go. Same approach as the migration: remove one thing, deploy, check. Not a big bang cleanup either.
One framework, one CSS system, each page ships only the JavaScript its islands need. The product page now loads a fraction of time it used to.
Conclusion
The product page with 2MB of JavaScript started with a simple observation: most of what’s on screen doesn’t need a runtime. The architecture exists to fix that. The migration path is concrete, phase by phase, from reverse proxy to cleanup. The tradeoffs are real but they’re manageable.
If you’re staring at a legacy frontend that ships a full framework to render a catalog, the question isn’t whether islands architecture makes sense (it does), it’s whether you and your users can afford to wait.