渐进式渲染怎么做才能不闪屏?

迷人的国凤 阅读 19

我试了用流式 SSR 返回 HTML,但首屏内容先显示骨架屏,等 JS 加载完又整个替换成真实内容,明显闪了一下,体验很不好。是不是应该让服务端直接返回部分真实数据?

现在服务端只返回空容器:<div id="app"></div>,然后前端 hydration 全靠客户端 JS。有没有办法让服务端先吐出首屏关键内容,再逐步 hydrate?

我来解答 赞 7 收藏
二维码
手机扫码查看
2 条解答
英瑞🍀
你这问题我之前也踩过坑,说白了就是你现在的做法根本不算真正的 SSR,充其量就是个「占位符」。

问题出在哪呢,服务端只吐了一个空 <div id="app"></div>,那首屏渲染的压力全在客户端 JS 身上。骨架屏消失、真实内容出来,这中间肯定会有个视觉跳变,因为两者是串行的,不是无缝衔接的。

正确的渐进式渲染应该是这样的:

服务端要真正渲染首屏内容,而不是空壳。用 React 举例,如果你用的是 React 18,配合 renderToPipeableStream 或者 renderToReadableStream,配合 Suspense 就能实现真正的流式 SSR。

大概的思路是这样,服务端:

import { renderToPipeableStream } from 'react-dom/server';

app.get('*', (req, res) => {
const { pipe } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/client.js'],
onShellReady() {
res.setHeader('Content-Type', 'text/html');
pipe(res);
}
}
);
});


然后在组件里用 Suspense 包裹需要异步获取数据的部分:

function App() {
return (
<div>
<h1>首屏标题</h1>
<Suspense fallback={<Skeleton />}>
<AsyncDataComponent />
</Suspense>
</div>
);
}


这样做的好处是,服务端会先吐出静态的 HTML 结构,用户立马就能看到内容。遇到 Suspense 边界,先显示 fallback(骨架屏),等数据准备好后,服务端继续往流里推送真实内容的 HTML,客户端 React 会自动 hydrate 接管。

关键是客户端 hydrate 的姿势要对:

import { hydrateRoot } from 'react-dom/client';

hydrateRoot(document.getElementById('app'), <App />);


注意是用 hydrateRoot 而不是 createRoot,前者会复用服务端吐出来的 DOM 节点,不会重新渲染一遍,这样就不会闪屏了。

还有个常见的坑,如果你用了数据预取(比如 renderToString 阶段收集数据),一定要确保客户端 hydration 的时候数据已经注入进去了,不然客户端又会重新请求一遍,导致二次渲染。

简单总结一下,你现在的方案是「假 SSR」,改成服务端真正渲染首屏内容 + 流式传输 + 正确的 hydration,闪屏问题就解决了。调试看看网络面板里 HTML 响应是不是已经有内容了,如果还是空的,说明服务端渲染没配好。
点赞
2026-03-02 22:02
Code°松奇
可以试试这样:服务端渲染的时候,不只是吐一个空的
,而是把首屏关键组件的 HTML 直接渲染出来,同时把对应的数据也通过 window.__INITIAL_STATE__