Next.js生产环境Suspense阻塞,无法显示fallback UI如何解决?
问题描述
我按照Next.js文档实现流式处理,为在开发环境可视化fallback加载效果,给API调用添加了4秒人工延迟。开发环境中能正常显示Loading...,但生产环境下页面会被阻塞4秒,直到数据完全加载完成才显示内容,Suspense的fallback完全不生效,改用loading.js页面也存在同样问题。
相关代码
app/dashboard/page.js:
import { Suspense } from 'React' import TestComponent from './testComponent' const Dashboard = () => { return ( <div> <Suspense fallback={<div>Loading...</div>}> <TestComponent /> </Suspense> </div> ) } export default Dashboard
app/dashboard/testComponent.js:
const TestComponent = async () => { const fetchDataWithDelay = () => { const res = await fetch("https://jsonplaceholder.typicode.com/posts", { cache: "no-store", }); const data = await res.json(); return new Promise((resolve) => { setTimeout(() => { resolve(data); }, 4000); }); } const data = await fetchDataWithDelay(); return ( <ul> {data.map((d)=>( <li key={d.id}> <p>{d.title}</p> <p>{d.body}</p> </li> ))} </ul> ) } export default TestComponent
问题原因
在Next.js App Router中,async组件的代码默认在服务器端执行。你把setTimeout放在fetch请求完成之后,意味着服务器会先等待API请求返回,再额外等待4秒才会把组件的HTML发送给客户端——这直接阻塞了流式渲染,服务器根本不会提前返回Suspense的fallback内容,而是等所有逻辑跑完才输出完整页面。
解决方案
方案1:将延迟逻辑移到客户端(推荐)
既然是为了测试fallback效果,应该让服务器先返回页面框架,客户端再触发数据请求和延迟渲染。给组件加上'use client'指令,把数据获取逻辑移到客户端:
// app/dashboard/testComponent.js 'use client'; import { useEffect, useState } from 'react'; const TestComponent = () => { const [data, setData] = useState(null); useEffect(() => { const fetchDataWithDelay = async () => { const res = await fetch("https://jsonplaceholder.typicode.com/posts", { cache: "no-store", }); const fetchedData = await res.json(); setTimeout(() => { setData(fetchedData); }, 4000); }; fetchDataWithDelay(); }, []); if (!data) return <div>Loading...</div>; return ( <ul> {data.map((d)=>( <li key={d.id}> <p>{d.title}</p> <p>{d.body}</p> </li> ))} </ul> ) } export default TestComponent;
修改后,服务器会先返回包含Suspense fallback的页面结构,客户端加载后再发起请求并延迟渲染数据,生产环境就能正常显示loading状态。
方案2:服务器端正确触发Suspense的方式(仅用于测试)
如果必须在服务器端模拟延迟,需要让组件在渲染时立即触发pending状态的Promise,从而激活Suspense:
// app/dashboard/testComponent.js const fetchDataWithDelay = async () => { const res = await fetch("https://jsonplaceholder.typicode.com/posts", { cache: "no-store", }); const data = await res.json(); await new Promise(resolve => setTimeout(resolve, 4000)); return data; }; // 提前创建处于pending状态的Promise const dataPromise = fetchDataWithDelay(); const TestComponent = async () => { const data = await dataPromise; return ( <ul> {data.map((d)=>( <li key={d.id}> <p>{d.title}</p> <p>{d.body}</p> </li> ))} </ul> ) } export default TestComponent;
关键是把Promise的创建放在组件外部,让组件在初始化渲染时就await这个pending的Promise,触发Suspense的fallback逻辑。
补充说明
生产环境的真实场景不会添加这种人工延迟,因此方案1更贴近实际开发需求,既满足测试fallback的需求,也符合客户端数据获取的最佳实践。
内容的提问来源于stack exchange,提问作者frost2709

