Closed
Description
背景
当前内容区由预渲染吐出首屏,但是随后加载 JavaScript 逻辑后,页面重新 loading 载入导致预渲染的页面「被浪费」。究其原因,其中涉及到变动的逻辑仅仅是在菜单区域,但是照成了全部的页面的重新 loading 是不可接受的。因此可以解耦菜单与内容区,在利用预渲染带来的首屏体验的同时,仅仅重新渲染需要加载动态逻辑的部分,进一步完善衔接体验。
- 问题演示
MailVideo.mov
以访问章节 快速上手 为例,用户从访问到首屏渲染页面到页面可交互(JavaScript 逻辑执行完毕),会经历如下步骤:
首屏阶段:当用户访问 快速上手 时,gp-pages 推送预渲染页面,用户获得友好的首屏体验 😁。
衔接阶段:从预渲染页面到页面可交互,出现了干扰体验的加载,体验十分不好 😭。
可交互阶段:左侧菜单按钮等 JavaScript 逻辑执行完毕,在这个阶段用户可以与页面进行交互。
策略
- ❎ 思路一: 内容区域使用 iframe 单独加载, 与 menu 分离。
- 要保证内容区的预渲染,如果内容区在 iframe 中加载,会导致内容区的渲染更加延后。
- ❎ 思路二: 重构左侧 menu, 不用 menu 动画逻辑, 一个层级只显示当前 menu 列表。相关 issue:优化首页菜单区域可可交互时间过长的问题 #219
- 缺陷: 该做法的交互与存量交互完全不同,成本高。
- 思路三: 菜单区域使用 iframe 单独加载, 与内容区分离。
- 缺陷:
- 牺牲部分体验: 菜单栏交互需要变更为 fixed 定位,体验没有原来的效果好。
- Iframe 区域同步事件成本高: 这部分没有进一步探究,可预计的是成本不小。
- 缺陷:
- 思路四: 从根路径解耦区分预渲染与线上环境的渲染,预渲染时渲染出大部分静态页面,线上渲染渲染补充动态页面与逻辑。同时移除「正在加载中逻辑」。
const ifProdRender = ifProd && !ifPrerender
if (!ifProdRender) {
ReactDOM.render(
<RouterRoot />,
document.getElementById('root'),
)
} else {
// render dynamic logic(such as menu) here.
ReactDOM.render(
<RouterRoot pointRender="menu" />,
document.getElementById('root'),
)
}
packages/crd-seed/layout/index.js
return (
<>
{
pointRender === 'menu'
// prod render
? renderMenuContainer()
// pre & dev render
: <div className={styles.wrapper}>
<Header
logo={logo}
href={ifAddPrefix ? `/${repo}` : `/`}
location={location}
indexProps={indexProps}
menuSource={menuSource}
/>
<div
className={cx(styles.wrapperContent, {
[styles.wrapperMobile]: isMobile,
})}
>
{renderPageHeader()}
<div id="menuPosition">
{renderMenuContainer()}
</div>
{renderContent()}
<Footer inlineCollapsed={inlineCollapsed} />
</div>
</div>
}
</>
)
此番修改后,首屏加载后,确实不会二次刷新了,但是带来了几个问题,
- menu 点击切换后,url 会发生变化,但是内容区未发生更新。
- 内容区(包含样式)未更新,因此不能展开收起。
当前在思路四的基础上,确实不可避免存在移除预渲染的 html 后的一刹那闪动问题。
- ✨ 思路五: 依赖 gp-pages 服务,基于预渲染加载 SSR 首屏渲染页面,然后客户端执行注水逻辑。
MailVideo.mov
- ReactDOM.render(
- <RouterRoot />,
- document.getElementById('root'),
- )
+ if (ifDev) {
+ // dev render
+ document.getElementById('root').innerHTML = ReactDOMServer.renderToString(<RouterRoot />)
+ ReactDOM.hydrate(
+ <RouterRoot />,
+ document.getElementById('root'),
+ )
+ } else if (ifPrerender) {
+ // prerender
+ document.getElementById('root').innerHTML = ReactDOMServer.renderToString(<RouterRoot />)
+ } else {
+ // prod render:
+ ReactDOM.hydrate(
+ <RouterRoot />,
+ document.getElementById('root'),
+ )
+ }