服务器端渲染(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>
封装任何您不想等待其所有数据加载的组件,并将您希望在加载时显示的组件放入回退
道具中即可。
函数 BlogPost() {
返回 (
<div> <Nav
<Nav /> <Sidebar
<Sidebar /> <Article
<Article /> <Article
<悬念 fallback={<CommentsSkeleton />}>
<评论 />
</悬念
</div> </div>
);
}
初始 HTML 将如下所示
<main> <!
<省略...-->
<section id="comments-spinner">
<!
<img width=400 src="spinner.gif" alt="加载中..." /> <!
</section
</main>
之后,当评论组件的数据在服务器上准备就绪时,我们将这样做
<div hidden id="comments">
<!
<p> 第一条评论</p
<p>第二条评论</p
</div> </div>
<script
// 此实现略有简化
document.getElementById('sections-spinner').replaceChildren(
document.getElementById('comments')
);
</script> </script>
这样,我们就不必为了在屏幕上显示某些内容而获取所有数据了。
改进水合
我们已经创建了初始 HTML,但在开始水合之前,我们仍需要加载所有 JavaScript。在这种情况下,我们可以使用代码分割和lazy
来隔离水合过程。
const Comments = lazy(() => import('./Comments'));
函数 BlogPost() {
返回
<div> <div
<Nav /> <Article
<Article />.
<悬念 fallback={<CommentsSkeleton />}>
<评论 /> <悬念
</悬念
</div> </div>
);
}
除注释部分外,其余 JavaScript 加载完成后,水合将开始。稍后,当加载完评论组件中的所有 JavaScript 后,评论也将水合。
下面是到目前为止的流程概览
可触发暂停的条件
并不是每一种数据加载都会触发暂停,那么哪些数据源与暂停兼容呢?
- 使用
use
钩子获取数据
import { use } from 'react';
// 获取数据的函数
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(res => res.json());
}
function UserProfile({ userId }) {
const user = use(fetchUserData(userId));
返回 (
<div> <h1>{user.name}</h1
<h1>{user.name}</h1> <p>{user.bio}</p
<p>{user.bio}</p>
</div> </div
);
}
// 用悬念进行包装
function App() {
返回 (
<Suspense fallback={<Loading />}>
<UserProfile userId={1} /> </Suspense
</Suspense> </Suspense
);
}
- 懒惰组件
import { lazy, Suspense } from 'react';
// 动态加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
返回 (
<div> <div>我的应用程序</div
<h1>我的应用程序</h1
<悬念 fallback={<Loading />}>
<HeavyComponent />
</Suspense
</div> </div>
);
}
- // 服务器组件的异步
// 服务器组件
async 函数 BlogPost({ id }) {
const post = await fetchBlogPost(id);
返回 (
<文章
<h1>{post.title}</h1> <p>{post.content}</p> <article
<p>{post.content}</p>
</article> </article
);
}
function BlogPage({ id }) {
返回 (
<悬念 fallback={<LoadingPost />}>
<BlogPost id={id} />
</Suspense> <Suspense
);
}
使用延迟值
function SearchResults({ query }) {
// 推迟对搜索查询的更改
const deferredQuery = useDeferredValue(query);
返回 (
<Suspense fallback={<Loading />}>
<搜索结果列表 query={deferredQuery} />
</Suspense
);
}
不触发暂停时
另一方面,以下常见的数据获取模式不会触发暂停状态
useEffect
内的数据修补
// ❌ 不会触发暂停状态
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUserData(userId).then(setUser);
}, [userId]);
if (!user) 返回 <Loading />;
返回 <h1>{user.name}</h1>;
}
- 正常 Promise 处理。
// ❌ 不会触发暂停
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 />;
返回 <h1>{user.name}</h1>;
}
使用 Tanstack 查询时
时,请确保使用 useSuspenseQuery
而不是useQuery
。
请参见