I Built the Same App 10 Times: Evaluating Frameworks for Mobile Performance

Started evaluating 3 frameworks for work, ended up building 10. Next-gen frameworks (Marko, SolidStart, SvelteKit, Qwik) all deliver instant 35-39ms performance. The real differentiator? Bundle sizes range from 28.8 kB to 176.3 kB compressed. Choose based on your priorities, not microscopic FCP differences.


I Built the Same App 10 Times: Evaluating Frameworks for Mobile Performance

Why I Built This

My team needed to choose a framework for an upcoming app. The requirements were clear: it had to work well on mobile. Not “acceptable on mobile,” but actually good. We’re building tools for real estate agents working in the field: open houses, parking lots, spotty cellular signals. When someone’s standing in front of a potential buyer trying to look professional, a slow-loading app isn’t just an annoyance. It’s a liability.

I started with what seemed like a reasonable comparison: Next.js (our current default when a framework is required) versus SolidStart and SvelteKit (alternatives I’d heard good things about). Three frameworks, should be straightforward. But when I built the first implementations and started measuring, something became clear: the issues I was seeing with Next.js weren’t specific to Next.js. They were fundamental to React’s architecture. I wondered whether the other dominant frameworks (Angular, Vue) might have similar mobile web performance limitations.

That question changed the scope. If I was going to make a real recommendation for the team, I needed to test all the major meta-frameworks and understand the full landscape of alternatives. Three frameworks became ten. What started as a practical evaluation for work turned into something bigger: a semi-comprehensive look at what’s actually possible for mobile web performance in 2025.

This post shares what I discovered. The measurements are real, the kanban apps are identical (same features, same database, same styling), and the differences are dramatic. Marko delivers 12.6 kB raw (6.8 kB compressed). Next.js ships 497.8 kB raw (154.5 kB compressed). That’s a 39x difference in raw size that translates to real seconds on cellular networks.

If you’re interested in the theoretical implications of why framework diversity matters, I wrote about that in React Won by Default. This post focuses on the data: what I built, what I measured, and what it means for teams making similar decisions.


Key Takeaways (TL;DR)

Next-Gen Frameworks Deliver Instant Performance: Marko (39ms), SolidStart (35ms), SvelteKit (38ms), and Nuxt (38ms) all achieve essentially instant First Contentful Paint in the 35-39ms range. This is 12 to 13 times faster than Next.js at 467ms. The 4ms spread between fastest and slowest is statistically measurable but perceptually meaningless to users. All next-gen frameworks feel instant. The real performance story isn’t splitting hairs over 3ms differences, it’s the massive gap between next-gen and React/Angular.

Bundle Size Champion: Marko delivers 88.8 kB raw (28.8 kB compressed) for the board page, 6.36 times smaller than Next.js’s 564.9 kB raw (176.3 kB compressed). This is 44% smaller than the next closest competitor (SolidStart at 41.5 kB compressed), making Marko the clear choice when bundle size is the absolute top priority.

Resumability Pattern: Qwik City at 114.8 kB raw (58.4 kB compressed) eliminates traditional hydration via resumability, yielding instant interactivity for larger client-side apps. Different architectural approach that solves different problems.

Nuxt Proves Established Frameworks Can Compete: At 224.9 kB raw (72.3 kB compressed) with 38ms FCP, Nuxt demonstrates that established “big three” frameworks can achieve next-gen performance when properly configured. Vue’s architecture allows competitive mobile web performance while maintaining a mature ecosystem. React and Angular show no path to similar results.

Critical scaling difference: MPA frameworks (Marko, HTMX) ship minimal JavaScript per page, staying lean as you add features. SPA frameworks ship routing and framework runtime upfront, with higher baselines even using code splitting. Marko delivers around 12.6 to 88.8 kB raw regardless of total routes. SPAs maintain 85.9 to 666.5 kB raw baselines plus route chunks.

The key finding? The dominant frameworks show dramatically different results. React has an unavoidable performance ceiling. TanStack Start achieves 373.6 kB raw (118.2 kB compressed) bundles using React 19, only 1.51 times better than Next.js’s 564.9 kB raw (176.3 kB compressed). Angular ships similarly heavy bundles via Analog at 666.5 kB raw (203.4 kB compressed). But Vue (via Nuxt) proved different, achieving competitive 224.9 kB raw (72.3 kB compressed) bundles with instant 38ms FCP that matches next-gen frameworks. Meanwhile, next-gen frameworks like SolidStart deliver 128.6 kB raw (41.5 kB compressed) bundles with equally instant 35ms FCP, 4.39 times smaller than Next.js and 2.91 times smaller than TanStack Start with React. The perfect controlled comparison: TanStack Start with React (373.6 kB raw) versus TanStack Start with Solid (182.6 kB raw). Same meta-framework, same patterns, but React bundles are 2x the size of Solid, isolating React’s runtime cost.

Mobile is the web. These measurements matter because mobile web is the primary internet for billions of people. If your app is accessible via URL, people will use it on phones with cellular connections. Optimizing for desktop and hoping mobile is good enough is backwards. The web is mobile. Build for that reality.

Each build uses the same database, features, and UI so the comparison stays fair. The differences in raw bundle size for the board page range from 4.39 to 6.36 times smaller compared to Next.js for modern alternatives. Important: These measurements represent disciplined baseline implementations with minimal dependencies. Real production apps typically ship 5 to 10 times more JavaScript from analytics, authentication, feature flags, and third party libraries, meaning the framework differences compound significantly in practice. On mobile devices with cellular connections, this matters enormously.

Before diving in, a reminder from my Progressive Complexity Manifesto: The frameworks compared here represent Level 5 complexity. They are powerful tools for when you need unified client-side state, a lot of reactivity, and/or client-side navigation. But most applications thrive at lower levels. For instance, Level 3 (server-rendered HTML enhanced with HTMX and vanilla JavaScript, as demonstrated in the kanban-htmx app in this repo) can handle complex interactive applications with minimal JavaScript. Level 4 adds occasional Web Components using Lit for reusable elements. These simpler approaches often deliver even smaller bundles and much simpler codebases. This post focuses on Level 5 options for cases that demand them, while remembering simpler paths often suffice.

Why Mobile Web Performance Matters

For this evaluation, mobile performance wasn’t just a nice to have. It was the primary constraint. Our users are real estate agents working in the field: open houses with 30 people hammering the same cell tower, parking lots between showings, anywhere but a desk with WiFi. They need tools that work instantly, not “eventually load.”

We’re not building a native app. We’re building for the web, which means if it has a URL, people will access it on their phones. And for our users, the app could be used on a phone just as frequently as a desktop.

This reality shaped the evaluation. I couldn’t just pick a framework that “works on mobile.” I needed something that genuinely performs well on cellular connections with mid-tier devices. The difference between a framework shipping 30 kB versus 170 kB isn’t academic. It’s the difference between an app that feels professional and one that makes our users look bad in front of clients.

The business cost of slow performance: Research from Tammy Everts at SpeedCurve reveals something surprising. While site downtime causes 9% permanent user abandonment, slow performance causes 28% permanent abandonment. That’s over 3x worse. Even more revealing: slowdowns occur 10x more frequently than outages, resulting in roughly 2x total revenue impact despite lower per-hour costs. Beyond the abandonment numbers, slow performance creates a psychological effect where users start perceiving your entire brand negatively. Content seems “boring,” design looks “tacky,” even when those elements haven’t changed. Slowness poisons everything. These aren’t abstract metrics. They’re measurable business costs that compound with every framework kilobyte you ship to mobile users.

The real-world cost: A 113 kB difference at 3G speeds (750 kbps) means 1.2 seconds for download plus 500ms to 1s for parse/execution on mobile CPUs. Total: 1.5 to 2 seconds slower between frameworks. On 4G the gap shrinks but remains noticeable. On spotty connections (like an open house with 30 people hammering the same cell tower) it becomes painful.

“But it’s cached!” This objection misses reality. Cache busting is standard. Every deployment means users download again. First impressions matter. So do second, third, and tenth impressions. Your users remember waiting.

This is why I expanded the evaluation beyond the initial three frameworks. I needed to understand what’s actually possible. When someone pulls up the app in a parking lot between showings, every second counts. Building for mobile performance first means desktop on WiFi is excellent by default. The reverse isn’t true. Optimize for desktop and mobile users suffer.

I discovered the difference between frameworks reflects fundamentally different engineering priorities. Some frameworks prioritize runtime flexibility, shipping extensive abstractions to support wide use cases. Others prioritize runtime size and mobile performance from the ground up. The bundle sizes I measured for the board page varied by up to 7x (from 28.8 kB compressed to 203.4 kB compressed), differences that matter enormously on cellular networks.

The Experiment Setup

I built a Kanban board application ten times, once in each of these frameworks: Next.js 16 (React 19 with built-in compiler) representing React’s Virtual DOM approach with automatic optimization, TanStack Start (also React 19) for a leaner React meta-framework without App Router overhead, TanStack Start + Solid (SolidJS 1.9) using the same meta-framework with fine-grained reactivity, Nuxt 4 (Vue 3) for Vue’s reactive refs with SSR-first developer experience, Analog (Angular 20) using Angular’s modern signals API with meta-framework tooling, Marko (@marko/run) for streaming SSR with fine-grained reactivity, SolidStart (SolidJS 1.9) for native Solid integration with fine-grained reactivity through signals, SvelteKit (Svelte 5) for fine-grained reactivity with runes, Qwik City for resumability instead of hydration, and Astro + HTMX for a traditional MPA approach.

Each implementation includes the exact same features: board creation and listing pages, four fixed lists per board (Todo, In Progress, QA, Done), full CRUD operations for cards, drag-and-drop card reordering within lists and movement between lists, assignee assignment from a static user list, tag management, comments on cards with authorship tracking, completion status toggles, optimistic UI updates for drag-and-drop and chart changes (HTMX lacks this though), and server-side form validation using Valibot.

All ten apps share the same foundation. The database is SQLite with Drizzle ORM using an identical schema across all implementations. Styling comes from Tailwind CSS plus DaisyUI to keep the UI consistent. Each framework implementation contains roughly 17 components. Most importantly, every app performs real database queries against relational data (boards → lists → cards → tags/comments/users) rather than working with hardcoded arrays.

You can check out the code here.

A critical choice about dependencies: These apps intentionally minimize dependencies compared to what many developers typically reach for. For mobile web applications, every dependency represents a choice to ship additional kilobytes to users. I used necessary UI libraries like drag-and-drop packages (which vary by ecosystem), but deliberately avoided data fetching libraries, state management helpers, and other utilities that frameworks already handle natively. Each ecosystem has popular packages that add convenience but increase bundle size (React developers often reach for tanstack-query for data fetching, state management libraries, or form helpers). To illustrate the trade-off: tanstack-query alone weighs approximately 13 kB gzipped. That single dependency is already larger than Marko’s entire homepage bundle at 6.8 kB. By avoiding these “nice to have” dependencies and using each framework’s built-in capabilities instead, the bundle differences you’ll see reflect framework architectural choices, not different amounts of functionality or third-party helpers.

Measurement Methodology: All bundle sizes in this comparison represent median values from 10 measurement runs with browser cache cleared between each run to ensure cold-load performance measurements. Server warmup requests and IQR outlier removal ensure robust statistics. I report both raw (uncompressed) JavaScript sizes and compressed transfer sizes. The raw size reflects actual code volume generated by each framework and is more consistent for comparison since it doesn’t vary by server compression settings. The compressed size shows what users actually download over the network. See the complete measurement methodology for details on statistical approach, test conditions, and limitations.

Here’s how the tech stacks compare:

CategoryNext.jsTanStack StartTanStack Start + SolidNuxtAnalogMarkoSolidStartSvelteKitQwikAstro + HTMX
FrameworkNext.js 16 (App Router)TanStack Start 1.133.8 (w/React)TanStack Start 1.133.8Nuxt 4Analog (Angular)@marko/run 0.8SolidStart 1.1.0SvelteKit + Svelte 5Qwik CityAstro 5 + HTMX
UI LibraryReact 19 + CompilerReact 19 + CompilerSolidJS 1.9Vue 3Angular 20Marko 6SolidJS 1.9Svelte 5QwikHTMX (server-driven)
Reactivity ModelVirtual DOM + CompilerVirtual DOM + CompilerSignals (fine-grained)Reactive refsSignals (zoneless)Signals (fine-grained)Signals (fine-grained)Runes (fine-grained)Signals + ResumabilityServer-driven (HTMX)
Data FetchingServer ComponentsTanStack Router loadersTanStack Router loadersuseAsyncData / useFetchinjectLoad + DIRoute data handlerscreateAsync with cacheRemote functions (query)routeLoader$Route handlers
MutationsServer ActionsServer functions (RPC)Server functions (RPC)API routes (server/api/*)ApiService + RxJSPOST handlersServer functionsRemote functions (form/command)Server actionsAPI routes + HTMX
DatabaseDrizzle ORM + better-sqlite3Drizzle ORM + better-sqlite3Drizzle ORM + better-sqlite3Drizzle ORM + better-sqlite3Drizzle ORM + better-sqlite3Drizzle ORM + better-sqlite3Drizzle ORM + better-sqlite3Drizzle ORM + better-sqlite3Drizzle ORM + better-sqlite3Drizzle ORM + better-sqlite3
StylingDaisyUIDaisyUIDaisyUIDaisyUIDaisyUIDaisyUIDaisyUIDaisyUIDaisyUIDaisyUI
Drag & Drop@dnd-kit/core, @dnd-kit/sortable@dnd-kit/core, @dnd-kit/sortable@thisbeyond/solid-dnd@formkit/drag-and-drop@angular/cdk@formkit/drag-and-drop@thisbeyond/solid-dndNative HTML5Native HTML5@formkit/drag-and-drop
Build ToolTurbopackViteViteViteVite + AngularViteVinxiViteVite + Qwik optimizerVite

This isn’t a todo list with hardcoded arrays. It’s a real app with database persistence, complex state management, and the kind of interactions you’d actually build for a real product.

Framework Architectures at a Glance

To avoid repetition throughout this post, here are the key architectural approaches for each framework tested:

React-based (Next.js, TanStack Start + React): Use Virtual DOM reconciliation where components re-render and React diffs changes before updating the DOM. React’s compiler automatically optimizes components through memoization using a Control Flow Graph-based High-Level Intermediate Representation, reducing manual optimization needs but not bundle size. Next.js employs React Server Components (RSC) which serialize component trees into a special RSC Payload format, adding meta-framework overhead. TanStack Start uses traditional SSR without RSC complexity. Both ship React’s runtime including Virtual DOM reconciler, synthetic event system, and platform abstractions, creating unavoidable baseline costs for mobile users.

Solid-based (SolidStart, TanStack Start + Solid): Fine-grained reactivity via signals with read/write segregation where getters are separate from setters. JSX syntax similar to React, but signals automatically track dependencies, eliminating manual dependency arrays and rules of hooks. Components run once during initial render; subsequent updates happen directly at the reactive primitive level without re-executing component functions, minimizing CPU overhead on mobile devices. TanStack Start provides more feature-rich routing which causes slightly larger bundles compared to SolidStart’s leaner integration.

SvelteKit: Compile-time optimization that transforms components into imperative DOM updates, with minimal runtime overhead since the compiler does most work at build time. Runes ($state, $derived, $effect) powered by signals enable fine-grained reactivity, with universal reactive primitives that work in .js/.ts files beyond just .svelte components. The compiler converts developer-written code into lean, optimized production code. This approach generates JavaScript with smaller bundles through aggressive tree-shaking, helping mobile performance on both network transfer and parse time.

Nuxt (Vue): Reactive refs with .value access for reactivity tracking. Uses aggressive optimization including compile cache for faster cold starts and reactive keys for intelligent data fetching. In Vue 3 the reactivity system has been refactored for improved performance and memory efficiency, critical for mobile devices. Vapor Mode (experimental, not used here) offers a compile-first approach that bypasses Virtual DOM entirely, compiling templates directly to native DOM operations with significantly smaller runtime overhead. Despite being a “big three” framework, Nuxt achieves competitive bundle sizes and exceptional runtime performance, with support for mixed component trees combining different rendering strategies.

Analog (Angular): Modern signals API provides fine-grained reactivity through primitives like signal, effect, linkedSignal, queries, and inputs. Zoneless mode enables removing zone.js from bundles entirely, eliminating its synchronization overhead which improves mobile CPU efficiency. Uses dependency injection patterns and ships with RxJS for enterprise reactive patterns, creating heavier bundles despite signals-based reactivity. Angular remains a “batteries-included” framework where common functionality is built-in rather than requiring third-party libraries. Incremental hydration reduces time-to-interactive by hydrating components progressively rather than all at once.

Marko: Streaming SSR with fine-grained reactivity powered by compiler analysis. The compiler analyzes the reactivity graph at build time and compiles away components themselves, shipping only the minimal code needed for events and effects, achieving zero component overhead at runtime. Statically analyzes which components are stateful versus server-only, breaking pages into top-level stateful components and selectively serializing only needed data. HTML-first syntax with automatic dependency tracking eliminates boilerplate. Supports streaming asynchronous SSR with selective hydration where only interactive parts ship JavaScript to the client, critical for minimizing mobile bundle size.

Qwik City: Resumability architecture that serializes application state and component boundaries directly into HTML during server rendering, allowing the client to “resume” execution without traditional hydration that requires re-executing components to attach event listeners. Employs fine-grained lazy loading down to the component level, deferring JavaScript downloads until actual user interaction occurs. Event handlers, component logic, and complex interactions are delivered lazily on-demand, eliminating bulk initial JavaScript execution that burdens mobile CPUs. Optimized for edge platforms with distributed deployment, delivering sub-second load times on mobile networks. Best suited for complex client-heavy applications requiring instant interactivity.

Astro + HTMX: Multi-page architecture (MPA) where Astro serves as a simple HTML renderer with no client-side JavaScript framework. HTMX handles all interactivity through declarative HTML attributes that trigger server requests and swap HTML fragments into the page. Instead of client-side state management, interactions send requests to the server which returns HTML snippets that HTMX injects into the DOM. This approach ships minimal JavaScript (just the HTMX library) and keeps pages lean as routes increase. Best suited for applications where server round trips are acceptable and client-side reactivity isn’t critical. Trades rich client-side state management for extreme simplicity and tiny bundles, optimal for form-driven or content-heavy applications.

TanStack Start: Meta-framework with a client-first architecture philosophy (versus server-first approaches like Next.js RSC), maintaining powerful server capabilities while prioritizing client-side routing and state management. Router-centric design where the majority of framework functionality comes from TanStack Router, which is framework-agnostic and supports React and Solid. Provides isomorphic loaders that work on both server and client, streaming SSR for progressive rendering, and server functions for type-safe RPC. React version ships traditional hydration with React’s baseline costs, while Solid version achieves roughly half the bundle size using identical routing infrastructure, demonstrating how UI library choice impacts mobile performance.

Fairness Check: Pinned versions, identical data volume on Board page, normalized CSS/icon handling (treeshake/purge). All measurements use Chrome Lighthouse with mobile emulation (Pixel 5, 4G throttling, 1x CPU). The measurement script uses 1x CPU to isolate bundle size impact from CPU performance variance. Cache is cleared between each measurement run to simulate first-visit experience.

Why I Expanded from Three to Ten Frameworks

When I started this evaluation, I expected to compare Next.js, SolidStart, and SvelteKit, then make a recommendation. But building those first three implementations revealed something I hadn’t anticipated: the performance issues I saw in Next.js weren’t just a React problem. They were likely systemic across the “big three” dominant framework ecosystems (React, Angular, Vue).

React (via Next.js) ships 154.5 to 176.3 kB compressed (486.1 to 564.9 kB raw) with poor runtime performance at 467ms FCP. Angular (via Analog) ships 125.4 to 203.4 kB compressed (430.3 to 666.5 kB raw). Both suffer from heavy baseline bundles that create performance costs for mobile users. Vue (via Nuxt) tells a dramatically different story. Nuxt ships competitive bundle sizes at 72.3 kB compressed (224.9 kB raw) AND achieves exceptional 38ms FCP, making it faster than all React and Angular options and competitive with next-gen frameworks like SvelteKit (38ms, tied) and Marko (39ms). This puts Nuxt in a unique position: it’s the only “big three” meta-framework that competes on mobile web performance. React requires architectural changes to achieve similar results. Angular has no clear path forward. Nuxt proved that with proper optimization, even established frameworks can deliver next-gen performance.

React’s explicit strategy: React Native for mobile. In practice, React’s web runtime trades bundle size for other goals. Many teams pursuing top-tier mobile performance choose React Native. The architectural choices that make React heavy on the web are deliberate. They solve real problems for desktop and app development. But for mobile web, React’s position is essentially: use React Native instead.

This is a strategic business decision, not a technical oversight. Facebook (Meta) doesn’t build heavy React web apps on mobile. They invest heavily in React Native and native apps. When you use their mobile app, you’re not running a web browser rendering a React SPA. You’re running native code. React Native is their solution for mobile performance. The React web framework can be expensive because the assumption is that if you care about mobile, you should use a different tool.

The problem with this strategy is that it abandons the open web. React Native requires building two separate applications. Your company needs React engineers for the web, different engineers or different skill sets for native mobile development, and App Store difficulties on top.

This isn’t just an inconvenience. It’s technofeudalism. React Native solves the mobile performance problem, but it does so by pushing developers out of the open web and into app store platforms where Apple and Google extract up to 30% of transactions, control distribution, and can revoke access at will. React’s mobile strategy inherently drives teams toward platform capture. The web offers an alternative: no gatekeepers, no platform fees, direct distribution. (I explore this dynamic in depth in “The Web is the Last Commons” section below, building on economist Yanis Varoufakis’s analysis of how app stores operate as digital fiefdoms rather than competitive markets.)

Other frameworks make a different bet: the web should work well on mobile without requiring a parallel native technology. The teams behind Marko, Solid, Svelte, Qwik, and Vue have done phenomenal engineering work rethinking these fundamentals from first principles. They’ve built innovative solutions that optimize for the web as a first-class platform for mobile. They’re all saying: you shouldn’t need a completely different technology stack just to reach people with phones. The web should be competitive on its own.

React’s choice is coherent within their ecosystem strategy. It makes sense given their investment in React Native. But it’s not neutral. It’s a choice that deprioritizes mobile web performance in favor of extensive runtime abstractions. For teams building mobile first web applications, it’s a choice that works against you.

That’s why I expanded the evaluation to ten frameworks. If I was going to make an honest recommendation for the team, I needed to understand what’s actually possible. React’s heavy bundle sizes aren’t bugs or poor engineering. They’re the predictable cost of React’s runtime architectural overhead. Angular has similar bundle size issues. Vue showed that the “big three” can compete on mobile web performance when properly configured. For teams building mobile first web applications without the resources for React Native, React and Angular create unavoidable performance limitations, but Nuxt offers a viable path forward.

The measurements that follow show exactly what that tradeoff looks like in practice. They also show what happens when frameworks prioritize mobile web performance from the start. Marko at 6.8 kB compressed. Solid at 30.6 kB compressed. Svelte at 47.8 kB compressed. These aren’t just smaller numbers. They’re fundamentally different architectural approaches that treat the web as a first class platform for mobile.

Bundle Size Reality Check

The Numbers (Versions used)

Production builds measured showing raw JavaScript size (with compressed/gzipped transfer size in parentheses). Raw size reflects actual code volume and is more consistent for comparison. Compressed size shows what users download over the network.

Framework versions tested: Next.js 16.0.0-beta.0 (React 19.2.0), TanStack Start 1.133.8 (React 19.2.0), Nuxt 4.1.2 (Vue 3.5.22), Analog (Angular core 20.3.3), Marko 6.0.85 with @marko/run 0.8.1, SolidStart (@solidjs/start 1.2.0, solid-js 1.9.9), SvelteKit 2.43.6 (Svelte 5), Qwik City 1.16.1 (Qwik 1.16.1), Astro 5.14.5 + HTMX.

These are minimal baseline implementations. Typical production apps include authentication, analytics, feature flags, form libraries, and other dependencies that multiply these numbers significantly. The framework overhead shown here compounds with every additional dependency.

Table ordered by board page size (smallest first):

FrameworkBoard Page Raw (Compressed)Homepage Raw (Compressed)Difference from Next.js (Board Page)
Marko88.8 kB (28.8 kB)12.6 kB (6.8 kB)6.36x smaller
Qwik City114.8 kB (58.4 kB)88.5 kB (43.6 kB)4.92x smaller
SvelteKit125.2 kB (54.1 kB)103.4 kB (47.8 kB)4.51x smaller
Astro + HTMX127.3 kB (34.3 kB)88.9 kB (22.0 kB)4.44x smaller
SolidStart128.6 kB (41.5 kB)85.9 kB (30.6 kB)4.39x smaller
TanStack Start + Solid182.6 kB (60.4 kB)153.0 kB (52.0 kB)3.09x smaller
Nuxt224.9 kB (72.3 kB)224.9 kB (72.3 kB)2.51x smaller
TanStack Start373.6 kB (118.2 kB)316.8 kB (100.7 kB)1.51x smaller
Next.js 16564.9 kB (176.3 kB)497.8 kB (154.5 kB)Baseline
Analog666.5 kB (203.4 kB)430.3 kB (125.4 kB)1.18x larger

Bundle Sizes

Field data validation: The Chrome User Experience Report (CrUX) provides real-world Core Web Vitals data from millions of actual websites using these frameworks on mobile devices. This field data complements the controlled measurements in this post. Important caveat: CrUX data reflects how these frameworks are used in production by average developers, not optimal implementations. If a framework shows poorly in CrUX but well in these tests, it demonstrates what’s possible with proper configuration, performance tuning, and dependency discipline. The gap between field data and optimized implementations reveals opportunity for improvement in real-world usage patterns.

The difference between Marko’s 88.8 kB raw (28.8 kB compressed) and Next.js’s 564.9 kB raw (176.3 kB compressed) translates to roughly 1.5 seconds on cellular. These seconds are the baseline. Time waiting to load increases with every feature and every dependency added. Those aren’t just abstract kilobytes. That’s their time, their patience, and ultimately their impression of your product.

Critical scaling consideration: These bundle sizes represent a mid-complexity app with multiple routes. MPA frameworks like Marko ship minimal JavaScript per page (6.8 to 28.8 kB compressed per route), staying lean as you add features. SPA frameworks ship routing and framework runtime upfront. Even with code splitting, SPAs maintain higher baselines: Solid/Svelte start at 30.6 to 54.1 kB compressed then add route chunks, while React/Vue/Angular start at 72.3 to 203.4 kB compressed. The architectural model creates different scaling characteristics.

Important context on HTMX: The Astro + HTMX implementation achieves excellent bundle sizes with the simplest codebase, but sacrifices client-side reactivity for server-driven interactions. HTMX excels for simpler, form-driven applications where most interactions trigger server requests. However, as your app’s need for rich client-side state management grows, HTMX becomes less practical. For reactive applications, Marko (6.8 to 28.8 kB compressed), Solid (30.6 to 41.5 kB compressed), and Svelte (47.8 to 54.1 kB compressed) maintain small bundles while delivering rich reactivity.

React’s Ceiling in Practice (TanStack vs Next)

TanStack Start achieves 100.7 to 118.2 kB compressed bundles (316.8 to 373.6 kB raw) while Next.js ships 154.5 to 176.3 kB compressed (497.8 to 564.9 kB raw) in this measurement. Both use React 19. That’s only a 33 to 35% improvement, primarily reflecting App Router + RSC and related runtime.

The answer reveals that React’s runtime architecture is the primary cost, not just Next.js’s meta-framework choices.

What’s the difference? Next.js ships the full React Server Components runtime plus serialization layers, component boundary management, caching infrastructure, App Router with all its routing features, progressive enhancement for Server Actions, image optimization, and middleware. TanStack Start strips most of that out: traditional SSR without RSC, leaner routing, and simple RPC-style server functions.

Both use server-side rendering, but Next.js’s RSC model adds substantial overhead. Server Components render on the server only, Client Components get marked with "use client", the server serializes everything to a special format, and the client needs runtime code to deserialize and coordinate those boundaries. TanStack Start uses the simpler traditional SSR approach: render on server, ship HTML, hydrate everything on the client. No serialization, no boundary coordination.

In this measurement, Next.js’s App Router + RSC adds roughly 53 to 58 kB compressed. The remaining 100.7 to 118.2 kB compressed (316.8 to 373.6 kB raw) is React’s core runtime cost: reconciliation, event system, and hydration baseline.

Compare that to alternatives. SolidStart delivers 30.6 to 41.5 kB compressed (85.9 to 128.6 kB raw) using JSX, 2.91x smaller than TanStack Start with React. SvelteKit achieves 47.8 to 54.1 kB compressed (103.4 to 125.2 kB raw), which is 1.97x to 2.47x smaller than TanStack Start. Qwik delivers 43.6 to 58.4 kB compressed (88.5 to 114.8 kB raw), which is 1.72x to 2.31x smaller.

For React teams, the path forward isn’t straightforward. TanStack Start proves you can remove Next.js’s overhead, but you’re still carrying React’s 100.7 to 118.2 kB compressed (316.8 to 373.6 kB raw) baseline. SolidStart offers similar JSX syntax with 2.91x smaller bundles. And if you like TanStack Start’s approach, you can use it with Solid for the same routing patterns with dramatically smaller bundles.

Here’s the bottom line: React’s architecture (not just the Virtual DOM, but also synthetic events, platform patching, and sheer feature complexity) creates unavoidable performance costs that no meta-framework optimization can eliminate. To be fair, Virtual DOM implementations can be small (see Preact at 4 kB). React’s size reflects deliberate choices to circumvent platform constraints and provide extensive features. TanStack Start proves this: removing App Router overhead yields only a 33 to 35% improvement. To escape this ceiling and achieve 3 to 4 times smaller bundles, you need a fundamentally different architectural approach. Frameworks that lean into the platform instead of circumventing it can deliver dramatic size reductions. The React team chose to accept these costs to solve other problems (Server Components, unified patterns). That’s a legitimate choice. But it’s not negotiable within React.

TanStack Start: React vs Solid

Here’s where it gets interesting. TanStack Start is a new meta-framework that currently supports both React and Solid. Using the same meta-framework with two different UI libraries gives us the perfect controlled comparison.

TanStack Start with React: Ships 373.6 kB raw (118.2 kB compressed) compared to Next.js’s 564.9 kB raw (176.3 kB compressed). That’s 34% smaller by raw size. If you’re stuck maintaining an existing Next.js codebase, TanStack Start offers a legitimate escape path from App Router complexity while staying in React. But that’s still 373.6 kB raw (118.2 kB compressed) of React’s core runtime.

TanStack Start with Solid: Delivers 182.6 kB raw (60.4 kB compressed). That’s 30% larger than SolidStart’s 128.6 kB raw (41.5 kB compressed), but still dramatically better than any React option. The size difference is largely due to TanStack Router having more features than SolidStart’s Router. This buys you additional routing capabilities and framework flexibility.

The controlled comparison that matters: React at 373.6 kB raw (118.2 kB compressed) versus Solid at 182.6 kB raw (60.4 kB compressed) using identical TanStack Start infrastructure. Same routing, same SSR approach, same patterns. React bundles are 2x the size of Solid. This isolates React’s runtime cost versus Solid’s architecture. No meta-framework differences, no excuses.

All four implementations achieve perfect 100 Lighthouse scores. Bundle size differences are real, but modern devices handle them without impacting perceived performance in this test.

For greenfield projects? Don’t choose React. TanStack Start with Solid gives you 182.6 kB raw (60.4 kB compressed) bundles, but native SolidStart delivers 128.6 kB raw (41.5 kB compressed) with tighter integration. If you want the absolute smallest with this architecture, go SolidStart. If you like TanStack Start’s patterns and might want framework flexibility later, TanStack Start with Solid is reasonable. But starting a new project with React (whether Next.js or TanStack Start) means voluntarily accepting 2x to 3x larger bundles for no performance gain.

The Verdict: What I’m Recommending

After building ten implementations (with help, of course; see the acknowledgements below) and measuring everything, the data gives clear direction. For our mobile first requirements, here’s what I found:

The next-gen frameworks all achieve essentially instant performance. The 35-39ms FCP range feels perceptually identical to users, and it’s 12 to 13 times faster than Next.js at 467ms. Since all next-gen frameworks feel equally fast, choose based on bundle size priorities and developer experience rather than microscopic FCP differences.

That said, context matters. Not every project can or should switch frameworks.

When Next.js still makes sense: For large existing React codebases, migration costs may outweigh performance benefits. If you’re stuck with React and can’t migrate, consider TanStack Start over Next.js for a 21-31% bundle reduction without App Router complexity. That’s a practical business decision. But for greenfield projects? There’s no legacy to maintain, no migration costs to weigh. You’re choosing to build on a foundation that costs your users 2x to 3x more JavaScript on every visit. You’re voluntarily accepting worse performance when better options cost nothing extra. That’s not a neutral technical choice. “We only know React” isn’t a technical constraint, it’s a learning investment decision. And “organizational politics” is real, but it’s not a technical justification. It’s an admission that better options exist but can’t be chosen.

Reality check on common objections:

“But hiring!” Competent developers learn frameworks. That’s the job. These alternatives are actually easier to learn than React: no rules of hooks, no dependency arrays, no manual memoization dance. The real difficulty isn’t learning curve, it’s creating a engineering culture that acknowledges constraints and makes intentional decisions with these constraints in mind.

“But ecosystem!” React’s ecosystem is both advantage and liability. Large libraries ship code for scenarios you’ll never encounter. That date picker with every locale? You need 3 features, you’re shipping 300. For mobile-first projects where every kilobyte matters, this becomes a problem. Modern AI tools make building exactly what you need feasible: generate the function instead of importing 50kB for 3 features. Smaller bundles, code you understand.

“But it’s risky!” Shipping 3x larger bundles to mobile users on cellular is the actual risk. Slow loads damage your brand and cost conversions. The “safe choice” has measurable costs.

“But my users are desktop-only!” Let’s be honest: “desktop-only” is usually an excuse to skip performance discipline entirely. And it’s rarely true for long. Six months later someone asks “can I check this on my phone?” and suddenly you’re stuck. Better to build it right from the start. Desktop users still benefit from faster parsing and execution. Even on WiFi, 30.6 kB compressed loads noticeably faster than 176.3 kB compressed. More importantly, why would you voluntarily accept 3x worse performance when the better option costs nothing extra? Performance is a feature regardless of screen size. Building with constraints makes you a better engineer. “Desktop-only” shouldn’t mean “no discipline.”

Why you should seriously consider the alternatives: The mental models are often simpler (see Framework Architectures section). Alternatives like Solid, Svelte, and Marko streamline patterns with automatic reactivity. Performance comes by default with 2x to 6x smaller bundles requiring no optimization work. Mobile web matters with real users on phones, cellular connections, and mid-tier devices. You’ll write less code, ship less JavaScript, and debug fewer framework quirks. Most importantly, greenfield projects deserve choices made on merit rather than defaults.

These alternatives are especially compelling for mobile-first applications where bundle size directly impacts user experience. They matter for the growing demographic of people who prefer phones over computers. Mobile professionals like real estate agents, field service workers, healthcare staff, delivery drivers, and sales reps benefit most. Teams building internal tools or MVPs without enterprise politics constraining decisions can move faster. Developers who value technical excellence over popularity contests will appreciate the engineering quality. Importantly, teams save significant money by maintaining a single high-performance web codebase instead of splitting resources between separate web and native applications. This often means smaller teams, lower overhead, and faster iteration cycles compared to organizations maintaining web apps and native mobile apps.

Choosing among the alternatives (organized by primary use case):

Smallest Bundles: Choose Marko for the absolute best bundle sizes (6.8 to 28.8 kB compressed). Marko delivers 44% smaller bundles than the next closest competitor, making it the clear winner when bundle size is your top priority. The MPA architecture ships minimal JavaScript per page, staying lean as you add routes. The developer experience is excellent once you embrace its streaming model. Note: Marko 6 is currently in beta (tagged as next on npm) and expected to leave beta by end of year, with no expected API changes but ongoing bug fixes and optimizations.

JSX Familiarity: Choose SolidStart if you want the easiest migration path from React. At 128.6 kB raw (41.5 kB compressed), SolidStart uses JSX syntax with automatic dependency tracking that eliminates manual memoization. This delivers 4.39x smaller bundles than Next.js while feeling immediately familiar to React developers. The mental model is actually simpler than React because signals are more straightforward than hooks.

Best All-Around Developer Experience: Choose SvelteKit for approachable syntax and excellent defaults. At 125.2 kB raw (54.1 kB compressed), SvelteKit delivers 4.51x smaller bundles than Next.js with progressive enhancement by default and minimal framework overhead. The compiler-based approach means less runtime code and cleaner component logic. Best for developers from any background seeking readable code with few framework quirks.

Resumability Pattern: Choose Qwik City if you have a larger application that demands immediate interactivity on load with significant client-side functionality. At 88.5 to 114.8 kB raw (43.6 to 58.4 kB compressed), Qwik uses resumability instead of hydration, yielding instant time-to-interactive. Different architectural approach that solves different scaling problems.

Established Ecosystem: Choose Nuxt if you want Vue’s mature plugin ecosystem with competitive mobile web performance. At 224.9 kB raw (72.3 kB compressed), Nuxt proves that established “big three” frameworks can achieve next-gen performance when properly configured. Best for teams already familiar with Vue, projects that benefit from extensive community plugins, or teams that value the safety of a well-established framework. Nuxt bridges the gap between the familiar and the performant.

Important scaling consideration: Marko’s MPA architecture ships minimal JavaScript per page (stays lean as you add routes), while SPAs like SvelteKit and SolidStart ship routing and framework runtime upfront then add route chunks. Both use code splitting, but the architectural models create different performance characteristics at scale.

As discussed in my Progressive Complexity Manifesto, these Level 5 frameworks are only necessary when you need unified client-side state and complex reactivity. Most apps can thrive at lower levels with simpler tools like HTMX and vanilla JS/TS.

When developers have real alternatives, everyone wins. React wouldn’t be adding a compiler if SolidStart and Svelte weren’t proving that automatic optimization matters. The entire ecosystem improves when we stop accepting “good enough” as the ceiling.

My recommendation for the team: After building all these implementations, Marko, SolidStart, and SvelteKit are all excellent choices that would serve our mobile first requirements well. All three feel perceptually instant to users. The real question is priorities: absolute smallest bundles (Marko), easiest React migration (SolidStart), or best all-around developer experience (SvelteKit). If the team has Vue experience, Nuxt is also compelling with its mature ecosystem and competitive performance.

For personal projects outside of work, I’ll be reaching for SvelteKit and increasingly Marko. Their developer experience just feels right, the code flows naturally, and they make building things fun.

Performance Reality: What Lighthouse Hides

Mobile performance scores on the Board page (median from 10 runs), ordered by FCP (fastest first):

FrameworkScoreFCP (ms)LCP (ms)TBT (ms)CLSBundle Size Raw (Compressed)
SolidStart100353500.000128.6 kB (41.5 kB)
Nuxt100383800.000224.9 kB (72.3 kB)
SvelteKit100383800.000125.2 kB (54.1 kB)
Marko100393900.00088.8 kB (28.8 kB)
TanStack Start + Solid100404000.013182.6 kB (60.4 kB)
TanStack Start100434300.000373.6 kB (118.2 kB)
Analog100535300.000666.5 kB (203.4 kB)
Astro + HTMX100545400.001127.3 kB (34.3 kB)
Qwik100585800.000114.8 kB (58.4 kB)
Next.js 1610046746700.000564.9 kB (176.3 kB)

Key Metrics:

FCP (First Contentful Paint) indicates when the first content appears on screen. LCP (Largest Contentful Paint) shows when the main content becomes visible. TBT (Total Blocking Time) measures how long the main thread remains blocked and unavailable for user interactions. CLS (Cumulative Layout Shift) evaluates visual stability, where 0 represents perfect stability with no unexpected layout shifts.

Measurement Conditions: These scores represent mobile device emulation using Pixel 5 profile with 4G network throttling (10 Mbps download, 40ms round-trip time). I use 1x CPU (no throttling) to isolate bundle size and network impact from CPU performance, which allows fair comparison across frameworks with different runtime characteristics. Each measurement was run 10 times with cache cleared between runs to capture cold-load (first-visit) performance. Server warmup requests and IQR outlier removal ensure robust statistics. Standard deviations vary based on framework characteristics.

But here’s what these metrics hide. All frameworks achieve perfect or near perfect scores (100), but the paint times tell the real story about architectural differences. SolidStart achieves the fastest board page load at 35ms FCP. Nuxt and SvelteKit tie for second at 38ms FCP, with Marko close behind at 39ms. TanStack Start with Solid and TanStack Start with React follow at 40ms and 43ms respectively, showing excellent optimization. The top eight frameworks (SolidStart, Nuxt, SvelteKit, Marko, TanStack variants, Analog, Astro, Qwik) all render in under 60ms, achieving near-instant perceived performance. Only Next.js lags dramatically behind at 467ms FCP, representing a 13.3x performance gap compared to SolidStart for identical functionality.

Those paint time differences matter in the real world. SolidStart’s 35ms FCP feels instant, while Next.js’s 467ms FCP is noticeably slower. The framework choice directly impacts whether your app feels like a premium tool or a liability.

Where the Difference Shows

All frameworks feel instant in optimal conditions. The twist is this. Smaller bundles (Marko, Solid, Svelte, Qwik) win dramatically on slow networks and mid-tier devices.

On desktop with WiFi, all these frameworks are fast. On cellular with a mid-tier phone, 3.54 to 39.2x smaller bundles create measurably better UX. The 130 kB you saved doesn’t just download faster. It also parses and executes faster on mobile CPUs.

Hydration vs Resumability:

  • Traditional (React, Solid, Svelte, Vue, Angular): Download bundle → Parse JS → Execute all components → Attach event listeners (hydration)
  • Marko: Download minimal JS → Run effects and attach event listeners directly (fine-grained tree shaking, no re-execution of server code)
  • Qwik: Download minimal JS → Resume from serialized HTML → Lazily load interaction handlers on demand

Both Marko and Qwik are resumable frameworks that avoid traditional hydration. The key difference is that Qwik uses lazy resumability, progressively loading JavaScript based on actual interactions, while Marko analyzes the reactivity graph at build time to bundle exactly the code needed for events and effects; no lazy loading, but maximum precision in what gets shipped. Traditional frameworks must re-execute all components just to wire up event listeners.

Addressing Common Critiques

I know what some of you are thinking. “Aren’t you comparing MPAs to SPAs unfairly?” And “Is this app complex enough?”

App Complexity Defense: This isn’t some toy todo list. It’s a solid mid-complexity app with real database persistence using SQLite plus Drizzle ORM, relational queries across boards to lists to cards to tags and comments, drag-and-drop reordering, (some) optimistic updates, modals, and server validation. It matches the kind of internal tools or MVPs teams crank out every day. The bundle differences come straight from framework overhead, not the features themselves, and those gaps only get bigger at scale with more routes and dependencies. If your production app throws in auth or real-time stuff, framework baselines would just bloat even more, not shrink.

MPA vs SPA Nuances: The routing pattern debate misses the point, reactivity models matter way more. With features like View Transitions API and the Speculation Rules API, MPAs like Marko or HTMX feel just as snappy as SPAs for navigation. The real split is in scaling. MPAs ship minimal JS per page, for example Marko sticks to 6.8 to 28.8 kB, while SPAs lug around upfront runtime from 83.9 to 666.5 kB baselines plus chunks.

A Note on Ecosystems: the “small ecosystem” concern is often overstated. For mobile-first applications, we should be extremely selective about every dependency we add. Each package increases bundle size and maintenance burden. Modern AI tools like Claude, ChatGPT, and Cursor excel at generating focused code for your specific use case. Instead of importing a 50 kB library for 3 features, AI can help you write exactly what you need in 2 kB. This approach yields smaller bundles, code you actually understand, and fewer supply chain risks. Large ecosystems are sometimes advantageous, but they’re also a liability when every import costs your mobile users.

Does Complexity Buy You Anything?

If Next.js’s bundles are only 21 to 31% larger than TanStack Start (both using React), what are you actually getting for that extra 44 to 54 kB? And more importantly, do the dominant frameworks’ baselines (React at 100.7 to 118.2 kB compressed, Vue at 72.3 kB compressed, Angular at 125.4 to 203.4 kB compressed) buy you anything compared to alternatives that deliver 30.6 to 54.1 kB compressed?

The Complexity Tax

Each of the big three has conceptual complexity that alternatives avoid. React has rules around hooks and dependency arrays. Angular carries dependency injection patterns and RxJS complexity. Vue requires understanding refs and reactivity tracking. After seeing that TanStack Start (with React) only achieves marginal improvements over Next.js (21 to 31%), it’s clear that framework complexity and bundle weight often go hand-in-hand. Here’s how state management, effects, and data fetching compare:

1. State Management

// React - useState with functional updates to avoid stale closures
const [count, setCount] = useState(0);
setCount((prev) => prev + 1);

// Solid - getter/setter pattern, explicit read/write
const [count, setCount] = createSignal(0);
setCount(count() + 1);

// Svelte - looks like normal variables
let count = $state(0);
count = count + 1;

// Vue/Nuxt - .value access for reactivity
const count = ref(0);
count.value = count.value + 1;

// Qwik - .value property, serializable
const count = useSignal(0);
count.value = count.value + 1;

// Angular/Analog - getter/setter like Solid
const count = signal(0);
count.set(count() + 1);

// Marko - direct assignment with automatic reactivity
<let/count=0>
<script>
  count = count + 1; // Automatically reactive
</script>

2. Effects with Dependencies

// React - manual dependency array (explicit but error-prone)
useEffect(() => {
  console.log(count);
}, [count]); // You must maintain this manually. Mistakes cause hard-to-debug stale closures and infinite loops. This is not a documentation problem, it's a design problem.

// Solid - automatic tracking
createEffect(() => {
  console.log(count()); // Automatically subscribes to count
});

// Svelte - automatic tracking
$effect(() => {
  console.log(count); // Automatically subscribes to count
});

// Vue/Nuxt - explicit or automatic
watch(
  () => count.value,
  (val) => console.log(val)
); // explicit
watchEffect(() => console.log(count.value)); // automatic

// Qwik - explicit tracking
useTask$(({ track }) => {
  track(() => count.value);
  console.log(count.value);
});

// Angular/Analog - automatic tracking
effect(() => {
  console.log(count()); // Automatically subscribes
});

// Marko - automatic tracking with <script>
<let/count=0>
<script>
  console.log(count); // Automatically subscribes to count
</script>

3. Server Data Fetching

// Next.js - async Server Component (implicit server boundary)
// Looks like regular component code but runs on server. No explicit data contract.
// This works beautifully until it doesn't, and when bugs arise from the server/client
// boundary being invisible, they're extremely hard to debug.
export default async function Page() {
  const board = await db.query.boards.findFirst();
  return <div>{board.name}</div>;
}

// SvelteKit - remote functions with query (experimental)
// .remote.ts file defines server-side query function
export const getBoardData = query(v.string(), async (board_id) => {
  return await db.query.boards.findFirst();
});

// In component: use $derived rune with await
const boardData = $derived(await getBoardData(params.id));

// SolidStart - streaming with Suspense
// Explicit async resource with streaming support
const board = createAsync(() => getBoard());
<Suspense>{board()?.name}</Suspense>;

// Qwik - automatic serialization
// $ suffix marks server-only code, automatically serialized
export const useBoard = routeLoader$(async () => {
  return await db.query.boards.findFirst();
});

// Nuxt - built-in caching
// Composable with explicit key and built-in deduplication
const { data: board } = await useAsyncData("board", () =>
  db.query.boards.findFirst()
);

// Analog - DI with server data
// Dependency injection brings Angular patterns to server data
export const load = injectLoad(() => {
  const service = inject(BoardService);
  return service.getBoard();
});

// Marko - route handlers with $global context
// Server handler runs first, sets data on context for page component
// +handler.ts
export async function GET(context) {
  const board = await db.query.boards.findFirst();
  context.board = board; // Available as $global.board in component
}

// +page.marko
<div>${$global.board.name}</div>;

The “big three” frameworks each carry conceptual complexity, though with different outcomes. React has rules around hooks and dependency arrays. Angular brings enterprise patterns like dependency injection alongside RxJS streams. Vue requires understanding refs and .value access, but unlike React and Angular, Vue (via Nuxt) has proven it can deliver competitive mobile web performance despite this complexity. These patterns become familiar with practice but never fully disappear and add cognitive overhead. Meta-frameworks compound these complexities: Next.js adds Server/Client boundaries and RSC serialization, Analog brings full Angular DI to the server, and Nuxt adds caching layers and composable patterns.

Most alternatives are conceptually simpler once learned. Svelte is most approachable (looks like enhanced HTML/JS). Solid and Marko use automatic tracking that eliminates manual dependency management. No rules of hooks, no dependency arrays, no .value boilerplate. The simpler mental models correlate with smaller bundles: less runtime complexity means less code to ship.

Does Next.js 16’s Built-in Compiler Change Anything?

The React team recognizes the complexity problem. Their solution, now fully integrated in Next.js 16: the React Compiler automatically handles memoization to reduce re-renders.

What the Compiler Does

The React Compiler analyzes your code and inserts useMemo and useCallback automatically. In Next.js 16, this is no longer experimental; it’s built-in and enabled by default. It’s a genuine improvement that reduces boilerplate.

The Results

The compiler helps with re-render optimization and removes manual memoization boilerplate. But it doesn’t address Next.js’s bundle size: Next.js ships 176.3 kB compressed (564.9 kB raw) for the board page while TanStack Start ships 118.2 kB compressed (373.6 kB raw) with the same React 19. The compiler can’t eliminate dependency arrays for useEffect. You still need to understand hooks, closures, and React’s rendering model.

The Irony

The compiler improves React’s developer experience, which is valuable. But it highlights an interesting contrast. React optimizes Virtual DOM re-renders, while alternatives like SolidStart eliminate re-renders entirely through fine-grained reactivity. SvelteKit’s compiler eliminates much of the runtime overhead at build time. Qwik eliminates hydration cost through resumability.

The difference in philosophy is clear. React’s compiler optimizes the existing model. The alternatives questioned the model itself.

The Web is the Last Commons: Why This Matters Beyond Frameworks

Here’s where this gets bigger than framework choice. When you ship a native app to the App Store or Google Play instead of building a web app, you’re not just making a technical decision. You’re accepting a deal that would’ve been unthinkable twenty years ago. Apple and Google each take up to 30% of every transaction (with exceptions depending on program and category). They set rules. They decide what you can ship. They can revoke your access tomorrow with no recourse. You have no alternative market. You can’t even compete on price because the fee is baked into many transactions.

Economist Yanis Varoufakis calls this “technofeudalism” in his book of the same name. The App Store isn’t a marketplace, it’s a fiefdom. Developers are digital serfs, bound to the cloud lords’ land (their platforms) with no exit. Users get locked into this too. The App Store is a curated garden where algorithms owned by two companies decide what you see. Your data gets harvested. Your choices get filtered. You’re not a customer with alternatives, you’re a subject in a walled garden.

The web? The web is different. No single company takes a cut. No algorithm curates your choices. Distribution is direct. Users can actually vote with their feet. It’s not perfect, but it’s the closest thing we have left to an open market where developers retain agency and users retain choice.

When companies abandon the web to go app-only, they’re not making a neutral technical decision. They’re voluntarily moving their users from a competitive marketplace into a feudal system. And yeah, I know that sounds dramatic, but Varoufakis has spent years documenting how the economics of digital platforms have created exactly this dynamic.

Why you should care, no matter what you believe:

If you lean capitalist, app stores create an environment that is the opposite of what capitalism is supposed to be. Monopolistic rent extraction replacing competition and innovation. No market mechanism to challenge them. That’s not capitalism, that’s just extraction.

If you lean anti-capitalist, technofeudalism is arguably worse than regular capitalism because at least capitalism has friction and regulatory handles. This has neither. It’s total private control with zero market competition.

Either way, the web is the last place where economic activity can happen outside the thumb of tech oligarchs. Building web apps matters. Shipping small, fast, performant web apps matters even more, and most web traffic comes from the mobile web. Every kilobyte you save is another reason for teams to choose the web over building a native app subject to app store control and fees.

Conclusion: What This Evaluation Revealed

What started as a simple framework comparison for an upcoming work project turned into something more revealing. The data shows clearly what’s possible when frameworks prioritize mobile web performance from the start.

The evaluation revealed what happens when you rethink fundamentals. SolidStart, SvelteKit, Qwik, and Marko represent different architectural priorities that push boundaries in ways the dominant frameworks cannot. Competition drives innovation. These alternatives show what’s achievable when mobile web performance is the primary design constraint, not an afterthought.

For teams serving mobile professionals on cellular networks (like ours), these costs are paid on every visit. React and Angular often face architectural performance ceilings. Vue proved that established frameworks can compete when properly configured. And remember, these measurements represent initial page loads. MPA frameworks maintain their lean profile across routes, while SPAs add route chunks to their baseline.

For anyone starting a new project, the evaluation raises an important question: Is there any reason to make your app 25.9x heavier than necessary? (And that’s before adding the authentication, analytics, and third party libraries that typically multiply production bundle sizes 5 to 10 times, making the real world gap even larger.) Building an app that works well on mobile isn’t difficult if you make good architectural decisions at the beginning. The right choice early on means your app performs well everywhere, not just on desktop WiFi. Choose MPA frameworks for the leanest per page bundles, or lightweight SPAs for excellent performance with familiar patterns.

Here’s what the evaluation made clear: if you want to build a better product than your competitors, why would you build exactly like them? When everyone uses Next.js, winning on performance requires fighting React’s architecture. When you use Marko, SolidStart, SvelteKit, or Nuxt instead, the advantage comes easily. Your app is faster by default. Your bundle is smaller without optimization work. Your users get a better experience without extra effort. That’s not just good engineering. That’s differentiation.

When you pick Marko and ship 28.8 kB instead of Next.js at 176.3 kB, you’re not just making your users’ experience better on cellular networks. You’re making the web more competitive. You’re making it a more attractive place for companies to exist. You’re pushing back against the gravity that pulls everything toward native only distribution.

Reproducing These Results

All measurements in this comparison follow a rigorous statistical methodology designed for reproducibility and defensibility. Each framework was measured 10 times per page using Chrome Lighthouse with mobile emulation (Pixel 5, 4G throttling, 1x CPU). Server warmup requests stabilize performance before measurements. IQR (Interquartile Range) outlier removal ensures robust statistics. Browser cache was cleared between runs to measure cold-load performance that simulates first-visit experience. I report median values to reduce the impact of outliers, and standard deviations quantify measurement reliability. The complete methodology including statistical approach, test environment details, compression detection, known limitations, and reproducibility instructions is documented at METHODOLOGY.md.

Call to Action

Try it yourself: Clone the repository, build all ten implementations, and test them on a throttled 3G connection in Chrome DevTools. When mobile web is your only option, the numbers tell a clear story.

On a less serious note: You can take a look at all ten apps to examine how the code looks and get a general feel for each framework. I advocate coding for fun, and the code in the repo might be a great place to help you try something new.

Share your experience: Have you tried Marko, SolidStart, SvelteKit, Qwik, or Nuxt? What framework would you choose for a mobile-first project and why? I’d love to hear your thoughts on Twitter or Bluesky.

Keep exploring: The full metrics data and measurement methodology are available in the repository for you to verify, reproduce, or extend. Build your own comparison and share your findings.

The real winner? You. Your team. Your users. When you start your next project with Marko, SolidStart, or SvelteKit, you’ll ship faster, smaller, and with less framework overhead. That’s a real competitive advantage.

Acknowledgements

A huge thanks to everyone who helped with this evaluation.

Early draft reviewers: Alex Russell and Dylan Piercey provided invaluable feedback on the post structure and arguments.

Framework-specific reviews:

Your contributions made this post more accurate and comprehensive. Thank you!

← Back to Blog