一、真实场景:一个让我失眠的React项目
2024年11月,我接手了一个内部工单管理系统。React 16 + Redux + React Router,2.4万行代码。项目跑起来浏览器标签页占用内存1.2GB。打包产物gzip后285KB,首屏在低端机器上要3秒以上。用户天天抱怨,产品经理天天催。
我用Chrome Performance面板测了性能数据,单次渲染500条工单列表需要640ms。什么概念?用户每点一次筛选按钮,页面白屏半秒以上。这是React 16的典型症状——父组件setState后整棵子树重新协调。
我当时的选项有三个:继续用React但全面重构到React 18/19;迁移到Vue 3;换用Svelte 5。我用一个调研工单卡片组件做了对比验证。下面是我实测的全过程。
二、方案对比:为什么最终选了Svelte
| 维度 | React 18.3.1 | Vue 3.5.12 | Svelte 5.1.9 |
|---|---|---|---|
| 运行时体积(gzip) | 约42KB(react+react-dom) | 约34KB(vue 完整版) | 约12KB(不含编译器) |
| 响应式原理 | 虚拟DOM + 协调 | 响应式代理(Proxy) | 编译时处理器+信号(signals) |
| 学习成本 | 中(hooks规则多) | 低(模板语法直观) | 低(类似Vue但更少API) |
| 细粒度更新 | 否,组件级 | 是,组件级+细粒度 | 是,变量级 |
| TypeScript支持 | 优秀 | 良好 | 优秀(原生支持) |
| 构建工具 | Vite 6.0.3 | Vite 6.0.3 | Vite 6.0.3(官方推荐) |
关键差距在运行时。React和Vue都在浏览器里做了大量工作:React的Fiber协调机制,Vue的Proxy劫持。Svelte 5把编译器放到构建阶段,直接生成原生DOM操作代码,没有虚拟DOM这一层。
Svelte 5最核心的变化是引入了runes——一组编译器层面的响应式原语。这不是又一个状态管理库,它是Svelte语言的组成部分。$state、$derived、$effect这些符号直接进编译器。
三、同一个组件,三套代码的实录
3.1 需求描述
一个工单卡片组件,功能:
- 展示工单标题、状态、优先级
- 点击「处理」按钮后工单状态变为「处理中」
- 点击「完成」按钮后状态变为「已完成」并显示完成时间
- 当工单优先级为「高」时,卡片左侧显示红色竖条
3.2 React 18 实现(使用函数组件和hooks)
// React 18.3.1, 使用 TypeScript 5.6.2
import React, { useState, useCallback } from 'react';
type TicketStatus = 'pending' | 'processing' | 'done';
type Priority = 'low' | 'medium' | 'high';
interface TicketCardProps {
title: string;
priority: Priority;
}
export default function TicketCard({ title, priority }: TicketCardProps) {
const [status, setStatus] = useState('pending');
const [completedAt, setCompletedAt] = useState(null);
const handleProcess = useCallback(() => {
setStatus('processing');
}, []);
const handleComplete = useCallback(() => {
setStatus('done');
setCompletedAt(new Date().toLocaleString('zh-CN'));
}, []);
return (
<div className={`ticket-card priority-${priority}`}>
{priority === 'high' && <span className="high-indicator" />}
<h3>{title}</h3>
<p>状态:{status}</p>
{status === 'done' && completedAt ? (
<p>完成时间:{completedAt}</p>
) : (
<div className="actions">
<button onClick={handleProcess} disabled={status !== 'pending'}>
处理
</button>
<button onClick={handleComplete} disabled={status === 'done'}>
完成
</button>
</div>
)}
</div>
);
}