渐进式渲染怎么做才能不闪屏? 迷人的国凤 提问于 2026-02-27 12:35:20 阅读 19 优化 我试了用流式 SSR 返回 HTML,但首屏内容先显示骨架屏,等 JS 加载完又整个替换成真实内容,明显闪了一下,体验很不好。是不是应该让服务端直接返回部分真实数据? 现在服务端只返回空容器:<div id="app"></div>,然后前端 hydration 全靠客户端 JS。有没有办法让服务端先吐出首屏关键内容,再逐步 hydrate? 我来解答 赞 7 收藏 分享 生成中... 手机扫码查看 复制链接 生成海报 反馈 发表解答 您需要先 登录/注册 才能发表解答 2 条解答 英瑞🍀 Lv1 你这问题我之前也踩过坑,说白了就是你现在的做法根本不算真正的 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°松奇 Lv1 可以试试这样:服务端渲染的时候,不只是吐一个空的 ,而是把首屏关键组件的 HTML 直接渲染出来,同时把对应的数据也通过 window.__INITIAL_STATE__ 或 的方式内联进去。这样前端 hydration 的时候,就能直接复用这部分 DOM,不会整个替换掉,自然就不闪了。 比如用 React + Next.js 的话,其实它默认就支持这种模式,只要你的组件在服务端能跑出内容,而且用了 useEffect 里再做副作用,一般不会闪。但如果你是自己搭的 SSR 流,那得注意几点: - 服务端渲染时,把首屏依赖的数据提前 fetch 好,拼到 HTML 里,比如: <div id="app"><h1>标题</h1><p>正文内容...</p></div> <script>window.__INITIAL_STATE__ = {"title":"标题","content":"正文内容..."}</script> <script src="/main.js"></script> - 前端 hydration 的时候,React 会对比 DOM 结构,如果内容一致,就不会重新渲染,只做事件绑定;如果内容不一致,就会报 hydration 错误,所以得保证服务端和前端初始 state 一致。 - 如果你想分阶段 hydrate,比如首屏先 hydrate,其他模块懒加载,可以用 ReactDOM.hydrateRoot 的 onRecoverableError 或者 Suspense + lazy,不过这个得看框架支持程度。 再提醒一个坑:骨架屏和真实内容如果 DOM 结构差异太大,hydration 也会很难受,建议骨架屏尽量复用真实组件的 DOM 结构(比如用同样的标签、class),只是内容是占位的,这样 hydration 才能平滑过渡。 我之前踩过这个坑,一开始也是空容器 + 骨架屏,后面改成服务端直接出首屏 HTML,配合 initialData 内联,闪屏问题基本解决了。 回复 点赞 2 2026-02-27 13:04 加载更多 相关推荐 1 回答 13 浏览 渐进式渲染时首屏内容被二次重绘怎么办? 我在用骨架屏做渐进式渲染时遇到个问题,当真实内容加载完成替换骨架屏时,页面会出现明显闪烁。比如下面这个商品卡片: <div class="skeleton"> <div class=... Dev · 炳诺 优化 2026-02-15 09:11:32 2 回答 68 浏览 渐进式渲染中骨架屏如何避免与真实内容重叠? 我在用骨架屏做渐进式渲染时遇到问题,真实内容加载后骨架屏会闪一下再消失,用户体验不好。我给骨架屏加了transition: opacity 0.3s,但内容出现闪一下消失的情况,有没有更好的解决方案?... 司徒子聪 优化 2026-01-29 20:35:22 2 回答 50 浏览 安全需求文档该怎么写才能防XSS漏洞? 我们在做用户评论功能时,测试发现XSS漏洞,但安全需求文档里只写了“过滤危险字符”,具体该怎么做才能有效防范呢? 之前尝试用正则表达式过滤了<script>标签和特殊字符,但测试人员用Un... UX-彩云 安全 2026-01-29 21:23:26 2 回答 62 浏览 React中Canvas绘制图形时,为什么每次渲染都会重复叠加? 在React组件里用Canvas画了一个矩形,每次修改状态重新渲染时,新旧图形会叠加显示,怎么才能让每次绘制覆盖之前的图形呢? 我尝试这样写代码,但问题依旧存在: class DrawCanvas e... Zz正宇 前端 2026-01-28 08:50:24 1 回答 5 浏览 Tag标签动态渲染时样式丢失怎么办? 我用 Vue 动态渲染一组 Tag 标签,数据是从接口拿的,但渲染出来的标签没有样式,class 好像没生效。 明明静态写死的标签是正常的,比如 测试 没问题,但用 v-for 渲染就只有文字,样式全... 爱学习的心霞 组件 2026-03-03 10:55:18 2 回答 11 浏览 react-window 渲染空白,数据明明有却显示不出来? 我用 react-window 的 FixedSizeList 渲染一个长列表,数据是从接口拿的,console.log 也能看到数组有内容,但页面就是一片空白,Item 组件根本没执行。 我试过把数... A. 卿硕 优化 2026-03-01 23:46:20 1 回答 7 浏览 Electron 主进程和渲染进程通信收不到消息怎么办? 我用 Electron 做了个小工具,主进程里监听了 'get-data' 事件,但渲染进程发了消息后完全没反应,也没报错,不知道是哪一步写错了。 我在主进程里这样注册的监听: ipcMain.han... 春莉 框架 2026-03-01 11:05:20 1 回答 35 浏览 Taro中Redux状态更新后页面不重新渲染怎么办? 我在Taro项目里用Redux管理状态,dispatch action之后state确实变了,但页面就是不重新渲染。我试过用useSelector取数据,也确认了reducer返回的是新对象,可组件还... A. 普涵 框架 2026-02-28 14:01:23 1 回答 15 浏览 前端怎么用惰性求值优化大数据列表渲染? 我有个页面要展示上万条数据的列表,直接渲染卡得不行。听说可以用惰性求值只处理可视区域的数据,但不知道具体咋实现。试过用 Array.prototype.slice 截取一部分,但滚动时还是卡顿明显。 ... 司徒成娟 优化 2026-02-27 18:12:23 1 回答 17 浏览 Akita状态更新后组件为什么不重新渲染? 我用Akita做状态管理,修改store里的数据后,React组件居然没重新渲染,是不是哪里写错了? 我试过用update方法直接改对象属性,也试过用produce,但都没用。控制台打印新状态是对的,... 技术艺童 框架 2026-02-26 18:29:20
问题出在哪呢,服务端只吐了一个空
,那首屏渲染的压力全在客户端 JS 身上。骨架屏消失、真实内容出来,这中间肯定会有个视觉跳变,因为两者是串行的,不是无缝衔接的。<div id="app"></div>正确的渐进式渲染应该是这样的:
服务端要真正渲染首屏内容,而不是空壳。用 React 举例,如果你用的是 React 18,配合
或者renderToPipeableStream,配合renderToReadableStream就能实现真正的流式 SSR。Suspense大概的思路是这样,服务端:
然后在组件里用 Suspense 包裹需要异步获取数据的部分:
这样做的好处是,服务端会先吐出静态的 HTML 结构,用户立马就能看到内容。遇到 Suspense 边界,先显示 fallback(骨架屏),等数据准备好后,服务端继续往流里推送真实内容的 HTML,客户端 React 会自动 hydrate 接管。
关键是客户端 hydrate 的姿势要对:
注意是用
而不是hydrateRoot,前者会复用服务端吐出来的 DOM 节点,不会重新渲染一遍,这样就不会闪屏了。createRoot还有个常见的坑,如果你用了数据预取(比如 renderToString 阶段收集数据),一定要确保客户端 hydration 的时候数据已经注入进去了,不然客户端又会重新请求一遍,导致二次渲染。
简单总结一下,你现在的方案是「假 SSR」,改成服务端真正渲染首屏内容 + 流式传输 + 正确的 hydration,闪屏问题就解决了。调试看看网络面板里 HTML 响应是不是已经有内容了,如果还是空的,说明服务端渲染没配好。
,而是把首屏关键组件的 HTML 直接渲染出来,同时把对应的数据也通过window.__INITIAL_STATE__或的方式内联进去。这样前端 hydration 的时候,就能直接复用这部分 DOM,不会整个替换掉,自然就不闪了。比如用 React + Next.js 的话,其实它默认就支持这种模式,只要你的组件在服务端能跑出内容,而且用了
useEffect里再做副作用,一般不会闪。但如果你是自己搭的 SSR 流,那得注意几点:- 服务端渲染时,把首屏依赖的数据提前 fetch 好,拼到 HTML 里,比如:
- 前端 hydration 的时候,React 会对比 DOM 结构,如果内容一致,就不会重新渲染,只做事件绑定;如果内容不一致,就会报 hydration 错误,所以得保证服务端和前端初始 state 一致。
- 如果你想分阶段 hydrate,比如首屏先 hydrate,其他模块懒加载,可以用
ReactDOM.hydrateRoot的onRecoverableError或者Suspense+lazy,不过这个得看框架支持程度。再提醒一个坑:骨架屏和真实内容如果 DOM 结构差异太大,hydration 也会很难受,建议骨架屏尽量复用真实组件的 DOM 结构(比如用同样的标签、class),只是内容是占位的,这样 hydration 才能平滑过渡。
我之前踩过这个坑,一开始也是空容器 + 骨架屏,后面改成服务端直接出首屏 HTML,配合
initialData内联,闪屏问题基本解决了。