🙋🏻♀️编者按:这是蚂蚁集团前端工程师云谦关于 Umi 4 新特性系列分享的第三篇,本文是 CSR 的请求优化,不涉及 SSR,SSR 相关的会在后面找机会介绍。 第一篇👉🏻 Umi 4 特性 01:MFSU V3,比 Vite 还要快 第二篇👉🏻 Umi 4 特性 02:React Router 6 和新路由
可能有些同学会觉得这个功能杀鸡用牛刀,Umi 大部分应用是中台应用,使用者相比 C 端产品的耐受度高,产物大一点打开慢一点性能差一点无所谓。我理解这是 ROI 权衡后的无奈结果,又有谁不希望速度更快和体验更好呢?
本篇用一个例子和大家介绍问题和 Umi 的解。例子是,两个组件 A 和 B,A 嵌套 B,A 和 B 都有各自的请求和渲染逻辑。
现状
Umi 的现状是,除了做了特殊优化,大部分 Umi 应用的请求都在 useEffect 中发起请求,request hooks 可能是 useRequest、useSWR 等,请求库可能是 umi-request、axios 等。renderA ▶ requestA ▶ renderB ▶ requestB效果类似这个 DEMO,https://codesandbox.io/s/fast-glade-rqnhtt 。这是最慢的请求,下一个要等上一个结束,也叫「请求瀑布」,在 React 文档中叫「Fetch-on-Render」。不仅如此,真实项目是要考虑工程化的,其中一点是产物如何加载,因为不管是请求发起还是渲染的逻辑,都来自于 JS 产物,而 JS 产物通常会基于路由做 code splitting,所以请求会变成这样。loadInitialCode ▶ loadA ▶ renderA ▶ requestA ▶ loadB ▶ renderB ▶ requestB
默认最快的请求
跳过其他备选和中间方案,我们直接看 Umi 4 的解。策略是能提前的提前,能并行的并行。这是当下 CSR 请求方案的最优解。loadInitialCode ▶ requestA
▶ requestB
▶ loadA ▶ renderA
▶ loadB ▶ renderB效果类似这个 Demo,https://codesandbox.io/s/frosty-hermann-bztrp 。用户代码如下,不再需要在 useEffect 里写请求。
import { useClientData } from 'umi';export defaunt () => { const data = useClientData();}export function async clientData() { return await fetch('/api/A');}
为啥 A 和 B 的 request 能在初始代码加载之后立即发起?因为框架会把请求相关代码从路由组件中拆出来,和路由配置、临时的入口文件等放到一起,和路由组件拆开,这样路由组件的 code splitting 就不会影响请求的 ready 实际。不仅是拆出来,还要把拆的内容从原路由组件中删除。背后实现是基于 esbuild 的 tree-shaking,tree-shaking 在 webpack 中是在压缩时处理,而在 esbuild 时是在 bundle 时处理。
request 和 render 之间会有 race condition 吗?比如 render 比 request 先执行,render 可能报错?不会。基于 Suspense 实现,request 和 render 是并行的,React 官方叫「Render-as-You-Fetch」,边请求边渲染。好处是,就算请求没有好,也会有骨架屏,不会一片空白。当然,不支持 Suspense 的场景会做降级,用「Fetch-Then-Render」的策略,等全部请求 ready 之后再执行渲染。参考:
https://17.reactjs.org/docs/concurrent-mode-suspense.html#traditional-approaches-vs-suspensehttps://remix.run/blog/react-server-componentshttps://www.chakshunyu.com/blog/a-fundamental-guide-to-react-suspense/