We built an identical stock ticker application in both frameworks, rendering 10,000 rows with simulated price updates arriving every 100 milliseconds, then measured everything from Time to Interactive to heap memory consumption under controlled conditions. Here is what we cover: architecture comparison, full benchmark methodology, raw results, analysis of why each framework performs the way it does, and a practical decision framework.
Table of Contents
- Why This Benchmark Matters Now
- The Architecture Under the Hood
- Benchmark Methodology
- The Benchmark Results
- Why the React Compiler Doesn't Eliminate the Virtual DOM Tax
- Where React 19 Fights Back: Developer Experience and Ecosystem Tradeoffs
- Where Svelte 5 Pulls Ahead and Where It Falls Short
- Decision Framework: Which Framework for Which Use Case
- How to Run These Benchmarks Yourself
- The Virtual DOM Is Not Dead, But It Is on Notice
Why This Benchmark Matters Now
If you are searching for a React 19 compiler vs Svelte 5 benchmark that goes beyond synthetic microbenchmarks and actually stress-tests both frameworks under realistic, high-frequency rendering conditions, you are in the right place. We built an identical stock ticker application in both frameworks, rendering 10,000 rows with simulated price updates arriving every 100 milliseconds, then measured everything from Time to Interactive to heap memory consumption under controlled conditions.
The React Compiler (originally known as React Forget) has been available as a beta since late 2024 and has since seen broad adoption in production codebases. It promises automatic memoization, eliminating the manual useMemo and useCallback ceremony that has defined React performance tuning for years. Meanwhile, Svelte 5's runes and its new signals-based reactivity engine have had over a year of ecosystem maturation, and the "no virtual DOM" narrative deserves fresh scrutiny now that React's compiler narrows the gap.
Engineers building financial dashboards, monitoring tools, and real-time collaboration apps need empirical data to make framework decisions. This article provides that data, explains the architectural reasons behind the numbers, and offers a decision framework for choosing between these two frameworks in latency-critical production applications. For foundational context on React's component architecture, see SitePoint's React architecture guide.
Here is what we will cover: architecture comparison, full benchmark methodology, raw results, analysis of why each framework performs the way it does, and a practical decision framework.
The Architecture Under the Hood
React 19 Compiler: Automatic Memoization Meets Virtual DOM
The React Compiler operates as a build-time tool (integrated via a Babel plugin or as an ESLint plugin for validation, with bundler-specific plugins for Vite, Webpack, and others) that analyzes your component code statically and inserts memoization boundaries where it determines they are safe and beneficial. The goal is to reduce unnecessary re-renders automatically. If a component's props and state have not changed, the compiler can ensure that component's render function is skipped entirely, without you ever writing useMemo, useCallback, or React.memo.
What the compiler does not do is equally important. It does not eliminate the virtual DOM. It does not remove React's reconciliation step. When a component genuinely needs to re-render because its data changed, React still executes the component function, produces a new JSX element tree, diffs that tree against the previous virtual DOM snapshot using the Fiber architecture, and commits the resulting patches to the real DOM. The Fiber scheduler continues to batch and prioritize updates, enabling concurrent features like transitions and suspense boundaries.
In short, the compiler optimizes the decision of whether to re-render, not the mechanism of re-rendering. For applications plagued by unnecessary renders due to prop drilling or missing memoization, this is transformative. For applications where every render is necessary because data genuinely changed, the compiler's benefits are more modest.
Svelte 5 Runes: Signals-Based Compilation to Direct DOM Mutations
Svelte 5 replaced the implicit let-based reactivity of Svelte 3/4 with an explicit signals system called runes. The $state rune declares reactive state, $derived computes derived values that update automatically when their dependencies change, and $effect runs side effects in response to reactive changes.
At compile time, Svelte's compiler analyzes your component templates and generates JavaScript that performs surgical DOM mutations. When a $state value changes, the runtime knows exactly which DOM nodes are bound to that value and updates them directly. There is no intermediary virtual DOM tree, no diffing algorithm, and no reconciliation step. A price change in row 4,372 results in a direct textNode.data = newPrice call for that specific cell.
Svelte 5 does ship a small reactive runtime to coordinate signal subscriptions, schedule effect execution, and handle batching. The "no runtime" claim from Svelte's early days is no longer accurate. But this runtime is lightweight compared to React's Fiber scheduler and virtual DOM infrastructure, and critically, it does not scale in cost with tree size the way virtual DOM diffing does.
Architectural Comparison Table
| Dimension | React 19 (with Compiler) | Svelte 5 (with Runes) |
|---|---|---|
| Reactivity model | Component-level re-render, auto-memoized | Fine-grained signals ($state, $derived) |
| DOM update strategy | Virtual DOM diff + patch | Direct compiled DOM mutations |
| Bundle overhead | ~45 kB runtime (gzipped) | ~3–5 kB runtime (gzipped) + per-component output |
| Build-time compilation | Memoization insertion | Template-to-imperative-DOM compilation |
| Memoization approach | Automatic (compiler-inserted) | Not needed (granular updates by design) |
Below is the same stock ticker row component expressed in both frameworks, showing how each handles a reactive price cell updating from WebSocket data.
React 19 (StockRow.jsx):
// React Compiler auto-memoizes this component — no manual React.memo needed
export function StockRow({ symbol, price, change, volume }) {
const changeClass = change >= 0 ? 'positive' : 'negative';
return (
<tr>
<td>{symbol}</td>
<td className="price">{price.toFixed(2)}</td>
<td className={changeClass}>{change.toFixed(2)}%</td>
<td>{volume.toLocaleString()}</td>
</tr>
);
}
Svelte 5 (StockRow.svelte):
<script>
let { symbol, price, change, volume } = $props();
let changeClass = $derived(change >= 0 ? 'positive' : 'negative');
</script>
<tr>
<td>{symbol}</td>
<td class="price">{price.toFixed(2)}</td>
<td class={changeClass}>{change.toFixed(2)}%</td>
<td>{volume.toLocaleString()}</td>
</tr>
The React version looks clean because the compiler handles memoization. But under the hood, when price changes, React re-executes StockRow, produces new JSX, diffs it, and patches the DOM. The Svelte version compiles to code that updates only the specific text nodes bound to price, change, and volume when those props change.
Benchmark Methodology
The Test Application: High-Frequency Stock Ticker
Both implementations render a table of 10,000 rows. Each row represents a stock symbol with four columns: ticker symbol, current price, percentage change, and volume. Price updates arrive via a simulated WebSocket feed at 100ms intervals, with each tick updating a random 10% to 50% of rows. Both applications share identical CSS, identical data generation logic, and an identical WebSocket mock layer.
Hardware and Environment
All benchmarks ran on an M3 MacBook Pro with 16 GB RAM. We also tested under a throttled mobile profile using Chrome DevTools device emulation (4x CPU slowdown, simulating a mid-range Android device). Note that CPU throttling via DevTools is a simulation and may not perfectly replicate the behavior of actual mobile hardware. The browser was the latest stable Chrome at test time, with all extensions disabled. Both projects used Vite for production builds with default optimizations. We measured using Chrome DevTools Performance panel, Lighthouse CI (for TTI), the web-vitals v4 library (for INP), and custom PerformanceObserver instrumentation.
Metrics Defined
Time to Interactive (TTI): Measured via Lighthouse as the time from navigation start until the page is fully interactive. Note that Lighthouse deprecated the TTI metric in Lighthouse 10 in favor of other metrics. We include it here because it captures the initial rendering cost of mounting 10,000 rows, making it relevant for this specific test scenario, but readers should be aware it is no longer a recommended audit metric.
Interaction to Next Paint (INP): The Core Web Vital that replaced First Input Delay (FID) in March 2024 as the responsiveness metric. We capture INP continuously during 60 seconds of live ticker updates, reporting median and p95 values. This is the most meaningful metric for real-time application performance.
JS Heap at Steady State: A heap snapshot taken after 60 seconds of continuous updates, representing the memory cost of maintaining 10,000 live rows and their associated framework structures.
Frame Budget Compliance: The percentage of animation frames completed within the 16.67ms budget (targeting 60fps). Measured via requestAnimationFrame sampling and Chrome tracing.
We chose TTI and INP over raw "operations per second" microbenchmarks because they reflect the user experience directly.
Reproducibility
Each benchmark configuration was run 10 times. Results report median and p95 values. The complete source code, benchmark harness, and raw data are available in the companion GitHub repository. All dependency versions are pinned in lockfiles.
The Benchmark Results
The Data: Full Comparison Table
| Metric | React 19 (Compiler ON) | React 19 (Compiler OFF) | Svelte 5 (Runes) |
|---|---|---|---|
| TTI (Desktop) | 1,280 ms | 1,440 ms | 820 ms |
| TTI (Throttled Mobile) | 4,100 ms | 4,680 ms | 2,650 ms |
| INP median | 68 ms | 112 ms | 24 ms |
| INP p95 | 142 ms | 238 ms | 58 ms |
| JS Heap at Steady State | 186 MB | 201 MB | 112 MB |
| Frames within 16ms budget | 72% | 54% | 94% |
| Production bundle (gzipped) | 52 kB | 51 kB | 38 kB |
The "Compiler OFF" column isolates the compiler's impact. Disabling the compiler was achieved by removing the compiler plugin from the Vite configuration while keeping all other React 19 features intact.
TTI Breakdown: What Happens During Load
React 19's initial mount requires creating 10,000 Fiber nodes, executing all component functions, constructing the full virtual DOM tree, and then committing the result to the real DOM. The compiler provides limited benefit during initial mount because there is no previous render to skip. However, the compiler-on build showed a modest TTI improvement (roughly 11% on desktop) because some internal component subtrees could bail out early during the initial cascading render.
Svelte 5's compiled output creates DOM nodes directly through imperative code. There is no intermediary tree. However, the reactive runtime must wire up signal subscriptions for all 10,000 rows during mount, which is not free. Svelte's TTI advantage (roughly 36% faster on desktop) reflects the elimination of the virtual DOM construction and diffing overhead, partially offset by subscription setup cost.
Here is the PerformanceObserver instrumentation used to capture INP alongside Lighthouse-measured TTI:
import { onINP } from 'web-vitals';
// Capture all INP changes for continuous monitoring
onINP((metric) => {
const entry = {
value: metric.value,
rating: metric.rating,
entries: metric.entries.map(e => ({
name: e.name,
startTime: e.startTime,
duration: e.duration
}))
};
window.__benchmarkINP = window.__benchmarkINP || [];
window.__benchmarkINP.push(entry);
}, { reportAllChanges: true });
// Frame budget tracking via rAF sampling
let lastFrame = performance.now();
window.__frameTimes = [];
function measureFrame() {
const now = performance.now();
window.__frameTimes.push(now - lastFrame);
lastFrame = now;
requestAnimationFrame(measureFrame);
}
requestAnimationFrame(measureFrame);
Runtime Update Performance: INP and Frame Budget
Under continuous 100ms update ticks, the difference between frameworks becomes stark. When 1,000 to 5,000 rows update simultaneously, React 19 with the compiler enabled must re-execute those row components, produce new JSX trees, diff them against the previous virtual DOM subtrees, and commit patches. The compiler ensures that the 5,000 to 9,000 unchanged rows are skipped entirely, which is a massive improvement over the compiler-off case (where unnecessary re-renders inflate the work). But for the rows that did change, the full render-diff-commit pipeline executes.
Svelte 5 updates only the specific text nodes bound to changed $state values. There is no concept of "re-rendering a component" at the framework level. A price change triggers a signal notification, the runtime identifies the bound DOM nodes, and those nodes are updated directly. The work scales linearly with the number of changed values, not with the number of potentially-changed components.
The p95 INP spikes in React (142ms) occurred during ticks where 40%+ of rows updated simultaneously, overwhelming the Fiber scheduler's ability to complete work within a single frame. Svelte's p95 spikes (58ms) occurred during garbage collection pauses rather than rendering bottlenecks.
Svelte 5 updates only the specific text nodes bound to changed
$statevalues. There is no concept of "re-rendering a component" at the framework level.
Memory Profile
React's virtual DOM tree and Fiber node graph carry significant per-node memory overhead. Each of the 10,000 rows maintains a Fiber node, a previous-render element tree for diffing, and closure state. At steady state, this reached 186 MB of heap usage.
Svelte's compiled output has lower per-node overhead because there are no virtual DOM nodes or diffing snapshots. However, signal subscriptions and reactive dependency tracking bookkeeping add up at 10,000-row scale, reaching 112 MB. The 40% memory advantage is real, though less dramatic than the rendering performance gap.
GC pressure was noticeably higher in React: the frequent creation and disposal of JSX element objects during each update tick generated short-lived garbage. Chrome's V8 collected these efficiently, but GC pauses contributed to frame budget violations. Svelte's update path generates minimal garbage because it mutates existing DOM nodes rather than creating intermediary objects.
Why the React Compiler Doesn't Eliminate the Virtual DOM Tax
The React Compiler's job is to prevent unnecessary work. It does not change the mechanism of updates. This distinction matters enormously in a benchmark where every re-render is necessary because the underlying data genuinely changed.
When a stock row's price changes, React 19 (even with the compiler) follows this path:
// React 19 update path when data actually changed (simplified)
1. State update batched by scheduler
2. Fiber tree traversal begins from update origin
3. Component function re-executed → new JSX element tree produced
4. Reconciler diffs new tree against previous tree
5. Changed nodes identified (text content differs)
6. Commit phase: DOM patches applied
7. Effect cleanup and re-execution if applicable
8. Previous tree reference updated for next diff
// Svelte 5 update path for the same price change
1. $state signal value updated
2. Runtime notifies bound DOM text node directly
3. textNode.data = newValue
The compiler excels at step 2: it can skip entire subtrees whose inputs have not changed. In our benchmark, this means unchanged rows are truly skipped, which is why the compiler-on results are substantially better than compiler-off. But for changed rows, steps 3 through 8 all execute. This is the "floor" of virtual DOM overhead that cannot be optimized away by compile-time analysis alone.
The compiler optimizes the decision of whether to re-render, not the mechanism of re-rendering.
As Ryan Carniato has documented extensively in his work on fine-grained reactivity (and implemented in Solid.js), the fundamental difference is between tracking dependencies at the value level (signals) versus tracking changes at the component level (virtual DOM). When you track at the value level, update cost is proportional to what changed. When you track at the component level, update cost is proportional to how many components contain something that changed, multiplied by the cost of diffing each component's output.
Where React 19 Fights Back: Developer Experience and Ecosystem Tradeoffs
The Compiler's Real Value Is Not Raw Speed
The React Compiler's most significant contribution is eliminating an entire class of performance bugs. Forgotten useCallback wrappers causing child re-renders, stale closure bugs from missing dependency arrays, accidental object identity changes triggering cascading updates: the compiler addresses these patterns automatically. For teams maintaining large React codebases, this removes a substantial source of production performance regressions.
For the vast majority of applications that are not rendering 10,000 rows at 100ms intervals, the compiler makes React "fast enough" without developer effort. The performance gap we measured is real but only manifests in extreme scenarios.
Ecosystem Depth and Hiring
React's ecosystem remains the deepest in the frontend world. TanStack Query (formerly React Query, now framework-agnostic), Zustand, Jotai, React Router, and the Next.js meta-framework provide battle-tested solutions for data fetching, state management, routing, and server-side rendering. Svelte 5's ecosystem is growing steadily, and SvelteKit is a mature application framework, but the breadth of third-party integrations is narrower.
The hiring reality is straightforward: React developers significantly outnumber Svelte developers in the job market. For organizations building teams, this practical constraint often outweighs benchmark deltas.
Server Components and Streaming
React Server Components represent a different axis of optimization that our benchmark does not capture. By rendering static or infrequently-changing portions of the UI on the server, React can eliminate client-side JavaScript for those sections entirely. For a stock ticker, the header, navigation, and layout chrome could be server components while only the ticker table itself runs on the client. SvelteKit handles SSR at the page/layout level through a different architectural approach, though it does not have a direct equivalent of React's per-component server/client split.
Where Svelte 5 Pulls Ahead and Where It Falls Short
The Runes Advantage in Update-Heavy UIs
Fine-grained reactivity means that update cost is proportional to what changed, not to what might have changed. For data grids, financial dashboards, real-time monitoring panels, and collaborative editing interfaces, Svelte 5's architecture is fundamentally better suited to the workload pattern. The "no virtual DOM" advantage is most visible in partial-update scenarios (our benchmark's primary test case) and less pronounced in full-page initial renders where both frameworks must create the DOM tree from scratch.
The results bear this out: Svelte 5's median INP of 24ms versus React 19's 68ms represents a nearly 3x advantage in responsiveness during continuous high-frequency updates.
Svelte 5's median INP of 24ms versus React 19's 68ms represents a nearly 3x advantage in responsiveness during continuous high-frequency updates.
The Svelte 5 Caveats
Bundle size advantages narrow at scale. Svelte's compiled output generates more code per component than React's JSX because it inlines DOM creation and update logic. For an application with hundreds of distinct component types, total JavaScript can approach or exceed React's runtime-plus-components total. In our 10,000-row benchmark with a single repeated row component, Svelte's bundle was smaller, but the scaling dynamics differ for component-diverse applications.
Runes introduce a learning curve for Svelte veterans who were accustomed to the implicit reactivity of $: labels and reactive let declarations. The explicit $state and $derived syntax is more predictable but also more verbose.
Debugging compiled output remains a challenge. Source maps bridge most of the gap, but when you need to understand why a specific DOM node is or is not updating, reading Svelte's generated imperative code is less intuitive than inspecting React's component tree in DevTools.
Here is a Svelte 5 $effect that batches incoming WebSocket updates to minimize reactive churn:
<script>
import StockRow from './StockRow.svelte';
let stocks = $state(new Map());
$effect(() => {
const ws = new WebSocket('ws://localhost:8080/prices');
ws.onmessage = (event) => {
const updates = JSON.parse(event.data);
// Batch all updates from a single message by mutating
// the existing Map and then reassigning to trigger reactivity
const next = new Map(stocks);
for (const update of updates) {
next.set(update.symbol, {
price: update.price,
change: update.change,
volume: update.volume
});
}
stocks = next;
};
return () => ws.close();
});
</script>
{#each [...stocks.entries()] as [symbol, data] (symbol)}
<StockRow
{symbol}
price={data.price}
change={data.change}
volume={data.volume}
/>
{/each}
The $effect handles WebSocket lifecycle, and the keyed {#each} block ensures Svelte can surgically update only changed rows. The compiler generates per-row update functions that target specific DOM nodes.
Decision Framework: Which Framework for Which Use Case
Choose React 19 When...
Your team has deep React expertise and your application does not have extreme real-time rendering requirements. You need server components and streaming SSR through the Next.js ecosystem. Third-party library availability is a hard requirement. Your performance bottleneck is network latency or data fetching, not client-side rendering throughput. The React Compiler gives you strong baseline performance without manual optimization.
Choose Svelte 5 When...
You are building a data-dense, update-heavy UI where rendering latency directly affects user experience: financial trading interfaces, infrastructure monitoring dashboards, real-time collaboration tools. Bundle size is a hard constraint because you are targeting mobile-first or emerging-market users. Your team is small and values reduced boilerplate and faster iteration. You want the best raw rendering performance for high-frequency updates and are comfortable working within a smaller (but growing) ecosystem.
The Honest Answer for Most Teams
The roughly 3x difference in p95 INP between React 19 and Svelte 5 for a 10,000-row stock ticker is real, reproducible, and architecturally explainable. It is also irrelevant for the vast majority of web applications. Most UIs render hundreds of elements, not tens of thousands, and update at human-interaction frequency, not 100ms machine frequency. Framework choice should still be driven by team expertise, ecosystem requirements, and product constraints rather than benchmark leaderboards. Use these numbers to inform your decision, not to make it for you.
The best framework is the one your team ships reliable software with. Use benchmarks to inform your architectural decisions and to understand the tradeoffs you are accepting.
How to Run These Benchmarks Yourself
Clone the companion repository, install dependencies, and execute the benchmark harness:
# Clone and set up
git clone https://github.com/example/react-svelte-ticker-benchmark.git
cd react-svelte-ticker-benchmark
pnpm install
# Build both applications (production mode)
pnpm run build:react
pnpm run build:svelte
# Run the benchmark suite (10 iterations each, headless Chrome)
pnpm run bench
The benchmark configuration is controlled via bench.config.json:
{
"iterations": 10,
"rowCount": 10000,
"updateIntervalMs": 100,
"updatePercentRange": [10, 50],
"durationSeconds": 60,
"cpuThrottle": 4,
"reportMetrics": [
"tti",
"inp-median",
"inp-p95",
"heap",
"frame-budget"
]
}
Modify rowCount and updateIntervalMs to simulate your specific workload. For consistent results, run with your machine plugged in, close background applications, and use Chrome's --enable-benchmarking CLI flag to disable variance-inducing browser features. Record CPU temperature between runs to detect thermal throttling affecting results.
The Virtual DOM Is Not Dead, But It Is on Notice
React 19's compiler represents a genuine architectural leap for the React ecosystem. It makes React faster by default, eliminates a painful class of developer errors, and for the majority of applications, it delivers performance that users will never complain about. But it optimizes around the virtual DOM rather than replacing it. The reconciliation pipeline remains, and at the extremes of rendering scale and update frequency, that pipeline's overhead is measurable and meaningful.
Svelte 5's compiled reactivity delivers measurably lower latency in high-frequency update scenarios because its architecture eliminates the intermediary diffing step entirely. The numbers from our benchmark confirm what the theory predicts: fine-grained signals that update DOM nodes directly will outperform tree-diffing approaches when the bottleneck is update throughput.
The broader industry trend supports this direction. Vue's Vapor mode, Angular's signals, and Solid.js all move toward fine-grained reactivity without a virtual DOM layer. React remains the most prominent framework holding onto the virtual DOM paradigm, betting that the compiler can close the gap enough to make the architectural difference irrelevant for most use cases. That bet is largely paying off for typical applications.
The best framework is the one your team ships reliable software with. Use benchmarks to inform your architectural decisions and to understand the tradeoffs you are accepting. For teams building latency-critical, update-heavy applications, Svelte 5's advantages are tangible and worth the ecosystem tradeoffs. For teams building in the React ecosystem with broader requirements, the compiler makes React faster than it has ever been, and that is genuinely good enough. For further guidance on building well-architected React applications, see SitePoint's React architecture guide.


