译:大型“服务器瀑布问题”与 RSCs

原文:https://www.epicreact.dev/server-waterfall-problem-rscs
作者:Kent C. Dodds
译者:ChatGPT 4 Turbo

编者注:一个老问题迁移到了服务端,但应该还好,因为服务端没有网络开销,同时有更多的控制权。

我们来分析下这个 React 树结构:

<App>
  <ShipSearch />
  <ShipDetails />
</App>

假设 App 组件渲染了 ShipSearchShipDetails 组件,并根据需要传递了 props。

ShipSearch 是一个服务器组件,它接收一个 search 并使用它来执行数据库查询:

async function ShipSearch({ search }) {
	const shipResults = await searchShips({ search })
	// ... 渲染内容
}

ShipDetails 组件也是一个服务器组件,它接收一个 shipId 并使用它来执行数据库查询:

async function ShipDetails({ shipId }) {
	const ship = await getShip({ shipId })
	// ... 渲染内容
}

由于这些都是同级组件,两个查询将同时运行。

searchShips 查询  ---->  searchShips 结果
getShip 查询      ---->  getShip 结果

服务器瀑布

这一切都很好。但假设 App 组件需要解析一个已登录用户:

async function App() {
	const user = await getLoggedInUser()
	// ... 渲染内容
}

哦哦,现在我们遇到了一个瀑布问题:

getLoggedInUser query ---->  getLoggedInUser result
                             searchShips query ---->  result
                             getShip query     ---->  result

问题在于现在 App 必须等待 getLoggedInUser 解析完毕之后才能渲染任何内容。这意味着 searchShipsgetShip 都必须等到 getLoggedInUser 解析之后才能执行,即使它们并不直接依赖于用户信息。

这通常也是 React Suspense 面临的问题,我在 React Suspense workshop exercise 6 中对此进行了深入讨论。

有几种方式来思考这个问题:

也许如果 getLoggedInUser 很快,这就不是问题 🤷‍♂️ 但这并不总是确定的,即使它今天很快,也不意味着明天不会有人添加一些其他慢的东西。不过,忽略这个问题究竟有多糟糕总是好事,因为可能还有更大的问题需要解决。

或者,我们可以将 searchShipsgetShip 提升到 App 中,并将结果传递给 ShipSearchShipDetails。然后我们可以使用 Promise.all 以确保它们并发运行。但是传递 props 可能很快就会变得令人烦恼。我们可以使用像 @epic-web/cachified 这样的库来去重查询,然后 App 只是更早地启动查询,这至少会更好。

不幸的是,另一个问题是,这很快就会变得难以管理,特别是如果有基于其他 props 的逻辑决定哪些查询应该运行。你必须把所有那些逻辑都移到你的整个应用都位于 App 中 😱。

我能想到的下一个解决方案是使用或构建一个编译器,它可以自动找到所有查询并预加载它们。这就是 Relay 所做的。不过,我对这种方法有几点不喜欢:

  1. 如果你有基于其他 props 的逻辑来决定哪些查询应该运行,这种方法就行不通。
  2. 它会给你的构建过程增加很多复杂性。
  3. 当你在产品代码中工作时,不明显会发生什么。

所以我想说的是,我不喜欢这些解决方案中的任何一个。但我想用更多的上下文来重新定义问题。

现状

假设我们生活在一个没有 RSCs 或 Suspense 的世界里。你只是在构建一个带有数据获取和渲染内容的组件的应用。

在那个世界里,你有三个选择:

  1. 将你的查询与需要它们的组件组合在一起(本地化)。
  2. 将你所有的查询提升到一个组件中,并向下传递结果。
  3. 使用编译器找到你所有的查询并自动预加载它们。

听起来熟悉吗?这正是我们在组件和数据方面一直面临的相同问题。数据的本地化和向下传递(属性钻取)之间总是存在这种紧张关系。

Remix 的一个好处是它给了你第四个选择:

  1. 将数据获取与你的组件解耦。

这允许 Remix 在请求到来时立即加载数据,无论组件是否已渲染。

这就是我多年来一直在做的事情,而且感觉棒极了。

那么,为什么是 RSCs?

这篇博文其实并不是要讲我为什么认为 RSCs 很棒。关于这一点,你可以阅读React 服务器组件:UI 的未来。只需知道,在路由边界的组合不如在组件边界的组合好,而我非常希望能有类似 React 的组合方式。

我想要做的是展示 RSCs 是如何适应我们在组件和数据之间一直存在的位置共享与属性传递之间的那种张力的。这里没有新问题。

但我觉得有趣的是,服务器端的瀑布流可能比客户端的瀑布流更好,主要是因为你可以控制网络。你的服务器渲染服务器与你的数据库之间的连接可能更强大、更快、更可靠、更近。或许不是,但重点是_你_在那里拥有控制权,并且如果这对你来说很重要,你可以改善它。你也可以更精细地控制缓存(来自不同客户端的请求可以为常见数据共享一个缓存)。

当你有一个客户端瀑布流时,你正在处理用户的设备和他们的网络连接,这可能很好或者很糟糕,但你肯定无法控制。

因此,将这个问题从客户端转移到服务器,对于许多场景来说似乎是一个净收益。

结论

RSCs 的“瀑布流”问题不是一个新问题。这是我们在组件和数据之间一直存在的位置共享与属性传递之间的同样张力。或许我们可以期待未来有一个新的解决方案。我欢迎想法(最好是不需要特殊编译器的)!在那之前,我实际上对于仅仅使事情变得足够快以至于问题不再是问题感到相当满意。