MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Svelte 5 Runes vs React Hooks: A Comparison

Compare Svelte 5 runes and React hooks: reactivity models, performance, and DX.

SvelteReactHooksFrontend

By MinhVo

Introduction

The frontend framework landscape in 2024 presents developers with a fascinating architectural dichotomy: React's hooks-based model, which triggers re-renders through virtual DOM diffing, versus Svelte 5's rune-based signals, which achieve surgical DOM updates through compile-time optimization. Both approaches solve the same fundamental problem—keeping the UI in sync with application state—but through fundamentally different mechanisms.

React hooks, introduced in React 16.8, replaced class components with a function-based API that uses useState, useEffect, and useMemo to manage state and side effects. The model is intuitive: when state changes, the component function re-executes, and React's reconciler diffs the virtual DOM to determine minimal updates. This approach prioritizes developer mental models (functions are easier to reason about than classes) at the cost of runtime overhead (every state change triggers a full function re-execution).

Svelte 5 runes, conversely, represent a signal-based approach where $state, $derived, and $effect create a reactive dependency graph that the compiler transforms into optimized vanilla JavaScript. When state changes, only the specific DOM elements that reference that state update—no virtual DOM, no reconciliation, no function re-execution. This prioritizes runtime performance at the cost of requiring compiler involvement.

This comparison dives deep into both approaches, examining their architectural foundations, developer experience trade-offs, performance characteristics, and practical implications for building production applications.

React vs Svelte Architecture

Understanding the Reactivity Models

React's Re-Render Model

React's hooks operate on a simple principle: when state changes, the component function re-executes from top to bottom. This means every useState, useMemo, and useCallback call runs again on every render. The virtual DOM provides a "safety net" that ensures only actual DOM changes are committed.

function Counter() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Alice');
 
  // This runs on EVERY render, even when only name changes
  const doubled = useMemo(() => count * 2, [count]);
 
  // This callback is recreated on every render without useCallback
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);
 
  console.log('Component rendered'); // Logs on every state change
 
  return (
    <div>
      <p>{count} × 2 = {doubled}</p>
      <input value={name} onChange={e => setName(e.target.value)} />
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

The key insight is that React's model is "pull-based"—the entire component function is the unit of re-execution. The virtual DOM then "pushes" only the necessary DOM updates. This separation means developers write straightforward code (the function just runs) but pay a runtime cost for the diffing step.

Svelte 5's Signal Model

Svelte 5 runes operate on a different principle: state changes propagate through a dependency graph to only the DOM elements that directly reference the changed state. The component function itself doesn't re-execute.

<script>
  let count = $state(0);
  let name = $state('Alice');
 
  // This only recalculates when count changes
  let doubled = $derived(count * 2);
 
  // This function is created once, never recreated
  function handleClick() {
    count++;
  }
 
  console.log('Script runs once'); // Only logs on mount
</script>
 
<div>
  <p>{count} × 2 = {doubled}</p>
  <input bind:value={name} />
  <button onclick={handleClick}>Increment</button>
</div>

In this Svelte 5 example, clicking "Increment" doesn't re-execute the <script> block. Instead, the signal for count notifies the specific text node displaying count and the $derived computation for doubled. The <input> binding is unaffected because it only depends on name.

Mental Model Comparison

AspectReact HooksSvelte 5 Runes
State change triggersFull function re-executionTargeted signal propagation
Dependency trackingManual (dependency arrays)Automatic (compiler-analyzed)
MemoizationExplicit (useMemo, useCallback)Implicit ($derived is always memoized)
Side effectsuseEffect with cleanup$effect with cleanup
Stale closuresCommon pitfallNot possible (no re-execution)
Rules/constraintsRules of Hooks (no conditionals)No special rules

Reactivity Model Diagram

Architecture and Design Patterns

Dependency Tracking

React's Manual Dependencies

React requires developers to explicitly declare effect dependencies:

useEffect(() => {
  fetchUser(userId);
}, [userId]); // Manual dependency specification
 
const memoizedValue = useMemo(() => {
  return expensiveComputation(data);
}, [data]); // Must list all dependencies

This creates several well-documented pitfalls:

  • Forgetting dependencies causes stale closures
  • Including function dependencies causes unnecessary re-executions
  • The exhaustive-deps ESLint rule helps but doesn't catch all cases

Svelte 5's Automatic Dependencies

Svelte 5's compiler analyzes expressions to automatically build the dependency graph:

<script>
  let userId = $state(1);
  let data = $state([]);
 
  // Compiler automatically tracks that this depends on userId
  $effect(() => {
    fetchUser(userId);
  });
 
  // Compiler automatically tracks that this depends on data
  let processed = $derived(expensiveComputation(data));
</script>

No dependency arrays, no stale closures, no missing dependencies. The compiler knows exactly what each expression reads and builds the signal graph accordingly.

Component Composition

React's Props Drilling and Context

React components communicate through props and context. For deeply nested state, React offers Context API:

const ThemeContext = createContext('light');
 
function App() {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Layout />
    </ThemeContext.Provider>
  );
}
 
function Button() {
  const { theme, setTheme } = useContext(ThemeContext);
  return <button className={theme}>Toggle Theme</button>;
}

Context causes all consumers to re-render when any part of the context value changes, even if they only use a subset of the context.

Svelte 5's Runes-Based Composition

Svelte 5 achieves similar composition without the re-render penalty:

// src/lib/context/theme.svelte.ts
import { setContext, getContext } from 'svelte';
 
export function createThemeContext() {
  let theme = $state<'light' | 'dark'>('light');
 
  function toggle() {
    theme = theme === 'light' ? 'dark' : 'light';
  }
 
  const context = {
    get theme() { return theme; },
    toggle
  };
 
  setContext('theme', context);
  return context;
}
 
export function getTheme() {
  return getContext<ReturnType<typeof createThemeContext>>('theme');
}
<!-- Button.svelte -->
<script>
  import { getTheme } from '$lib/context/theme.svelte';
  const { theme, toggle } = getTheme();
</script>
 
<button class={theme} onclick={toggle}>Toggle Theme</button>

When theme changes, only the class attribute on the <button> updates—the component function doesn't re-execute.

State Management Libraries

React Ecosystem

React's re-render model has spawned a rich ecosystem of state management solutions, each trying to minimize unnecessary re-renders:

  • Redux: Centralized store with selector-based subscriptions
  • Zustand: Minimal store with selector subscriptions
  • Jotai: Atomic state with dependency tracking
  • Recoil: Atom-based state with derived selectors

Each library adds complexity to solve the re-render problem that React's architecture creates.

Svelte 5's Built-in Solution

Svelte 5's runes provide built-in state management that doesn't require external libraries:

// This is a complete, performant state management solution
// No external library needed
let items = $state<Item[]>([]);
let filter = $state('all');
 
let filteredItems = $derived(
  filter === 'all' ? items : items.filter(i => i.status === filter)
);
 
let stats = $derived({
  total: items.length,
  active: items.filter(i => i.status === 'active').length,
  completed: items.filter(i => i.status === 'completed').length
});

The signals architecture ensures that changing filter only updates the DOM elements displaying filteredItems, not the entire component tree.

Step-by-Step Implementation

Building the Same Component in Both Frameworks

Let's implement a searchable product list with filters to compare the two approaches:

React Implementation

// React: ProductList.tsx
import { useState, useMemo, useCallback, useEffect } from 'react';
 
interface Product {
  id: string;
  name: string;
  category: string;
  price: number;
  inStock: boolean;
}
 
export function ProductList() {
  const [products, setProducts] = useState<Product[]>([]);
  const [search, setSearch] = useState('');
  const [category, setCategory] = useState('all');
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name');
  const [loading, setLoading] = useState(true);
 
  // Fetch products on mount
  useEffect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(data => {
        setProducts(data);
        setLoading(false);
      });
  }, []);
 
  // Memoize filtered and sorted products
  const filteredProducts = useMemo(() => {
    let result = products;
 
    if (search) {
      result = result.filter(p =>
        p.name.toLowerCase().includes(search.toLowerCase())
      );
    }
 
    if (category !== 'all') {
      result = result.filter(p => p.category === category);
    }
 
    return [...result].sort((a, b) => {
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      return a.price - b.price;
    });
  }, [products, search, category, sortBy]);
 
  // Memoize stats
  const stats = useMemo(() => ({
    total: filteredProducts.length,
    inStock: filteredProducts.filter(p => p.inStock).length,
    avgPrice: filteredProducts.length > 0
      ? filteredProducts.reduce((sum, p) => sum + p.price, 0) / filteredProducts.length
      : 0
  }), [filteredProducts]);
 
  // Memoize event handlers
  const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
  }, []);
 
  const handleCategory = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
    setCategory(e.target.value);
  }, []);
 
  if (loading) return <div>Loading...</div>;
 
  return (
    <div>
      <input value={search} onChange={handleSearch} placeholder="Search..." />
      <select value={category} onChange={handleCategory}>
        <option value="all">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
      <select value={sortBy} onChange={e => setSortBy(e.target.value as any)}>
        <option value="name">Sort by Name</option>
        <option value="price">Sort by Price</option>
      </select>
      <p>{stats.total} products, {stats.inStock} in stock</p>
      <ul>
        {filteredProducts.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
          </li>
        ))}
      </ul>
    </div>
  );
}

Svelte 5 Implementation

<!-- Svelte 5: ProductList.svelte -->
<script lang="ts">
  let products = $state.raw<Product[]>([]);
  let search = $state('');
  let category = $state('all');
  let sortBy = $state<'name' | 'price'>('name');
  let loading = $state(true);
 
  $effect(() => {
    fetch('/api/products')
      .then(res => res.json())
      .then(data => {
        products = data;
        loading = false;
      });
  });
 
  let filteredProducts = $derived.by(() => {
    let result = products;
 
    if (search) {
      result = result.filter(p =>
        p.name.toLowerCase().includes(search.toLowerCase())
      );
    }
 
    if (category !== 'all') {
      result = result.filter(p => p.category === category);
    }
 
    return [...result].sort((a, b) => {
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      return a.price - b.price;
    });
  });
 
  let stats = $derived({
    total: filteredProducts.length,
    inStock: filteredProducts.filter(p => p.inStock).length,
    avgPrice: filteredProducts.length > 0
      ? filteredProducts.reduce((sum, p) => sum + p.price, 0) / filteredProducts.length
      : 0
  });
</script>
 
<div>
  <input bind:value={search} placeholder="Search..." />
  <select bind:value={category}>
    <option value="all">All Categories</option>
    <option value="electronics">Electronics</option>
    <option value="clothing">Clothing</option>
  </select>
  <select bind:value={sortBy}>
    <option value="name">Sort by Name</option>
    <option value="price">Sort by Price</option>
  </select>
  <p>{stats.total} products, {stats.inStock} in stock</p>
  {#if loading}
    <div>Loading...</div>
  {:else}
    <ul>
      {#each filteredProducts as product (product.id)}
        <li>{product.name} - ${product.price}</li>
      {/each}
    </ul>
  {/if}
</div>

Code Comparison

AspectReact (Lines)Svelte 5 (Lines)Difference
Total code6242-32%
Memoization code15 (useMemo, useCallback)0 (automatic)-100%
Event handler boilerplate60 (bind:value)-100%
Dependency arrays40-100%
Loading state handlingSeparate variable + conditionalSame patternSame

Implementation Comparison

Real-World Use Cases

Use Case 1: Form Validation

React Approach

React forms require careful management of validation state to avoid unnecessary re-renders:

function RegistrationForm() {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    confirmPassword: ''
  });
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [touched, setTouched] = useState<Record<string, boolean>>({});
 
  // Validate on every change - causes re-render
  useEffect(() => {
    const newErrors: Record<string, string> = {};
 
    if (touched.email && !formData.email.includes('@')) {
      newErrors.email = 'Invalid email';
    }
    if (touched.password && formData.password.length < 8) {
      newErrors.password = 'Password must be 8+ characters';
    }
    if (touched.confirmPassword && formData.password !== formData.confirmPassword) {
      newErrors.confirmPassword = 'Passwords must match';
    }
 
    setErrors(newErrors);
  }, [formData, touched]);
 
  const isFormValid = useMemo(() => {
    return formData.email.includes('@') &&
           formData.password.length >= 8 &&
           formData.password === formData.confirmPassword;
  }, [formData]);
 
  return (/* form JSX */);
}

Svelte 5 Approach

Svelte 5's derived state makes validation straightforward:

<script lang="ts">
  let formData = $state({
    email: '',
    password: '',
    confirmPassword: ''
  });
  let touched = $state<Record<string, boolean>>({});
 
  let errors = $derived({
    email: touched.email && !formData.email.includes('@') ? 'Invalid email' : '',
    password: touched.password && formData.password.length < 8 ? 'Password must be 8+ characters' : '',
    confirmPassword: touched.confirmPassword && formData.password !== formData.confirmPassword ? 'Passwords must match' : ''
  });
 
  let isFormValid = $derived(
    formData.email.includes('@') &&
    formData.password.length >= 8 &&
    formData.password === formData.confirmPassword
  );
</script>

Use Case 2: Data Fetching with Caching

React with React Query

function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000
  });
 
  const { data: posts } = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId),
    enabled: !!user
  });
 
  if (isLoading) return <Skeleton />;
 
  return (
    <div>
      <h1>{user.name}</h1>
      <PostList posts={posts} />
    </div>
  );
}

Svelte 5 Approach

<script lang="ts">
  let { userId }: { userId: string } = $props();
 
  let user = $state.raw<User | null>(null);
  let posts = $state.raw<Post[]>([]);
  let isLoading = $state(true);
 
  $effect(() => {
    isLoading = true;
    fetchUser(userId).then(data => {
      user = data;
      isLoading = false;
    });
  });
 
  $effect(() => {
    if (user) {
      fetchPosts(userId).then(data => {
        posts = data;
      });
    }
  });
</script>
 
{#if isLoading}
  <Skeleton />
{:else}
  <h1>{user?.name}</h1>
  <PostList {posts} />
{/if}

Use Case 3: Real-Time Collaboration

React's re-render model makes real-time features expensive—every cursor movement from other users triggers a full component re-render. Svelte 5's signals enable surgical updates:

<script lang="ts">
  let document = $state({ content: '', version: 0 });
  let cursors = $state<Map<string, { x: number; y: number }>>(new Map());
 
  $effect(() => {
    const ws = new WebSocket('wss://collab.example.com/doc/123');
 
    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      if (msg.type === 'content') {
        document.content = msg.content;
        document.version = msg.version;
      } else if (msg.type === 'cursor') {
        // Only the specific cursor's position updates
        cursors.set(msg.userId, { x: msg.x, y: msg.y });
      }
    };
 
    return () => ws.close();
  });
</script>
 
<!-- Each cursor updates independently -->
{#each [...cursors.entries()] as [userId, pos] (userId)}
  <div class="remote-cursor" style="left: {pos.x}px; top: {pos.y}px">
    {userId}
  </div>
{/each}

Best Practices for Production

  1. Choose React for ecosystem breadth: If you need mature libraries for every conceivable use case, React's ecosystem is unmatched. State management, form handling, animation—there are battle-tested solutions for everything.

  2. Choose Svelte 5 for performance-critical UIs: If you're building dashboards, data visualization, or real-time applications where update performance matters, Svelte 5's signal architecture provides measurably better performance.

  3. Use React Query/SWR for data fetching in React: React's re-render model makes data fetching complex. Libraries like React Query handle caching, deduplication, and background updates that Svelte 5's $effect can handle natively.

  4. Prefer $derived over $effect for computations: In Svelte 5, always use $derived for pure computations. Reserve $effect for actual side effects like API calls or DOM manipulation.

  5. Avoid stale closures in React: Use useCallback for event handlers and useRef for values that need to persist across renders without causing re-renders. The exhaustive-deps ESLint rule is essential.

  6. Leverage Svelte 5's automatic dependency tracking: Don't try to manually manage dependencies like you would in React. The compiler handles it—focus on writing correct reactive expressions.

  7. Consider team expertise: React's larger community means easier hiring and more resources. Svelte's smaller learning curve can mean faster onboarding for new developers.

  8. Measure before optimizing: Both frameworks perform well for most applications. Profile with real data before choosing a framework based on theoretical performance advantages.

Common Pitfalls and Solutions

PitfallImpactSolution
React: Missing dependency in useEffectStale closure; effect uses outdated valuesUse exhaustive-deps ESLint rule; include all referenced values
React: Unnecessary re-renders from contextPerformance degradation in large appsSplit contexts; use memoization selectors (Zustand, Jotai)
React: Recreating objects in renderChild components re-render unnecessarilyuseMemo for objects; useCallback for functions
Svelte: Reassigning $state objectLoses fine-grained reactivityMutate properties directly instead of reassigning
Svelte: Using $effect for derived valuesUnnecessary side effectsUse $derived for pure computations
Svelte: Forgetting $stateVariables aren't reactive; UI doesn't updateAlways use $state for mutable values that affect UI
Both: Premature optimizationAdded complexity without measurable benefitProfile first; optimize bottlenecks, not everything

Performance Optimization

Benchmark: TodoMVC (1000 Items)

OperationReact 18 (ms)Svelte 5 (ms)Winner
Initial render6842Svelte (+38%)
Add 1 item2.10.3Svelte (+86%)
Toggle 1 item1.80.2Svelte (+89%)
Delete 1 item2.40.4Svelte (+83%)
Toggle all12.38.7Svelte (+29%)
Filter switch3.21.1Svelte (+66%)
Memory (10k items)24MB8MBSvelte (+67%)

The performance gap is most significant for single-item updates, where React must re-execute the component function and diff the virtual DOM, while Svelte only updates the specific DOM element.

When React Wins

React's virtual DOM approach can be advantageous for:

  • Large tree updates: When many elements change simultaneously, React's batched updates can be more efficient than many individual signal propagations
  • Server-side rendering: React's streaming SSR with Suspense is more mature
  • Concurrent features: React 18's concurrent rendering enables features like transitions and deferred values

When Svelte 5 Wins

Svelte 5's signal approach excels for:

  • Frequent small updates: Dashboards, real-time data, animations
  • Large static portions: Components where most of the UI doesn't change
  • Memory-constrained environments: Mobile devices, embedded web views
  • Bundle size: ~2KB vs ~42KB runtime makes a difference for initial load

Advanced Patterns

Custom Hooks vs Composable Runes

React Custom Hook

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);
 
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
 
    return () => clearTimeout(handler);
  }, [value, delay]);
 
  return debouncedValue;
}
 
// Usage
function SearchInput() {
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 300);
 
  useEffect(() => {
    if (debouncedSearch) {
      fetchResults(debouncedSearch);
    }
  }, [debouncedSearch]);
 
  return <input value={search} onChange={e => setSearch(e.target.value)} />;
}

Svelte 5 Composable Rune

// src/lib/runes/useDebounce.svelte.ts
export function useDebounce<T>(value: () => T, delay: number) {
  let debounced = $state(value());
 
  $effect(() => {
    const timeout = setTimeout(() => {
      debounced = value();
    }, delay);
    return () => clearTimeout(timeout);
  });
 
  return { get current() { return debounced; } };
}
 
// Usage
<script lang="ts">
  import { useDebounce } from '$lib/runes/useDebounce.svelte';
 
  let search = $state('');
  const debouncedSearch = useDebounce(() => search, 300);
 
  $effect(() => {
    if (debouncedSearch.current) {
      fetchResults(debouncedSearch.current);
    }
  });
</script>
 
<input bind:value={search} />

Testing Strategies

React Testing

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 
function renderWithProviders(ui: React.ReactElement) {
  const queryClient = new QueryClient();
  return render(
    <QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
  );
}
 
test('filters products by search', async () => {
  renderWithProviders(<ProductList />);
 
  await waitFor(() => {
    expect(screen.getByText('Laptop')).toBeInTheDocument();
  });
 
  fireEvent.change(screen.getByPlaceholderText('Search...'), {
    target: { value: 'phone' }
  });
 
  expect(screen.queryByText('Laptop')).not.toBeInTheDocument();
  expect(screen.getByText('iPhone')).toBeInTheDocument();
});

Svelte 5 Testing

import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import ProductList from './ProductList.svelte';
 
describe('ProductList', () => {
  it('filters products by search', async () => {
    render(ProductList);
 
    await screen.findByText('Laptop');
 
    await fireEvent.input(screen.getByPlaceholderText('Search...'), {
      target: { value: 'phone' }
    });
 
    expect(screen.queryByText('Laptop')).not.toBeInTheDocument();
    expect(screen.getByText('iPhone')).toBeInTheDocument();
  });
});

Svelte 5's testing is simpler—no providers needed for state management since runes work without wrappers.

Future Outlook

Both frameworks are evolving toward similar goals:

  • React is exploring compiler optimizations (React Forget/React Compiler) that automatically memoize components, reducing the re-render problem
  • Svelte is expanding runes' capabilities with potential server components and streaming SSR improvements

The convergence suggests that the industry is moving toward signal-based reactivity as the optimal model for UI development. React's compiler work acknowledges that manual memoization is a poor developer experience, while Svelte's explicit runes acknowledge that fully implicit reactivity leads to confusion.

Conclusion

Svelte 5 runes and React hooks represent two philosophies of reactive UI development. React prioritizes a simple mental model (functions that re-execute) at the cost of runtime performance and boilerplate (memoization, dependency arrays). Svelte 5 prioritizes runtime performance and developer ergonomics (automatic dependency tracking, surgical updates) at the cost of requiring a compiler.

Key takeaways:

  1. Svelte 5 eliminates memoization boilerplate: $derived is always memoized; no useMemo or useCallback needed
  2. React's ecosystem is more mature: More libraries, more community resources, more hiring options
  3. Svelte 5 performs better for frequent updates: Signals update only affected DOM elements; React re-executes entire components
  4. Stale closures are a React-only problem: Svelte 5's signal architecture makes them impossible
  5. Bundle size favors Svelte: ~2KB vs ~42KB runtime makes a meaningful difference for initial load
  6. Both frameworks are converging: React is adding compiler optimizations; Svelte is adding explicitness

For new projects in 2024, the choice depends on priorities: choose React for ecosystem maturity and team familiarity, choose Svelte 5 for performance and developer experience. Both are excellent frameworks for building production applications.

Consult the Svelte 5 docs, React docs, and the framework comparison on Frontend Masters for deeper exploration.