You need to migrate a legacy frontend codebase to a modern framework—what’s your plan
Avoid a big-bang rewrite. Assess and set goals, then migrate incrementally with the strangler-fig pattern — run old and new side by side, migrate route-by-route or feature-by-feature, keep shipping product, add tests as a safety net, and measure progress.
The headline answer: incremental migration via the strangler-fig pattern — not a big-bang rewrite. Rewrites are high-risk, freeze product delivery, and usually fail or overrun badly.
1. Assess before touching anything
- Why migrate? Performance, hiring, maintainability, security, ecosystem? The goal shapes the plan and justifies the cost.
- Map the codebase — routes, features, shared components, dependencies, dead code, the riskiest/most-coupled areas.
- Pick the target deliberately (React/Next, Vue, Svelte…) — team skills, ecosystem, requirements.
- Get buy-in — leadership needs to know it's incremental and product keeps shipping.
2. Strangler-fig: new grows around the old
Run the legacy app and the new app side by side, and migrate piece by piece until the legacy is "strangled" out.
- Routing seam — a proxy/router sends some routes to the legacy app, others to the new one. Migrate route by route (or feature by feature).
- Or embed — mount new components inside the old app (or vice versa) at boundaries; web components or micro-frontends can bridge frameworks.
- Shared shell — common header/nav/auth so the seams aren't visible to users.
- Start with a low-risk, self-contained area to validate the approach and tooling, then move to higher-value areas.
3. Keep shipping product
- Migration runs alongside feature work, not instead of it — pure-migration freezes don't survive contact with the business.
- New features get built in the new stack; touched legacy areas get migrated opportunistically.
- Avoid a long-lived branch — integrate continuously.
4. Safety nets
- Add tests around legacy behavior before migrating it — characterization/E2E tests on critical flows so you can prove parity.
- TypeScript, linting, CI gates on the new code.
- Feature-flag the new implementation; roll out gradually; keep a rollback path.
- Monitor errors and performance per migrated slice.
5. Watch the tradeoffs
- Two stacks at once — bigger bundle, duplicated some logic, more complexity during the transition. Accept it as temporary; minimize the window.
- Shared state/auth across the boundary needs a deliberate bridge.
- Don't let "incremental" become "forever" — track progress (% migrated) and keep momentum.
6. Finish
- Migrate the last pieces, delete the legacy code and its dependencies, remove the routing seam and any bridges.
The framing
"I'd resist a rewrite. Assess and set a clear goal, then strangler-fig: run new alongside old, migrate route-by-route behind feature flags with tests proving parity, keep shipping product throughout, and measure progress until the legacy is fully replaced and deletable."
Follow-up questions
- •Why is a big-bang rewrite usually a bad idea?
- •How does the strangler-fig pattern work in practice for a frontend?
- •How do you keep shipping features during a migration?
- •How do you handle shared auth/state across the old/new boundary?
Common mistakes
- •Proposing a big-bang rewrite with a feature freeze.
- •A long-lived migration branch that diverges from main.
- •Migrating without tests, so you can't prove behavior parity.
- •Starting with the hardest, most-coupled area.
- •Letting the migration stall half-done, leaving two stacks forever.
Performance considerations
- •Running two frameworks temporarily inflates bundle size and complexity — keep the transition window short and code-split the seam. Per-slice monitoring catches regressions as each piece moves.
Edge cases
- •Shared global state/auth spanning old and new code.
- •SEO/routing must stay stable through the transition.
- •A legacy area nobody understands anymore.
- •Third-party integrations tightly coupled to the old framework.
Real-world examples
- •Route-by-route migration behind a proxy: legacy serves /old/*, new app serves migrated routes.
- •Embedding new React components into a legacy app via a mount point, expanding outward.