上回我吐槽了Next.js和SSR的“花活”很多人说我是“因爱生恨”。但今天我要聊一个真正让我夜不能寐、每天写代码都想砸键盘的核心痛点——代码混杂。什么是代码混杂就是在一个.tsx文件里服务端的逻辑和客户端的逻辑像两团被猫玩过的毛线缠在一起剪不断理还乱。Next.js 美其名曰“全栈框架”、“前后端融合”在我看来这根本就是一场心智负担的灾难。一、同一个文件两个世界让我们来看一个“经典”的Next.js页面tsx// pages/profile/[id].tsx // 这行代码是在哪里执行 export async function getServerSideProps(context) { const userId context.params.id; // 这是服务器端的秘密里面有数据库密码 const dbSecret process.env.DATABASE_PASSWORD; // 去数据库捞数据... const userData await db.query(SELECT * FROM users WHERE id ?, [userId]); return { props: { user: userData } }; } // 这个组件又是在哪里执行 export default function UserProfile({ user }) { // 这看起来是客户端代码... const [isFollowed, setIsFollowed] useState(false); // 但是这个 useEffect 在服务端也跑了一次 useEffect(() { // 只能客户端做的事 console.log(窗口宽度:, window.innerWidth); // 服务端直接报错 }, []); const handleFollow async () { // 这又是在哪里客户端因为有点击事件 const res await fetch(/api/follow, { method: POST }); // ... }; return ( div h1{user.name}/h1 button onClick{handleFollow} {isFollowed ? 已关注 : 关注} /button /div ); }看到没有一个文件里服务器端的逻辑getServerSideProps、客户端的逻辑useState, useEffect, onClick、以及一些暧昧不清、两头都跑的代码组件的render全挤在一块。我每次打开这种文件脑子里都要启动一次“双核处理器”左脑这段代码在Node.js环境跑有process.env有数据库连接。右脑这段代码在浏览器环境跑有window有document。问题是我的脑子不是双核的啊二、“水合”灾难两边都得伺候的祖宗Next.js 最引以为傲的“水合”Hydration恰恰是代码混杂最恶心的体现。你的组件代码先在服务器上跑一遍生成HTML字符串。然后到了浏览器再跑一遍给这个HTML“注入生命”。这就导致了一个致命问题你写的每一行组件代码都必须同时服务端和客户端“两开花”。你一不小心在组件顶层用了一个localStorage.getItem(token)好了服务端直接报错——“localStorage is not defined”。你小心翼翼地加了个typeof window ! undefined的判断代码变得丑陋不堪。更恶心的是就算你处理好了报错水合不匹配Hydration Mismatch又会找上门。服务器渲染的HTML和客户端第一次渲染的Virtual DOM对不上React就给你报警告甚至直接重渲染把你“SSR优化”带来的那点性能优势全部葬送。我写的是React不是兼容性测试工具三、“我到底该把逻辑放哪”——一个哲学问题在Next.js里你无时无刻不在面临这种灵魂拷问这个数据获取逻辑放在getServerSideProps里还是放在useEffect里用fetch还是放在getStaticProps里构建时拿还是用SWR在客户端“重新验证”这个环境变量是NEXT_PUBLIC_开头的吗不是的话它只存在于服务端客户端用不了。是的话它又会被打包进客户端的JS里万一里面有敏感信息呢这个工具函数它用了fs模块读文件又用了fetch请求API。它到底是一个“服务端工具”还是“客户端工具”要不要加个use client指令我每天不是在写业务逻辑我是在给代码做“环境归属鉴定”。这种心智负担不是来自于技术的复杂性而是来自于框架设计本身对“关注点分离”原则的背叛。四、所谓的“全栈”不过是“全混”有人说“你可以在/pages/api里写后端逻辑在/pages里写页面逻辑这不就分开了吗”这更是个笑话。pages/api里的代码本质上就是一个个Serverless函数。但你依然要在同一个项目里跟你的UI代码共享node_modules共享tsconfig.json。你的前端代码可以轻易地import一个原本只想在getServerSideProps里用的工具函数然后发现它在客户端打包时引用了crypto核心模块直接导致你的页面体积暴涨。Next.js 的“全栈”不是前后端分离而是前后端“大乱炖”。它用一个模糊的边界吞噬了开发者对代码执行环境的清晰认知。五、我只是个前端我不想当“全栈背锅侠”我真的只是想写一个漂亮的UI处理一些用户的点击交互。但现在我被迫要关心Node.js 的内存限制数据库连接池的配置Serverless 函数的冷启动时间API 路由的 CORS 问题服务端渲染的缓存策略我只是个前端啊我当初入这行是因为我喜欢视觉反馈、喜欢交互、喜欢用代码创造界面。我不是来给运维和后端擦屁股的。当我把一个useState写错了导致整个页面的SSR崩溃返回一个500错误页面时我真的想仰天长啸“我特么只是想加个按钮的状态啊”结语把“关注点分离”还给我所以亲爱的Next.js求求你了。别再跟我吹什么“全栈”、“统一”、“融合”了。我怀念的是那个前端代码只跑在浏览器的时代。我怀念的是那个后端接口明明白白写在另一个项目里的时代。我怀念的是那个src/下面是客户端server/下面是服务端井水不犯河水的时代。清晰的边界让团队可以分工协作让新人可以快速上手让我的脑子可以不用时刻切换上下文。你问我为什么还在用因为老板看了营销文说“大家都在用”因为技术选型不是我能决定的。但至少请让我在代码里保留最后一声呐喊别再让我在一个文件里同时写服务器和客户端的代码了我的大脑它真的会过载
狠狠吐槽一下SSR/Next.js(二):服务端和客户端代码缠在一起,剪不断,理还乱。
发布时间:2026/7/8 10:01:59
上回我吐槽了Next.js和SSR的“花活”很多人说我是“因爱生恨”。但今天我要聊一个真正让我夜不能寐、每天写代码都想砸键盘的核心痛点——代码混杂。什么是代码混杂就是在一个.tsx文件里服务端的逻辑和客户端的逻辑像两团被猫玩过的毛线缠在一起剪不断理还乱。Next.js 美其名曰“全栈框架”、“前后端融合”在我看来这根本就是一场心智负担的灾难。一、同一个文件两个世界让我们来看一个“经典”的Next.js页面tsx// pages/profile/[id].tsx // 这行代码是在哪里执行 export async function getServerSideProps(context) { const userId context.params.id; // 这是服务器端的秘密里面有数据库密码 const dbSecret process.env.DATABASE_PASSWORD; // 去数据库捞数据... const userData await db.query(SELECT * FROM users WHERE id ?, [userId]); return { props: { user: userData } }; } // 这个组件又是在哪里执行 export default function UserProfile({ user }) { // 这看起来是客户端代码... const [isFollowed, setIsFollowed] useState(false); // 但是这个 useEffect 在服务端也跑了一次 useEffect(() { // 只能客户端做的事 console.log(窗口宽度:, window.innerWidth); // 服务端直接报错 }, []); const handleFollow async () { // 这又是在哪里客户端因为有点击事件 const res await fetch(/api/follow, { method: POST }); // ... }; return ( div h1{user.name}/h1 button onClick{handleFollow} {isFollowed ? 已关注 : 关注} /button /div ); }看到没有一个文件里服务器端的逻辑getServerSideProps、客户端的逻辑useState, useEffect, onClick、以及一些暧昧不清、两头都跑的代码组件的render全挤在一块。我每次打开这种文件脑子里都要启动一次“双核处理器”左脑这段代码在Node.js环境跑有process.env有数据库连接。右脑这段代码在浏览器环境跑有window有document。问题是我的脑子不是双核的啊二、“水合”灾难两边都得伺候的祖宗Next.js 最引以为傲的“水合”Hydration恰恰是代码混杂最恶心的体现。你的组件代码先在服务器上跑一遍生成HTML字符串。然后到了浏览器再跑一遍给这个HTML“注入生命”。这就导致了一个致命问题你写的每一行组件代码都必须同时服务端和客户端“两开花”。你一不小心在组件顶层用了一个localStorage.getItem(token)好了服务端直接报错——“localStorage is not defined”。你小心翼翼地加了个typeof window ! undefined的判断代码变得丑陋不堪。更恶心的是就算你处理好了报错水合不匹配Hydration Mismatch又会找上门。服务器渲染的HTML和客户端第一次渲染的Virtual DOM对不上React就给你报警告甚至直接重渲染把你“SSR优化”带来的那点性能优势全部葬送。我写的是React不是兼容性测试工具三、“我到底该把逻辑放哪”——一个哲学问题在Next.js里你无时无刻不在面临这种灵魂拷问这个数据获取逻辑放在getServerSideProps里还是放在useEffect里用fetch还是放在getStaticProps里构建时拿还是用SWR在客户端“重新验证”这个环境变量是NEXT_PUBLIC_开头的吗不是的话它只存在于服务端客户端用不了。是的话它又会被打包进客户端的JS里万一里面有敏感信息呢这个工具函数它用了fs模块读文件又用了fetch请求API。它到底是一个“服务端工具”还是“客户端工具”要不要加个use client指令我每天不是在写业务逻辑我是在给代码做“环境归属鉴定”。这种心智负担不是来自于技术的复杂性而是来自于框架设计本身对“关注点分离”原则的背叛。四、所谓的“全栈”不过是“全混”有人说“你可以在/pages/api里写后端逻辑在/pages里写页面逻辑这不就分开了吗”这更是个笑话。pages/api里的代码本质上就是一个个Serverless函数。但你依然要在同一个项目里跟你的UI代码共享node_modules共享tsconfig.json。你的前端代码可以轻易地import一个原本只想在getServerSideProps里用的工具函数然后发现它在客户端打包时引用了crypto核心模块直接导致你的页面体积暴涨。Next.js 的“全栈”不是前后端分离而是前后端“大乱炖”。它用一个模糊的边界吞噬了开发者对代码执行环境的清晰认知。五、我只是个前端我不想当“全栈背锅侠”我真的只是想写一个漂亮的UI处理一些用户的点击交互。但现在我被迫要关心Node.js 的内存限制数据库连接池的配置Serverless 函数的冷启动时间API 路由的 CORS 问题服务端渲染的缓存策略我只是个前端啊我当初入这行是因为我喜欢视觉反馈、喜欢交互、喜欢用代码创造界面。我不是来给运维和后端擦屁股的。当我把一个useState写错了导致整个页面的SSR崩溃返回一个500错误页面时我真的想仰天长啸“我特么只是想加个按钮的状态啊”结语把“关注点分离”还给我所以亲爱的Next.js求求你了。别再跟我吹什么“全栈”、“统一”、“融合”了。我怀念的是那个前端代码只跑在浏览器的时代。我怀念的是那个后端接口明明白白写在另一个项目里的时代。我怀念的是那个src/下面是客户端server/下面是服务端井水不犯河水的时代。清晰的边界让团队可以分工协作让新人可以快速上手让我的脑子可以不用时刻切换上下文。你问我为什么还在用因为老板看了营销文说“大家都在用”因为技术选型不是我能决定的。但至少请让我在代码里保留最后一声呐喊别再让我在一个文件里同时写服务器和客户端的代码了我的大脑它真的会过载