The frontend framework decision record: why the UI library you chose determines your bundle size ceiling and your migration cost when requirements change
React versus Vue versus Svelte versus HTMX is decided in the first sprint session where an engineer asks "what frontend framework should we use?" — and never documented as a deliberate architecture choice with SSR requirements, state management coupling, or component ecosystem lifecycle evaluated. The server-side rendering model determines whether a client-side-rendering-only framework selection requires a full routing and data-fetching rewrite when SEO becomes necessary years later. State management coupling determines how expensive major framework version upgrades become when the state library's API is tied to the framework's instance API. Bundle size ceiling is set by the bundler the framework ships with and the framework's code splitting model, neither of which are apparent when the application has eight components in the founding sprint. Each structural property is determined by configuration choices made in the founding session that are invisible in the final code.
A 17-person B2B SaaS company built their analytics dashboard using Create React App in early 2022. The founding engineer had used CRA on a previous project, the AI session recommended it as the standard starting point, and the team shipped their first three features in three weeks. Two years later, the marketing team requested SEO optimization for the product's landing pages and blog, and the growth team identified that server-rendered thank-you pages after paid ad conversions would improve Google Ads Quality Scores by reducing page load time below the 2.5-second Largest Contentful Paint threshold. The engineering team opened a React SSR implementation session with their AI assistant expecting to add a configuration option.
The session revealed that Create React App does not support server-side rendering. CRA is a client-side-rendering-only framework: its build output is a static directory containing an index.html shell and a JavaScript bundle; the browser downloads the bundle and renders all content after JavaScript execution. There is no server that generates HTML before the browser receives the response. The marketing team's SEO requirement and the growth team's LCP requirement both depended on server-rendered HTML that the server returns on the first byte, before any JavaScript executes. CRA could not produce this output regardless of configuration.
The team investigated workarounds. React-snap, a headless-browser pre-renderer, crawled the application and generated static HTML snapshots of each route for SEO. It could handle the product's public pages, but could not handle authenticated dashboard routes, routes that required user-specific data, or the dynamic blog pages that required fetching post content from the CMS API. The pre-rendered snapshots for authenticated pages showed the loading state — the empty shell that CRA renders before data arrives — which provided no SEO value and broke the page structure for search engine crawlers that did receive JavaScript execution. The workaround satisfied none of the original requirements.
The migration decision: Next.js App Router. The migration scope emerged over four sprint planning sessions as the team inventoried the work. The routing layer used react-router-dom v5 with JSX route declarations in a centralized routes file; every route had to be converted to Next.js's file-based routing system, where the file path under the app/ directory determines the URL. Forty-three of the application's 97 components were data-fetching components that used useEffect to call the API after mounting in the browser; these components had to be converted to React Server Components with async/await data fetching in the component body, or explicitly marked as client components with the 'use client' directive and the existing useEffect pattern retained. The layout components — the persistent navigation shell, the sidebar, the breadcrumb trail — had to be rewritten using Next.js's layout.tsx nesting model, because CRA's React Router DOM layout pattern (a wrapper route component that renders its children) does not map directly to App Router's layout hierarchy. The deployment model changed from a static CDN to a Node.js server, requiring infrastructure changes in the CI pipeline and a new hosting configuration for the application server.
The migration took seven sprints — fourteen weeks. During that period, the marketing team's SEO work was blocked and the growth team's conversion page was served with the pre-rendered snapshot workaround. The original CRA selection was made in a single 30-minute session in the product's first sprint; the question was "what should I use to build a React app?"; CRA was recommended as the standard starting point. The session did not address whether SSR would be required, because no one knew in the first sprint that the product would need SEO two years later. The CRA selection was never written down as a decision — it was a tool choice, not an architecture decision. The SSR gap was not documented. The migration cost was not anticipated.
A 23-person HR-tech SaaS company built their product using Vue 2 in 2019. The founding frontend engineer had three years of Vue 2 experience at their previous company and selected it without evaluating alternatives. The product grew to 220 components, used Vuex for state management, Vue CLI for builds, and Vuetify 2 for their component library. The team shipped features steadily through 2020, 2021, and 2022. In 2021, the Vue team announced that Vue 2 would reach end-of-life on December 31, 2023 — security patches would no longer be released after that date.
The team acknowledged the end-of-life date internally but deprioritized the migration until a compliance audit in early 2024 flagged the EOL framework as a supply chain risk. The auditing firm's report classified a production system running on an unmaintained framework with known CVEs as a medium-severity finding requiring remediation within 90 days. The remediation deadline — April 1, 2024 — set the migration timeline. The team scoped the migration and found five interdependent tracks.
The build tooling had to move from Vue CLI to Vite. Vue CLI uses webpack; Vite uses ES modules. The migration required updating every file that used process.env.VUE_APP_* environment variables to import.meta.env.VITE_*, rewriting the webpack configuration for CSS modules and SVG imports into Vite plugins, and reconfiguring the test suite which used Vue CLI's Jest setup with incompatible transformer configurations for Vite's ESM output. The state management had to migrate from Vuex 3 to Pinia. Every component that used Vuex's mapState, mapMutations, and mapActions helpers — 67 components — had to be rewritten to use Pinia's useStore composable. The store module structure changed: Vuex's namespaced module pattern (five store modules with explicit namespace configuration) was converted to five Pinia stores, each defined as a composable function with no namespace configuration required. Vuetify 2 had to migrate to Vuetify 3. The grid system classes changed from shorthand class names on v-col to responsive props. The form validation pattern changed from Vuetify's internal rules array to Vee-Validate 4 integration. The theme configuration changed from a theme object in the Vue.use() registration call to a createVuetify() configuration with a separate theme definition. Every form component — 41 components — required updates to the validation pattern. Every grid layout — 34 page components — required updates to the column props. The Options API components that used Vue 2's removed APIs had to be updated: 18 components used the $on event bus pattern (removed in Vue 3), 12 components used template filters (removed in Vue 3), 9 components used the Vue 2 v-model with value/input (changed to modelValue/update:modelValue in Vue 3). The class-based components — 14 components written using the vue-class-component plugin — had to be rewritten entirely, because vue-class-component was not updated for Vue 3.
The migration required eleven months. During that period, the team shipped zero user-visible features in the frontend codebase. The backend team shipped independently. The team missed the compliance deadline by five months — the auditing firm granted an extension after a progress briefing in April 2024. The original Vue 2 selection was a one-sentence mention in an early team Slack message: "I'll use Vue — I know it from my last job." It was never written down as a decision. The end-of-life risk was not evaluated because end-of-life dates are not a consideration in a founding sprint. The migration cost — eleven months of frontend engineering effort — was not anticipated. A frontend framework ADR written at founding would not have prevented the Vue 2 EOL, but it would have documented that the selection was made without evaluating EOL timeline, that no alternatives with longer support commitments (React's long support history, Svelte's compile-time model that reduces framework runtime dependencies) were considered, and that a migration plan should be drafted when the EOL date is announced. None of that documentation existed.
The four structural properties the framework decision determines
When a team selects a frontend framework in the founding sprint, they are making a decision with four structural properties that constrain what the product can do and what migrations will cost as the product scales. Each property becomes a constraint only after the product needs something the original choice doesn't provide.
Server-side rendering model and SEO ceiling. The client-side rendering versus server-side rendering distinction is not a configuration option — it is determined by the framework and meta-framework selected at founding time. A framework that generates an HTML shell with a JavaScript bundle (Create React App, vanilla Vite React without a meta-framework, Vue CLI without Nuxt) produces all page content in the browser after JavaScript executes. A framework that generates HTML on the server before the first byte is sent to the browser (Next.js, Nuxt, SvelteKit, Remix, Astro) allows search engine crawlers, social media preview renderers, and AI crawler bots to receive populated HTML without executing JavaScript. The SSR model also determines Largest Contentful Paint performance: an SSR page's LCP is measured against the server-rendered HTML content on the first byte response; a CSR page's LCP is measured against the client-rendered content after the data fetch completes in the browser, adding the round-trip time for the data fetch to the LCP measurement. For products where SEO or paid advertising quality scores depend on page load performance, the CSR-to-SSR migration is not additive — it requires rewriting the routing layer, the data fetching layer, and the deployment model simultaneously. The meta-framework selection must accompany the base library selection in the ADR: choosing React without choosing Next.js, Remix, or Astro leaves the SSR question open, and "we'll add SSR later" consistently becomes a full rewrite when the requirement materializes. The new CTO onboarding problem manifests acutely in the frontend framework layer: the new CTO who inherits a CRA application and needs to explain to the board why the marketing team's SEO initiative requires a fourteen-week engineering project needs the original framework selection document to explain the SSR gap — which does not exist.
State management coupling and major version migration cost. Each frontend framework has an associated state management ecosystem, and the coupling between the state management library and the framework version creates migration dependencies that are not visible at founding time. Vue 2's canonical state management was Vuex 3. Vuex 3 is not compatible with Vue 3 — the plugin registration API changed, and the options-API integration layer that Vuex 3 uses to inject store access into Vue 2 components was removed in Vue 3. Vue 3's canonical state management is Pinia, which uses the Composition API and has a fundamentally different store definition syntax. A product that has built its entire state layer on Vuex 3 must migrate the state management simultaneously with the framework version upgrade — the two migrations are inseparable. React's state management ecosystem has fewer hard coupling points but still creates migration costs at major version boundaries: Redux's connect higher-order component pattern (the standard React-Redux pattern through v6) was superseded by hooks in React-Redux v7, requiring connected components to be refactored from connect(mapStateToProps, mapDispatchToProps)(Component) to useSelector and useDispatch hooks inside the component function. The coupling is not a hard break — React-Redux v7 supports both patterns — but codebases that mix the two patterns become hard to maintain, and new engineers joining a team that uses the old pattern face a productivity gap because all recent documentation, tutorials, and examples use the hooks API. The state management coupling should be documented in the framework ADR alongside the framework choice, because the state management library is effectively a secondary architectural commitment that inherits the framework's version lifecycle. A team that documents "we chose Vuex 3 because it is the standard for Vue 2; any future Vue version migration will require evaluating the Pinia migration simultaneously" has a record that prevents the state management migration scope from being a surprise.
Bundle size ceiling and Core Web Vitals floor. The JavaScript delivered to the browser is bounded below by the framework runtime and above by the application code. React's runtime (ReactDOM plus React core) is approximately 44 KB gzipped. Vue 3's runtime is approximately 33 KB gzipped. Svelte compiles components to vanilla JavaScript with no framework runtime — its runtime overhead is approximately 1.5 KB for the Svelte reactivity bindings. The runtime size matters because it is the fixed cost of every page load regardless of application code size. For a new product with ten components and minimal third-party libraries, the framework runtime dominates the bundle and the choice between React and Svelte is most visible in bundle size. At scale — 200 components, a charting library, a data table, a date picker, a rich text editor — the framework runtime is a small fraction of the total bundle and the bundler's tree-shaking capability and the framework's code splitting model become the dominant factors. Next.js provides automatic route-based code splitting: the JavaScript delivered to the browser for any given page contains only the components in that page's component tree, plus the React runtime and the Next.js router. Vue applications using Vite benefit from route-based lazy loading through dynamic imports, but unlike Next.js, the splitting is not automatic — each route must be explicitly configured as a lazy import. The performance optimization decision record documents the per-route bundle size targets and the Core Web Vitals measurement methodology; the frontend framework ADR documents the framework's code splitting model and the bundler configuration that implements the targets, because the code splitting model is a property of the framework selection that the performance record depends on without documenting. A product that selects a CSR-only framework and then discovers in the performance record that its LCP score is failing cannot fix LCP by optimizing bundle size — LCP for data-fetching pages is fundamentally limited by the time required for JavaScript to execute and the data fetch to complete in the browser, which is not reducible below the browser's data fetch round-trip time regardless of bundle size.
Component ecosystem and third-party dependency lifecycle. Selecting a framework also means selecting a component ecosystem, and that ecosystem has its own major version lifecycle that is tied to but not synchronized with the framework's lifecycle. A team that builds an admin dashboard using Vuetify 2 on Vue 2 encounters a Vuetify 3 migration when they upgrade to Vue 3 — Vuetify 3 rewrites the grid system, form validation, and theme configuration in ways that are breaking for every component that uses them. The Vuetify 3 migration is a separate migration track from the Vue 3 migration, with its own breaking changes, its own documentation, and its own completion timeline. A team that builds a data-heavy application using Material-UI v4 on React encounters a Material-UI v5 (MUI) migration that renames the package from @material-ui/* to @mui/*, migrates the styling system from JSS to Emotion, and introduces a new sx prop API — all of which require per-component updates across the application. Neither of these migrations is caused by the framework — they are caused by the component library maintaining its own version lifecycle in parallel with the framework's lifecycle. The component library selection is not documented in the framework ADR when the framework ADR does not exist. When the ADR does exist, the component library selection and its upgrade policy — "we will upgrade Vuetify within 6 months of each major Vue version release" or "we accept the risk of a major Vuetify upgrade coinciding with a major Vue upgrade" — is a second-order commitment that the ADR documents explicitly. The build-vs-buy decision record covers the general framework for evaluating third-party components versus custom implementations; the frontend ADR's component library section applies that framework specifically to UI component decisions where the "buy" option (adopting a component library) carries the implicit cost of synchronizing with the library's version lifecycle across the framework's version boundary.
Framework options and their structural properties
React 18 (Meta, MIT license). React is the most widely adopted frontend library, with the largest hiring pool, the deepest component ecosystem, and the most active third-party library support of any JavaScript frontend option. React's structural properties: the virtual DOM reconciliation model (diffing the component tree to determine DOM updates) adds per-update overhead that Svelte's compiled model eliminates; React 18's concurrent features (Suspense, transitions, server components) reduce this overhead for specific patterns but require App Router migration from Pages Router to access. React requires an explicit state management choice — React does not provide a built-in global state solution, and the correct choice among Redux Toolkit, Zustand, Jotai, TanStack Query, and React Context depends on the product's data complexity and team familiarity. React is CSR-only without a meta-framework; adding SSR requires selecting Next.js, Remix, or Astro and migrating to that framework's routing and data fetching model. The 44 KB runtime cost is present on every page regardless of application code. React's large ecosystem means that most third-party services provide React SDKs, but it also means that third-party SDK breaking changes affect a React codebase frequently — authentication libraries, analytics SDKs, payment widgets, and feature flag SDKs all ship React-specific packages with their own version lifecycles.
Next.js 14+ (Vercel). Next.js is the canonical React meta-framework, providing SSR, SSG, ISR, and React Server Components in a file-based routing system. Next.js's structural properties: the App Router (available since Next.js 13, stable in 14+) introduces React Server Components — components that execute on the server and produce HTML without any client-side JavaScript, reducing the browser bundle to only interactive components marked with 'use client'. App Router's layout.tsx nesting model allows persistent UI elements (navigation, sidebars) to be rendered once per navigation session rather than re-rendered on every route change. Server actions allow form submissions and mutations to execute server-side functions directly, eliminating explicit API route creation for form handling. The Vercel deployment model provides edge function support, streaming HTML via React Suspense, and integrated image optimization; self-hosting Next.js requires a Node.js server and manual integration of the features that Vercel provides automatically. The App Router migration from Pages Router is itself a significant refactoring effort — the two routing models are not compatible, and Next.js provides a coexistence strategy (both routers can exist in the same project during migration) but not an automated conversion. The CI/CD pipeline decision record intersects with Next.js deployment: the pipeline must account for the Node.js server requirement and the difference in deployment behavior between Pages Router static export and App Router server-side rendering, because a Pages Router application can be deployed to a static CDN while an App Router application requires a server-side runtime.
Vue 3 (Evan You, MIT license). Vue 3 introduced the Composition API as the canonical pattern for component logic, replacing the Vue 2 Options API pattern (though Options API remains supported for compatibility). Vue 3's structural properties: Pinia is the officially recommended state management, providing a composable store definition that integrates natively with Vue 3's reactivity system and TypeScript; Vuex 4 is Vue 3-compatible but not recommended for new projects. Vite is the official build tool, providing native ES module hot module replacement during development and Rollup-based tree-shaken production builds. Vue 3's runtime is approximately 33 KB gzipped. Nuxt 3 provides SSR/SSG/ISR on Vue 3 with a file-based routing model analogous to Next.js. The hiring pool for Vue is smaller than React but larger than Svelte — Vue has strong adoption in the Asia-Pacific region and in agencies building content sites. The component ecosystem (Vuetify 3, PrimeVue, Element Plus, Naive UI) is active but smaller than React's. Vue 3's migration guide from Vue 2 documents the breaking changes; the migration from class-based components written with vue-class-component is not documented in Vue's migration guide because vue-class-component is a third-party plugin, and its absence from Vue 3 is not mentioned in Vue's own documentation — teams discover this gap when they reach class-based components in their migration inventory.
Svelte 5 / SvelteKit. Svelte is a compile-time framework — the Svelte compiler converts component source code into vanilla JavaScript that performs direct DOM manipulations without a virtual DOM reconciliation step. The compiled output does not include a framework runtime in the traditional sense; the 1.5 KB runtime provides minimal initialization logic. Svelte 5 introduced runes, a new reactivity model using signal-like primitives ($state, $derived, $effect) that replaces the implicit reactive variable assignment model of Svelte 4. The runes model is a breaking change from Svelte 4's reactivity syntax; projects migrating from Svelte 4 to Svelte 5 must convert reactive declarations and stores to the runes API. SvelteKit provides SSR, SSG, and edge deployment with a file-based routing model similar to Next.js. Svelte's structural advantages: no external state management library is required — Svelte's built-in stores and Svelte 5's runes handle global state patterns that React requires external libraries for; bundle size is typically 50–70% smaller than equivalent React applications because there is no virtual DOM diff overhead in the compiled output; the learning curve is shorter for engineers unfamiliar with virtual DOM concepts. Svelte's structural limitations: the ecosystem is substantially smaller than React's — many third-party services do not provide Svelte-specific components, requiring React or vanilla JavaScript alternatives; the hiring pool is smaller; the Svelte 4 to Svelte 5 breaking change demonstrates that Svelte's smaller team is capable of introducing major API changes with significant migration costs comparable to the Vue 2 to Vue 3 transition. The test strategy decision record intersects with Svelte: Svelte components require Svelte-specific testing utilities (Svelte Testing Library), and engineers familiar with React Testing Library will find the API analogous but not identical; the component testing investment must be rebuilt when migrating to Svelte from another framework.
HTMX. HTMX is not a frontend framework — it is a library (approximately 14 KB unminified) that extends HTML with attributes enabling partial page replacement, server-pushed updates, and hypermedia-driven interactions without JavaScript application code. HTMX applications serve HTML from the server and use HTMX attributes (hx-get, hx-post, hx-swap) to specify which server endpoint to call and which DOM element to replace with the response. The browser never manages application state — state lives on the server, and each HTMX request returns updated HTML for the relevant portion of the page. HTMX eliminates the JavaScript framework runtime, the state management library, the component build system, and the client-side routing layer — the entire frontend is server-rendered HTML with HTMX attributes for interactivity. HTMX is appropriate for products whose interactions are primarily CRUD operations on server data: forms that submit data, lists that filter or paginate, detail pages that update a record. HTMX is not appropriate for products requiring client-side state that is not synchronized with the server on every interaction: offline capability, complex multi-step wizard forms that accumulate user input before submission, real-time collaborative editing, or canvas-based interactions. The decisions that were never written down pattern applies to HTMX: teams that choose HTMX for a product with CRUD-first requirements often fail to document the decision and the rationale, leading a later engineer to "add React" to a specific interactive feature without recognizing that adding a React build pipeline changes the project's build tooling, deployment model, and maintenance complexity for the parts that remain HTMX.
Astro. Astro is a content-site-first framework that uses an island architecture: static content is rendered to HTML at build time with zero JavaScript; interactive components from any framework (React, Vue, Svelte, Solid) are mounted as islands with JavaScript limited to each island's component tree. Astro delivers zero JavaScript for static content by default, making it the correct choice for marketing sites, documentation, and blogs where most content is static and interactivity is limited to isolated components (search, newsletter signup, interactive demos). Astro is not appropriate as the primary framework for an application with complex client-side state, real-time data, or user sessions that span multiple pages with shared state updates — the island model does not provide a mechanism for state to be shared across islands without a server round-trip or a client-side store that re-introduces JavaScript complexity. The monorepo decision record intersects with Astro in products that separate their marketing site from their application: a monorepo that contains both an Astro marketing site and a Next.js application can share design tokens, component primitives, and TypeScript types, but the Astro and Next.js build pipelines are independent and the Turborepo task graph must be configured to build them correctly.
AI chat session types and what each one misses
The frontend framework decision follows a consistent pattern in AI chat history. The founding session establishes the framework based on familiarity or the session's default recommendation. A performance investigation session discovers the CSR/SSR gap when SEO or LCP fails. A major version upgrade session discovers the state management coupling when the upgrade guide's scope expands to include the state library. A dependency audit session discovers the component library version lifecycle when a CVE or EOL is flagged. Each session addresses the immediate symptom without surfacing the architectural decision that caused it. The WhyChose extractor surfaces these sessions because they contain decisions that belong in a frontend framework ADR and are consistently left in conversational form.
The "what frontend framework should I use?" session. This is the founding session. The engineer asks for a recommendation; the AI provides a framework that matches the engineer's experience level and the project's stated requirements. The session covers the framework's component model, the basic project setup command, and the first few components. What the session misses: the SSR requirement, which is not in scope for the founding sprint and is not mentioned because the product has no public pages yet; the state management choice, which is deferred to "when we need it"; the component library selection, which is "whatever looks good for the UI we want to build"; the team's long-term hiring plan, which affects which frameworks maintain a sufficient hiring pool; the framework's major version history, which indicates whether EOL events are likely and when. The framework selection from this session becomes the technical substrate for the entire frontend codebase. Changing frameworks after the codebase reaches 100+ components requires a migration that replaces routing, state management, component library, and build tooling simultaneously. An ADR written after the founding session documents the framework and meta-framework choice, the SSR decision (required, not required, or deferred with explicit criteria for when it would become required), the state management choice and its coupling to the framework version, and the component library selection with its upgrade policy.
The "my React app needs SEO / my LCP is failing" session. This session is triggered by a growth team request for SEO or by a Core Web Vitals report showing failing LCP scores. An engineer queries: "how do I add server-side rendering to my React app?" or "why is my LCP so high and what can I do about it?" The session recommends react-snap for static pre-rendering or Vite's SSR mode for server rendering, or diagnoses that the LCP failing is caused by client-side data fetching that cannot be eliminated without SSR. What the session misses: the scope of the migration required to add SSR to a CSR-only application is not surfaced in the session. The engineer receives recommendations for workarounds (pre-rendering, deferred hydration, skeleton screens to improve perceived LCP) without being told that the correct fix — SSR — requires migrating to a different meta-framework. The workarounds are documented in the session; the migration cost and scope are not. The performance optimization decision record addresses the measurement methodology; the frontend framework ADR's SSR section documents the decision made at founding and the explicit criteria under which an SSR migration would be required (for example: "if organic search traffic becomes a acquisition channel and LCP on landing pages falls below 2.5 seconds, re-evaluate SSR adoption with a migration cost estimate").
The "how do I upgrade to Vue 3 / React 18 / the new major version?" session. This session is triggered by an EOL announcement, a CVE, or a new feature that requires the major version. An engineer opens the migration guide and begins querying the AI about specific breaking changes. What the session misses: the state management migration is documented in the state management library's migration guide, not the framework's migration guide, and the session that opens the framework upgrade guide does not automatically surface the state management coupling. The engineer begins the framework upgrade, discovers that the state management library's Vue 2-compatible version does not work with Vue 3, and opens a separate session for the state management migration — a session that may recommend Pinia but does not document the ordering dependency between the build tooling migration, the state management migration, and the component library migration. The migration scope is discovered incrementally through a series of sessions, each addressing a specific track, without a session that documents the full interdependence of the migration tracks. An ADR written at founding that documents the state management coupling and the component library lifecycle would provide the migration dependency map that prevents the incremental discovery pattern.
The "why is our bundle so large / why are our builds slow?" session. This session is triggered by a performance review or a developer experience complaint about build times. An engineer queries: "our production bundle is 2.3 MB — how do we reduce it?" or "our Webpack build takes 4 minutes — how do we speed it up?" The session recommends bundle analysis tools (webpack-bundle-analyzer, Vite's rollup-plugin-visualizer), code splitting via dynamic imports, tree-shaking of specific libraries, and image optimization. What the session misses: the bundle size ceiling set by the framework's runtime and bundler model is not surfaced in the context of a bundle optimization session. An engineer optimizing bundle size on a CRA project receives advice about code splitting and tree-shaking without being told that CRA's webpack configuration, which is hidden behind react-scripts and cannot be modified without ejecting, limits the tree-shaking capability for certain module patterns. An engineer asking why builds are slow on a Vue CLI project receives advice about Vue CLI configuration without being told that Vue CLI is unmaintained for Vue 3 and that migrating to Vite would reduce build times by 10–30× for most projects. The CI/CD pipeline decision record includes build time targets; the frontend framework ADR's bundler section documents the framework's bundler model and the build time implications, so that a slow build is diagnosed against the documented bundler model rather than treated as a configuration problem to be solved with session-by-session optimization.
Five ADR sections for frontend framework architecture
A frontend framework ADR that prevents the CRA-to-Next.js migration surprise, the Vue 2 EOL discovery, the state management migration scope, and the bundle size dead ends covers five sections that teams consistently omit from the founding session.
First, framework and meta-framework selection with explicit SSR requirements. The ADR documents the base library choice (React, Vue 3, Svelte) with rationale, and the meta-framework choice (Next.js, Nuxt, SvelteKit, Remix, or none) with explicit documentation of the SSR decision. The SSR section must specify: whether SSR is required at founding, the criteria under which SSR would become required (for example, "if organic search traffic is targeted as an acquisition channel, SSR must be adopted before SEO work begins"), and the estimated migration cost if SSR is added retroactively to a CSR-only foundation. Documenting the migration cost estimate forces the team to confront the SSR decision explicitly at founding rather than deferring it to the migration project. The meta-framework section also documents the hosting model: a CSR-only application can be served from a CDN; an SSR application requires a server-side runtime; the hosting model selection (Vercel, AWS Lambda, EC2, self-hosted Node.js) has cost and operational implications that are part of the framework decision. The infrastructure-as-code strategy decision record documents the hosting infrastructure; the frontend ADR references the infrastructure record for the specific hosting configuration selected for the framework's deployment model. The framework ADR also specifies the mobile app target: a product targeting mobile app users alongside web users has different framework constraints — React Native shares component patterns with React web but not the framework runtime; Vue 3's Ionic integration provides a mobile app path; Svelte's NativeScript integration is less mature. The mobile app target must be in the framework ADR because selecting a framework optimized for web without documenting the mobile app constraint creates a second framework decision later, often resulting in a separate codebase for mobile rather than a shared component architecture.
Second, state management architecture with framework version coupling documented. The ADR documents the state management library choice (Redux Toolkit, Zustand, Jotai, TanStack Query, Pinia, Svelte stores, or React Context for simpler products) with the rationale for that choice over alternatives. The coupling section documents explicitly: whether the state management library is tied to the framework's major version (Vuex 3 is Vue 2-only; Pinia is Vue 3-only; React-Redux supports multiple React major versions), and whether a major framework version upgrade would require a simultaneous state management migration. If coupling exists, the ADR documents the migration plan: the estimated scope of converting the state layer when the framework's next major version requires it, and the approach for managing the transition (supporting two state management patterns during migration, or a flag-day cutover that requires all components to be migrated simultaneously). TanStack Query (formerly React Query) is worth calling out as a state management option that reduces coupling: its role is server state synchronization (caching and refetching remote data), which does not depend on framework-specific reactivity systems — TanStack Query v5 supports React, Vue, Svelte, and Solid, making it a lower-coupling choice for the server state layer. The pattern of undocumented state management choices is as common as undocumented framework choices — the state management library is often selected in a single session when the first cross-component state sharing requirement appears, and the selection is treated as an implementation detail rather than an architecture decision. Documenting the state management choice in the framework ADR ensures it is evaluated alongside the framework selection rather than independently.
Third, bundle size targets with per-route measurement and Core Web Vitals CI gates. The ADR documents the initial bundle size targets per route (total JavaScript, CSS, and image payload on first load), the bundler configuration that implements the targets (automatic route-based code splitting for Next.js; explicit lazy imports for Vue Router; SvelteKit's default per-route splitting), and the CI gate that enforces the targets (a bundle size check that fails the build if any route's JavaScript payload exceeds the documented target). Bundle size targets must be set per route rather than for the entire application, because a large admin dashboard can have a large per-page bundle for complex pages while the marketing landing page must be under 100 KB to hit the LCP targets required for paid advertising quality scores. The CI gate must run on every pull request — bundle size regressions introduced in feature branches are impossible to reverse without revert commits once they reach the main branch. The Core Web Vitals section documents the LCP, FID (First Input Delay, replaced by INP in 2024), and CLS (Cumulative Layout Shift) targets for the product's key pages, and the measurement methodology: synthetic measurement in CI using Lighthouse CLI for consistent comparison, real user monitoring using a small Analytics integration (Google Analytics 4 Core Web Vitals reporting or a custom timing event) for actual user device and network conditions. The observability strategy decision record covers backend telemetry; the frontend ADR's metrics section documents the frontend-specific performance instrumentation and the alert thresholds for Core Web Vitals regressions, since a backend performance record that monitors server-side latency does not capture the client-side rendering performance that determines the user experience and the Google Search ranking signals.
Fourth, component library selection with major version upgrade policy. The ADR documents the component library choice (MUI, Ant Design, Radix UI, shadcn/ui, Headless UI, Vuetify, PrimeVue, Svelte Material UI, or a custom design system), the rationale for adoption versus building custom (adoption advantages: existing accessibility implementation, comprehensive component coverage, community maintenance; build advantages: no external dependency lifecycle, complete control over API surface), and the upgrade policy for major component library versions. The upgrade policy must specify: the timeline for adopting major component library releases (within 6 months of release, within 12 months, or at the next major framework upgrade), the approach for evaluating breaking changes in major releases (automated codemods where available, manual migration with per-component testing), and the criteria for abandoning a component library in favor of an alternative (library becomes unmaintained, library drops framework compatibility, library's API diverges too far from the design system requirements). The adoption versus build decision should document the accessibility implementation explicitly: component libraries like MUI and Radix UI have invested significant effort in WCAG 2.1 AA compliance, keyboard navigation, and screen reader support; building custom components requires equivalent investment, and the build-vs-buy decision record must account for accessibility implementation cost when comparing third-party component libraries against custom alternatives. The ADR also documents the design token strategy: whether the component library's theming system is the source of truth for design tokens (colors, typography, spacing), or whether a separate design token package (such as a CSS custom property file generated from Figma) is the source of truth and the component library is configured to consume those tokens. The design token source of truth determines the migration complexity when the component library changes: a product that uses a component library's theming system as the design token source of truth must migrate the design tokens when migrating the component library.
Fifth, framework major version migration criteria with migration cost estimation methodology. The ADR documents the explicit criteria under which a major framework version migration is required (EOL announcement, critical CVE with no patch available on the current version, a required feature available only in the major version), the migration cost estimation methodology (component count × average component migration time, plus build tooling migration time, plus state management migration time, plus component library migration time, plus testing infrastructure update time), and the minimum notice period between the migration decision and the migration start (time required to plan, resource, and schedule the migration without disrupting the product roadmap). The EOL section must document the framework's end-of-life policy: React has no announced EOL dates and Meta maintains backward compatibility; Vue 2's EOL was announced 18 months before the EOL date; Svelte's runes introduction in Svelte 5 was announced during development and provided a compatibility period for Svelte 4 syntax. For frameworks with announced EOL policies, the ADR documents the EOL date and the migration trigger: "if Vue 3 announces an EOL date less than 24 months away and no LTS release is available, initiate a framework migration evaluation." The migration cost section must include the component library migration as a separate line item, because the component library migration scope is not derivable from the framework migration guide and is consistently underestimated when migration planning begins. The monorepo decision record intersects with the migration cost methodology: a monorepo that contains multiple applications using the same framework benefits from a shared migration effort for the build tooling and state management layers, but the per-application component migration must be estimated independently for each application in the monorepo. The migration criteria ADR section prevents the compliance audit emergency that drove the eleven-month migration in the second narrative — a product that has documented EOL criteria and a migration estimation methodology can begin the migration proactively when the EOL announcement occurs rather than reactively when an auditor flags the unmaintained framework as a supply chain risk.
None of these five sections appear in the "what frontend framework should I use?" AI session that established the framework and started the first components. The founding session covers the minimum viable setup — a working application with the first few routes and components. It does not address the SSR model and whether client-side rendering will be sufficient as the product's acquisition strategy evolves. It does not address state management coupling and the migration dependency it creates for major framework version upgrades. It does not address bundle size targets and the Core Web Vitals gate that enforces them before regressions accumulate. It does not address the component library lifecycle and the upgrade obligation it creates at each framework major version. It does not address the migration cost estimation methodology that converts a future framework EOL from an emergency into a planned project. These are not advanced optimization concerns — they are the operational requirements of a frontend codebase that continues to work correctly as the product needs SEO, the framework releases major versions, the component library evolves, and the bundle size grows with the product's feature set. The WhyChose extractor surfaces the founding session, the performance investigation session, the major version upgrade session, and the bundle optimization session from AI chat history; the frontend framework ADR takes the library choices buried in those sessions and converts them into a documented SSR decision, state management coupling acknowledgment, bundle size target, component lifecycle policy, and migration criteria — written before the fourteen-week CRA migration and the eleven-month Vue 2 migration that make those requirements visible at the worst possible moment.
FAQs
Why does adding SSR to a Create React App project require a full framework migration rather than a configuration change?
Create React App is a client-side-rendering-only framework. It generates an index.html shell with a script tag that loads the JavaScript bundle; the browser downloads the bundle, executes it, and renders all content after JavaScript execution. There is no server that generates HTML before the browser receives the response. Adding SSR requires a server that imports the React component tree and calls ReactDOMServer.renderToPipeableStream on each request — CRA does not provide this server and its build output (a static directory of HTML, CSS, and JavaScript files) is not structured for server-side rendering.
Migrating from CRA to Next.js requires three structural changes. First, the routing layer changes from react-router-dom's JSX route declarations to Next.js's file-based routing system, where every route must be converted to a page file in the app directory. Second, the data fetching layer changes from useEffect hooks (which run after mount in the browser) to React Server Components with async/await data fetching that runs on the server during rendering, or client components with the useEffect pattern retained — components that use browser APIs or hooks must be explicitly marked as client components. Third, the deployment model changes from a static CDN to a Node.js server that runs continuously to handle SSR requests. The migration is not incremental — the routing, data fetching, and deployment changes are interdependent and cannot be applied one at a time while the application remains functional.
What are the interdependent migration tracks when upgrading from Vue 2 to Vue 3, and why must they be completed in a specific order?
A Vue 2 to Vue 3 migration has five interdependent tracks. First, the build tooling must migrate from Vue CLI (webpack) to Vite (ES modules), because Vue CLI is unmaintained for Vue 3. This track affects environment variable handling, CSS module configuration, and test setup, and must be complete before other tracks can be tested. Second, state management must migrate from Vuex 3 to Pinia; Vuex 3 is not compatible with Vue 3's plugin registration API, and Pinia uses a fundamentally different store definition syntax. Third, the component library must migrate — Vuetify 2 to Vuetify 3 rewrites the grid system, form validation, and theme configuration, requiring per-component updates across every form and layout component. Fourth, Options API components using Vue 2's removed APIs (the $on event bus, template filters, Vue 2's v-model binding) must be updated. Fifth, class-based components written with vue-class-component must be rewritten entirely, because the plugin was not updated for Vue 3.
The ordering dependency is: build tooling first (other tracks cannot be tested until Vite works), state management second (components cannot be migrated until their store is available in the new API), component library third (library API changes affect every component that uses it), Options API fixes fourth (isolated to specific components after the library is updated), class components last (self-contained and can be migrated component by component). A team that begins class component migration before state management migration will write components that use the new store API against a Vuex version that is incompatible with Vue 3, creating a second migration pass on those components.
How does the choice between React, Vue, and Svelte affect JavaScript bundle size, and at what scale does the difference become significant for Core Web Vitals?
React's runtime (ReactDOM plus React core) is approximately 44 KB gzipped. Vue 3's runtime is approximately 33 KB gzipped. Svelte's runtime is approximately 1.5 KB gzipped — Svelte compiles components to vanilla JavaScript with direct DOM manipulations and no virtual DOM reconciliation step. The runtime difference is most visible at small application scale, where the framework runtime is the dominant bundle cost. At large application scale (200+ components, substantial third-party libraries), the framework runtime is a small fraction of the total bundle and the bundler's tree-shaking capability and code splitting model become the dominant factors.
For Core Web Vitals, the more significant distinction is the rendering model rather than the runtime size. An SSR application (Next.js, Nuxt, SvelteKit) delivers populated HTML on the first byte, allowing Largest Contentful Paint to be measured against the server-rendered content before JavaScript executes. A CSR application (React without Next.js, Vue without Nuxt) delivers an empty HTML shell; LCP is measured after JavaScript executes and the data fetch completes in the browser, which adds the data fetch round-trip time to the LCP measurement regardless of bundle size. For products where LCP on data-fetching pages is a search ranking or advertising quality signal, the choice of CSR versus SSR has more impact on Core Web Vitals than the choice between React and Vue's runtime sizes.