渐进式渲染怎么做才能不闪屏? 迷人的国凤 提问于 2026-02-27 12:35:20 阅读 59 优化 我试了用流式 SSR 返回 HTML,但首屏内容先显示骨架屏,等 JS 加载完又整个替换成真实内容,明显闪了一下,体验很不好。是不是应该让服务端直接返回部分真实数据? 现在服务端只返回空容器:<div id="app"></div>,然后前端 hydration 全靠客户端 JS。有没有办法让服务端先吐出首屏关键内容,再逐步 hydrate? 我来解答 赞 14 收藏 分享 生成中... 手机扫码查看 复制链接 生成海报 反馈 发表解答 您需要先 登录/注册 才能发表解答 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 内联,闪屏问题基本解决了。 回复 点赞 3 2026-02-27 13:04 加载更多 相关推荐 2 回答 43 浏览 渐进式渲染怎么做才能不闪屏? 我在做首屏优化,尝试用渐进式渲染先显示骨架屏再加载真实内容,但页面老是闪一下白屏或者布局跳动,体验很不好。 我现在的做法是用 visibility: hidden 隐藏内容区域,等数据回来再切 vis... 一世杰 优化 2026-03-22 22:30:21 2 回答 49 浏览 渐进式渲染时首屏内容被二次重绘怎么办? 我在用骨架屏做渐进式渲染时遇到个问题,当真实内容加载完成替换骨架屏时,页面会出现明显闪烁。比如下面这个商品卡片: <div class="skeleton"> <div class=... Dev · 炳诺 优化 2026-02-15 09:11:32 2 回答 97 浏览 渐进式渲染中骨架屏如何避免与真实内容重叠? 我在用骨架屏做渐进式渲染时遇到问题,真实内容加载后骨架屏会闪一下再消失,用户体验不好。我给骨架屏加了transition: opacity 0.3s,但内容出现闪一下消失的情况,有没有更好的解决方案?... 司徒子聪 优化 2026-01-29 20:35:22 2 回答 67 浏览 安全需求文档该怎么写才能防XSS漏洞? 我们在做用户评论功能时,测试发现XSS漏洞,但安全需求文档里只写了“过滤危险字符”,具体该怎么做才能有效防范呢? 之前尝试用正则表达式过滤了<script>标签和特殊字符,但测试人员用Un... UX-彩云 安全 2026-01-29 21:23:26 2 回答 100 浏览 React中Canvas绘制图形时,为什么每次渲染都会重复叠加? 在React组件里用Canvas画了一个矩形,每次修改状态重新渲染时,新旧图形会叠加显示,怎么才能让每次绘制覆盖之前的图形呢? 我尝试这样写代码,但问题依旧存在: class DrawCanvas e... Zz正宇 前端 2026-01-28 08:50:24 1 回答 41 浏览 渐进式图片在 React 中怎么实现才有效? 我在做图片加载优化,听说渐进式 JPEG 能提升体验,但直接用普通 <img> 标签好像没效果。我试过把图片转成 progressive 格式,但在 React 组件里加载时还是从上到下一... 诸葛茜茜 优化 2026-03-31 11:24:13 1 回答 41 浏览 List列表数据渲染后无法正确显示样式怎么办? 我在用Vue写一个商品列表,数据能正常从接口拿到,也用v-for渲染出来了,但每个列表项的间距和对齐总是乱的,明明CSS写了margin和flex布局。 控制台也没报错,结构看起来没问题,但就是样式没... A. 春景 组件 2026-03-31 05:50:16 1 回答 34 浏览 时间分片渲染长列表时样式错乱怎么办? 我在用 requestIdleCallback 做长列表的时间分片渲染,数据是分批 append 到 DOM 的,但每次新批次插入后,滚动位置会跳动,而且有些 item 的样式看起来不对。 我检查了 ... 夏侯绍博 优化 2026-03-30 21:22:12 1 回答 26 浏览 关键渲染路径阻塞,CSS和JS到底该怎么放? 我在优化页面首屏加载时,发现即使把CSS放在head里、JS放底部,Lighthouse还是提示“阻塞渲染”。明明已经按教程做了啊,是不是还有其他坑? 比如我现在的结构是这样: <!DOCTYP... Prog.丹丹 优化 2026-03-30 16:55:14 1 回答 39 浏览 如何在React中准确监控组件渲染耗时? 我在做性能优化时想监控某个React组件的渲染时间,但用performance.now()测出来的结果不太稳定,有时候差几十毫秒,不知道是不是方法不对。 我目前是在useEffect里记录开始和结束时... シ鑫哲 前端 2026-03-30 14:11:13
问题出在哪呢,服务端只吐了一个空
,那首屏渲染的压力全在客户端 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内联,闪屏问题基本解决了。