Next.js 16 与 Remix 对比
Next.js 16(默认启用 Turbopack、支持部分预渲染)与 Remix(Shopify 出品,注重 Web 标准)——2026 年 React 元框架对比。
在服务端预渲染、默认零 JS 的 React 层
在浏览器中渲染、经典且成熟的 React 使用方式
在内容繁重、SEO 关键的场景中,RSC 带来可衡量的收益:react.dev 的示例中,75K gzip 大小的库完全不会进入客户端。在高交互的内部面板(仪表盘、后台管理)中,client-first 方案更简单,出错面也更小。一个真实存在的关键警告是:把完整对象作为 prop 从 Server 传递到 Client,会把该对象的所有字段都写入 flight payload;如果边界划错,收益会悄悄流失。请用测量数据做决定,而不是跟风。
| 分类 | React Server Components | Client-side React (SPA) |
|---|---|---|
| 性能 | 8/10 | 6/10 |
| 学习难易度 | 4/10 | 8/10 |
| 生态系统 | 7/10 | 9/10 |
| 社区 | 7/10 | 9/10 |
| 就业市场 | 7/10 | 8/10 |
| 面向未来 | 9/10 | 7/10 |
// Next.js App Router — Server Component(默认)
// app/posts/[slug]/page.tsx
import { db } from '@/lib/db';
import LikeButton from './like-button'; // Client Component(在单独文件中标记 'use client')
interface PageProps {
params: Promise<{ slug: string }>;
}
export default async function PostPage({ params }: PageProps) {
const { slug } = await params;
// 直接在服务端 await —— 没有 client-server 瀑布流,
// marked/sanitize-html 这类重量级库完全不会进入 client bundle
const post = await db.post.findUnique({ where: { slug } });
if (!post) return <div>未找到</div>;
return (
<article>
<h1>{post.title}</h1>
{/* 只有可序列化的 prop 才能传给 Client Component */}
<div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
<LikeButton postId={post.id} initialCount={post.likeCount} />
</article>
);
}
// app/posts/[slug]/like-button.tsx
'use client';
import { useState, useTransition } from 'react';
export default function LikeButton({ postId, initialCount }: { postId: string; initialCount: number }) {
const [count, setCount] = useState(initialCount);
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() => startTransition(async () => {
setCount((c) => c + 1);
await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
})}
>
点赞 ({count})
</button>
);
}// Vite + React — Client-side SPA
// src/pages/PostPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
interface Post {
id: string;
title: string;
renderedHtml: string;
likeCount: number;
}
export default function PostPage() {
const { slug } = useParams();
const [post, setPost] = useState<Post | null>(null);
const [isLiking, setIsLiking] = useState(false);
useEffect(() => {
// 所有数据获取都在客户端 —— 瀑布流风险从这里开始
let cancelled = false;
fetch(`/api/posts/${slug}`)
.then((res) => res.json())
.then((data) => { if (!cancelled) setPost(data); });
return () => { cancelled = true; };
}, [slug]);
if (!post) return <div>加载中...</div>; // JS 未运行时这个界面根本不会显示
async function handleLike() {
setIsLiking(true);
setPost((p) => (p ? { ...p, likeCount: p.likeCount + 1 } : p));
await fetch(`/api/posts/${post!.id}/like`, { method: 'POST' });
setIsLiking(false);
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
<button disabled={isLiking} onClick={handleLike}>
点赞 ({post.likeCount})
</button>
</article>
);
}
// vite.config.ts —— 代码分割交给 Rollup 处理
export default {
build: {
rollupOptions: {
output: { manualChunks: { vendor: ['react', 'react-dom', 'react-router'] } },
},
},
};在内容繁重、SEO 关键的场景中,RSC 带来可衡量的收益:react.dev 的示例中,75K gzip 大小的库完全不会进入客户端。在高交互的内部面板(仪表盘、后台管理)中,client-first 方案更简单,出错面也更小。一个真实存在的关键警告是:把完整对象作为 prop 从 Server 传递到 Client,会把该对象的所有字段都写入 flight payload;如果边界划错,收益会悄悄流失。请用测量数据做决定,而不是跟风。
获取免费咨询在内容繁重、SEO 关键、首屏加载(LCP/TTI)很重要的场景(博客、产品页、对比页)中,RSC 能带来可衡量的收益——服务端获取数据,客户端 JS 更少。根据 Next.js 16.3 官方数据,服务端渲染改用 Node.js 原生流之后,在高负载下可多处理 22% 的请求。在高交互的内部面板(仪表盘、后台管理)中,client-first 通常提供更简单的开发体验。