Next.js中Server Actions为何串行执行?寻求并行配置方案
Next.js Server Actions 并行调用被排队的问题与解决
问题背景
在Next.js应用中,从客户端组件触发多个Server Actions调用时,发现这些请求会被服务器按顺序排队执行,总耗时为单次请求的N倍(比如5次请求耗时25秒而非5秒),即便客户端用异步Promise方式发起调用。
通过复现代码验证了该现象:
- 点击"Multiple Server Requests"按钮(同时调用两个Server Actions),服务器日志显示两次请求依次执行,总耗时10秒;
- 点击"Single Server Requests"按钮(单次请求中在服务器内部并行调用两个函数),则两个函数同时执行,总耗时5秒。
复现代码
serverFunctions.js
"use server" // 模拟5秒延迟的Server Action export async function ServerDelay(index) { console.log(`calling ${index} ` + new Date().toLocaleTimeString()) return new Promise(resolve => { setTimeout(() => { resolve({'index': index, 'time': new Date().toLocaleTimeString()}) }, 5000) }) } // 服务器内部并行调用的示例 export async function launchOnServer() { ServerDelay(1).then(x => {console.log(x)}) ServerDelay(2).then(x => {console.log(x)}) }
page.js
"use client" import {ServerDelay, launchOnServer} from "./serverFunctions"; // 客户端本地延迟函数(对比用) export async function ClientDelay(index) { console.log(`calling ${index} ` + new Date().toLocaleTimeString()) return new Promise(resolve => { setTimeout(() => { resolve({'index': index, 'time': new Date().toLocaleTimeString()}) }, 5000) }) } // 客户端同时发起两个Server Actions请求 function launchSeparately() { ServerDelay(1).then(x => {console.log(x)}) ServerDelay(2).then(x => {console.log(x)}) } // 客户端本地并行调用(对比用) function clientLaunch() { ClientDelay(1).then(x => {console.log(x)}) ClientDelay(2).then(x => {console.log(x)}) } // 客户端发起单次请求,服务器内部并行处理 function launchBoth() { launchOnServer() } export default function Home() { return ( <div> <button onClick={launchSeparately}>Multiple Server Requests</button><br/><br/> <button onClick={launchBoth}>Single Server Requests</button><br/><br/> <button onClick={clientLaunch}>Client Request</button> </div> ) }
问题原因
Next.js默认对同一客户端会话的Server Actions请求启用会话锁定:当一个请求处理时,会话Cookie会被锁定,后续同会话的请求必须等待前一个请求完成才能执行。这是为了避免并发修改会话数据(如用户会话状态)导致的一致性问题,但同时也限制了无会话依赖的Server Actions的并行执行。
解决方案
方案1:禁用会话锁定(适合无会话依赖的场景)
如果你的Server Action不需要读取或修改会话数据(如不需要访问cookies()、headers()或用户会话),可以在函数中添加unstable_noStore(Next.js 13.4+)来禁用会话锁定,允许并行执行:
"use server" import { unstable_noStore as noStore } from 'next/cache' export async function ServerDelay(index) { noStore() // 禁用会话锁定,允许并行执行 console.log(`calling ${index} ` + new Date().toLocaleTimeString()) return new Promise(resolve => { setTimeout(() => { resolve({'index': index, 'time': new Date().toLocaleTimeString()}) }, 5000) }) }
修改后,点击"Multiple Server Requests"按钮,服务器日志会显示两个请求同时启动,总耗时约5秒:
calling 1 10:20:00 calling 2 10:20:00 POST / 200 in 5012ms POST / 200 in 5015ms
方案2:批量请求(推荐,更高效)
将多个请求的参数打包成一个数组,在单个Server Action中内部并行处理所有子请求,这种方式只发起一次网络请求,避免了会话锁定限制,同时减少网络开销:
优化后的serverFunctions.js
"use server" async function ServerDelay(index) { console.log(`calling ${index} ` + new Date().toLocaleTimeString()) return new Promise(resolve => { setTimeout(() => { resolve({'index': index, 'time': new Date().toLocaleTimeString()}) }, 5000) }) } // 批量处理多个请求的Server Action export async function BatchServerDelays(indices) { // 并行执行所有延迟请求 const results = await Promise.all(indices.map(index => ServerDelay(index))) return results }
客户端调用修改
"use client" import { BatchServerDelays } from "./serverFunctions"; function launchBatch() { // 发起批量请求,传入需要处理的索引数组 BatchServerDelays([1,2,3,4,5]).then(results => { console.log(results) }) } export default function Home() { return ( <div> <button onClick={launchBatch}>Batch Server Requests</button><br/><br/> {/* 其他按钮... */} </div> ) }
注意事项
- 使用
unstable_noStore时,必须确保Server Action不依赖会话数据,否则可能导致数据不一致; - 批量请求的方式更符合Server Actions的设计初衷,减少客户端与服务器的交互次数,性能更优;
- 若你的Server Action必须访问会话数据,建议使用批量请求方案,避免会话锁定导致的排队问题。
内容的提问来源于stack exchange,提问作者Rich S
相关产品推荐
相关产品推荐

