服务器端渲染(SSR)的局限性
服务器端渲染(SSR)在初始加载性能和搜索引擎优化方面有很大的优势,但它也有一些明显的局限性。在本文中,我们将了解传统 SSR 的一些问题,并学习 React 18 中为解决这些问题而引入的 Suspense。
1. 数据获取的阻塞问题
SSR 最大的问题之一就是需要一次性从服务器获取所有数据。在将组件树呈现为 HTML 之前,您需要获得该页面所需的所有数据。
例如,博文页面由以下组件组成
- 导航栏
- 侧边栏
- 帖子内容
- 评论区
如果获取评论数据需要很长时间,开发人员有以下选择
-
将评论排除在服务器渲染之外
- 优点:可以快速显示其他组件
- 缺点:在加载 JavaScript 之前,用户无法看到评论
-
用注释渲染服务器
- 优点:评论包含在初始 HTML 中
- 缺点:在等待注释数据时,整个页面的渲染会延迟
2. 水合问题
SSR 的第二个大问题与水合过程有关。为了将服务器上生成的 HTML 与客户端上的组件树相匹配,React 必须等待所有组件的 JavaScript 代码加载完毕。
React 的水合过程也存在问题。目前,React 会一次性水合整个组件树,这会导致以下问题
-
在整个组件树水合之前,您无法与任何组件交互
-
性能问题,尤其是在低端设备上
- 如果 JavaScript 逻辑较多,屏幕会冻结
- 用户即使想转到其他页面,也必须等待水合完成
3. 瀑布流程的局限性
造成这些问题的根本原因是 SSR 遵循了以下有序的 "瀑布式 "流程
- 获取数据(服务器)
- HTML 渲染(服务器)
- JavaScript 代码加载(客户端)
- 水合(客户端)
每个步骤只有在前一个步骤完全完成后才能开始,这就降低了整体性能和用户体验。
新的解决方案--悬念
为了解决这些问题,React 18 引入了 "暂停 "功能。有了 Suspense,应用程序的不同部分可以分别加载和水合,从而有效解决上述问题。
如何使用
使用 Suspense 非常简单。只需用<Suspense>
封装您不想等待所有数据加载的组件,并将您想在加载时显示的组件放在fallback
props 中即可。
function BlogPost() {
return (
<div>
<Nav />
<Sidebar />
<Article />
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}
初始 HTML 页面如下所示
<main>
<!-- 생략... -->
<section id="comments-spinner">
<!-- Spinner -->
<img width=400 src="spinner.gif" alt="Loading..." />
</section>
</main>
之后,一旦服务器上的注释组件数据准备就绪,它就会经历以下过程
<div hidden id="comments">
<!-- Comments -->
<p>First comment</p>
<p>Second comment</p>
</div>
<script>
// This implementation is slightly simplified
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments')
);
</script>
这样,我们就不必为了在屏幕上显示某些内容而获取所有数据了。
改进水合
我们已经创建了初始 HTML,但在开始水合之前,我们仍需要加载所有 JavaScript。在这种情况下,可以通过lazy
进行代码拆分,将水合过程分割开来。
const Comments = lazy(() => import('./Comments'));
function BlogPost() {
return (
<div>
<Nav />
<Article />
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}
除注释部分外,水合过程在 JavaScript 的其他部分加载完成后开始。稍后,当加载完评论组件中的所有 JavaScript 后,评论也将水合。
下面是到目前为止的流程概览
可触发暂停的条件
并不是每一种数据加载都会触发 "暂停",那么哪些数据源与 "暂停 "兼容呢?
-
use
使用钩子获取数据javascriptimport { use } from 'react'; // 데이터를 가져오는 함수 function fetchUserData(userId) { return fetch(`/api/users/${userId}`) .then(res => res.json()); } function UserProfile({ userId }) { const user = use(fetchUserData(userId)); return ( <div> <h1>{user.name}</h1> <p>{user.bio}</p> </div> ); } // Suspense로 래핑 function App() { return ( <Suspense fallback={<Loading />}> <UserProfile userId={1} /> </Suspense> ); }
-
懒惰组件
javascriptimport { lazy, Suspense } from 'react'; // 동적으로 로드되는 컴포넌트 const HeavyComponent = lazy(() => import('./HeavyComponent')); function App() { return ( <div> <h1>My App</h1> <Suspense fallback={<Loading />}> <HeavyComponent /> </Suspense> </div> ); }
-
服务器组件中的异步
javascript// 서버 컴포넌트 async function BlogPost({ id }) { const post = await fetchBlogPost(id); return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> </article> ); } function BlogPage({ id }) { return ( <Suspense fallback={<LoadingPost />}> <BlogPost id={id} /> </Suspense> ); }
-
useDeferredValue
在服务器组件中javascriptfunction SearchResults({ query }) { // 검색어 변경을 지연시켜 처리 const deferredQuery = useDeferredValue(query); return ( <Suspense fallback={<Loading />}> <SearchResultsList query={deferredQuery} /> </Suspense> ); }
不触发暂停时
另一方面,以下常见数据提取模式不会触发暂停状态
-
useEffect
从内部修补数据javascript// ❌ Suspense를 발동시키지 않음 function UserProfile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetchUserData(userId).then(setUser); }, [userId]); if (!user) return <Loading />; return <h1>{user.name}</h1>; }
-
通用承诺处理
javascript// ❌ Suspense를 발동시키지 않음 function UserProfile({ userId }) { const [user, setUser] = useState(null); const [error, setError] = useState(null); Promise.resolve(fetchUserData(userId)) .then(setUser) .catch(setError); if (error) return <Error error={error} />; if (!user) return <Loading />; return <h1>{user.name}</h1>; }
使用 Tanstack 查询时
必须使用useSuspenseQuery
,而不是之前使用的useQuery
。
另请参见