译零 TransZero · TanStack Query 中文文档
主站

性能与请求瀑布

更新于 2026-11-02 Solid 查看英文原文 ↗

应用性能是一个宽泛而复杂的领域。虽然 Solid Query 无法让你的 API 变得更快,但在使用 Solid Query 时,你仍需留意若干事项,以确保获得最佳性能。

使用 Solid Query(或任何允许你在组件内部拉取数据的数据拉取库)时,最大的性能隐患是请求瀑布(request waterfalls)。本页其余内容将解释什么是请求瀑布、如何识别它们,以及如何重构你的应用或 API 来避免它们。

预取与路由集成指南 在此基础上进一步展开,教你当无法或不便于重构应用或 API 时,如何提前预取数据。

服务端渲染与注水指南 教你如何在服务端预取数据,并将该数据传递给客户端,从而避免在客户端重复拉取。

什么是请求瀑布?

请求瀑布是指:对某个资源(代码、CSS、图片、数据等)的请求,必须等到另一个资源的请求完成之后才能开始。

考虑一个网页:在加载 CSS、JS 等资源之前,浏览器首先需要加载 HTML 标记(markup)。这就是一次请求瀑布。

Code
11. |-> Markup22.   |-> CSS32.   |-> JS42.   |-> Image

如果你在 JS 文件中拉取 CSS,现在就形成了双重瀑布:

Code
11. |-> Markup22.   |-> JS33.     |-> CSS

如果该 CSS 使用了背景图片,则构成三重瀑布:

Code
11. |-> Markup22.   |-> JS33.     |-> CSS44.       |-> Image

识别和分析请求瀑布最有效的方式,通常是打开浏览器开发者工具(devtools)的“网络”(Network)标签页。

每次瀑布至少代表一次往返服务器的请求(除非资源已本地缓存)。实践中,某些瀑布可能代表多次往返,因为浏览器建立连接本身就需要若干次来回通信;但此处我们暂且忽略这一细节。正因如此,请求瀑布的负面影响高度依赖于用户的网络延迟。以三重瀑布为例,它实际上对应 4 次服务器往返。若延迟为 250ms(在 3G 网络或较差网络条件下并不少见),则仅延迟一项就达 4×250 = 1000ms。如果我们能将其扁平化为第一种情形(仅 2 次往返),延迟即可降至 500ms,背景图片的加载时间甚至可能缩短一半!

请求瀑布与 Solid Query

现在让我们聚焦 Solid Query。我们先考察不启用服务端渲染的情形。在发起任何查询之前,我们必须先加载 JS;因此,在屏幕上显示数据前,我们已面临双重瀑布:

Code
11. |-> Markup22.   |-> JS33.     |-> Query

以此为基础,我们来看几种可能导致 Solid Query 中出现请求瀑布的模式,以及如何规避它们:

  • 单组件瀑布 / 串行查询
  • 嵌套组件瀑布
  • 代码分割(Code Splitting)

单组件瀑布 / 串行查询

当单个组件先拉取一个查询,再拉取另一个查询时,即构成请求瀑布。这通常发生在第二个查询是依赖查询的情况下——即其拉取过程依赖第一个查询返回的数据:

TypeScript
1// Get the user2const userQuery = useQuery(() => ({3  queryKey: ['user', email],4  queryFn: getUserByEmail,5}))6 7const userId = () => userQuery.data?.id8 9// Then get the user's projects10const projectsQuery = useQuery(() => ({11  queryKey: ['projects', userId()],12  queryFn: getProjectsByUser,13  // The query will not execute until the userId exists14  enabled: !!userId(),15}))

尽管并非总可行,但为实现最优性能,最好重构你的 API,使这两个查询可通过单次请求完成。以上例而言,与其先拉取 getUserByEmail 再基于其结果拉取 getProjectsByUser,不如新增一个 getProjectsByUserEmail 查询,从而扁平化瀑布。

嵌套组件瀑布

嵌套组件瀑布指父组件与子组件均包含查询,且父组件在其查询完成前不渲染子组件。

若子组件的渲染依赖父组件的数据,或子组件需通过 props 接收父组件查询结果的某一部分来构造自身查询,则构成一种依赖型嵌套组件瀑布。

我们先看一个子组件不依赖父组件的例子。

TypeScript
1import { Switch, Match } from 'solid-js'2 3function Article(props) {4  const articleQuery = useQuery(() => ({5    queryKey: ['article', props.id],6    queryFn: getArticleById,7  }))8 9  return (10    <Switch>11      <Match when={articleQuery.isPending}>12        Loading article...13      </Match>14      <Match when={articleQuery.isSuccess}>15        <ArticleHeader articleData={articleQuery.data} />16        <ArticleBody articleData={articleQuery.data} />17        <Comments id={props.id} />18      </Match>19    </Switch>20  )21}22 23function Comments(props) {24  const commentsQuery = useQuery(() => ({25    queryKey: ['article-comments', props.id],26    queryFn: getArticleCommentsById,27  }))28 29  ...30}

注意:尽管 <Comments> 接收父组件传入的 prop id,但该 id 在 <Article> 渲染时已可用,因此完全可与文章查询并行拉取评论。在真实应用中,子组件可能深嵌于父组件之下,此类瀑布往往更难识别和修复;但就本例而言,一种扁平化方案是将评论查询提升至父组件:

TypeScript
1import { Switch, Match, Show } from 'solid-js'2 3function Article(props) {4  const articleQuery = useQuery(() => ({5    queryKey: ['article', props.id],6    queryFn: getArticleById,7  }))8 9  const commentsQuery = useQuery(() => ({10    queryKey: ['article-comments', props.id],11    queryFn: getArticleCommentsById,12  }))13 14  return (15    <Switch>16      <Match when={articleQuery.isPending}>Loading article...</Match>17      <Match when={articleQuery.isSuccess}>18        <ArticleHeader articleData={articleQuery.data} />19        <ArticleBody articleData={articleQuery.data} />20        <Switch>21          <Match when={commentsQuery.isPending}>Loading comments...</Match>22          <Match when={commentsQuery.isSuccess}>23            <Comments commentsData={commentsQuery.data} />24          </Match>25        </Switch>26      </Match>27    </Switch>28  )29}

此时两个查询将并行拉取。

另一种扁平化方式是在 <Article> 组件中预取评论,或在路由层级于页面加载或导航时预取这两个查询。更多细节请参阅预取与路由集成指南。

接下来,我们考察一种依赖型嵌套组件瀑布。

TypeScript
1import { Switch, Match, For } from 'solid-js'2 3function Feed() {4  const feedQuery = useQuery(() => ({5    queryKey: ['feed'],6    queryFn: getFeed,7  }))8 9  return (10    <Switch>11      <Match when={feedQuery.isPending}>12        Loading feed...13      </Match>14      <Match when={feedQuery.isSuccess}>15        <For each={feedQuery.data}>16          {(feedItem) => {17            if (feedItem.type === 'GRAPH') {18              return <GraphFeedItem feedItem={feedItem} />19            }20            return <StandardFeedItem feedItem={feedItem} />21          }}22        </For>23      </Match>24    </Switch>25  )26}27 28function GraphFeedItem(props) {29  const graphQuery = useQuery(() => ({30    queryKey: ['graph', props.feedItem.id],31    queryFn: getGraphDataById,32  }))33 34  ...35}

第二个查询 getGraphDataById 在两个层面依赖其父组件:首先,它仅在 feedItem 是图结构时才执行;其次,它需要从父组件获取 id。

Code
11. |> getFeed()22.   |> getGraphDataById()

本例中,仅靠将查询提升至父组件,或简单添加预取,都无法轻易扁平化瀑布。正如本指南开头所述的依赖查询示例,一种可行方案是重构 API,将图结构数据直接包含在 getFeed 查询中。

即使存在少量请求瀑布,你的应用仍可保持良好性能;但需意识到这是常见的性能隐患,应时刻保持警惕。尤其隐蔽的一种情况是涉及代码分割时,我们接下来便探讨这一点。

代码分割

将应用的 JS 代码拆分为更小的代码块,并仅加载必要部分,通常是实现高性能的关键步骤。但它也存在弊端:往往引入请求瀑布。当这些被分割的代码块内部又包含查询时,问题将进一步加剧。

考虑这个稍作修改的 Feed 示例。

TypeScript
1import { lazy, Switch, Match, For } from 'solid-js'2 3// This lazy loads the GraphFeedItem component, meaning4// it wont start loading until something renders it5const GraphFeedItem = lazy(() => import('./GraphFeedItem'))6 7function Feed() {8  const feedQuery = useQuery(() => ({9    queryKey: ['feed'],10    queryFn: getFeed,11  }))12 13  return (14    <Switch>15      <Match when={feedQuery.isPending}>16        Loading feed...17      </Match>18      <Match when={feedQuery.isSuccess}>19        <For each={feedQuery.data}>20          {(feedItem) => {21            if (feedItem.type === 'GRAPH') {22              return <GraphFeedItem feedItem={feedItem} />23            }24            return <StandardFeedItem feedItem={feedItem} />25          }}26        </For>27      </Match>28    </Switch>29  )30}31 32// GraphFeedItem.tsx33function GraphFeedItem(props) {34  const graphQuery = useQuery(() => ({35    queryKey: ['graph', props.feedItem.id],36    queryFn: getGraphDataById,37  }))38 39  ...40}

该示例呈现双重瀑布,如下所示:

Code
11. |> getFeed()22.   |> JS for <GraphFeedItem>33.     |> getGraphDataById()

但这仅基于示例代码本身。若考虑该页面的首次加载过程,我们实际需完成 5 次服务器往返,才能渲染出图结构!

Code
11. |> Markup22.   |> JS for <Feed>33.     |> getFeed()44.       |> JS for <GraphFeedItem>55.         |> getGraphDataById()

注意:服务端渲染下的表现略有不同,我们将在服务端渲染与注水指南中深入探讨。另请注意,包含 <Feed> 的路由本身也很可能被代码分割,这会再增加一次网络跳转。

在代码分割场景下,将 getGraphDataById 查询提升至 <Feed> 组件并设为条件执行,或添加条件预取,反而可能有益。这样,该查询可与代码加载并行执行,使示例部分变为:

Code
11. |> getFeed()22.   |> getGraphDataById()32.   |> JS for <GraphFeedItem>

但这本质上是一种权衡:你现在将 getGraphDataById 的数据拉取逻辑与 <Feed> 打包在同一 bundle 中,因此需根据实际情况评估哪种方案更优。更多实现方法请参阅预取与路由集成指南。

总结与要点

请求瀑布是一种非常常见且复杂的性能问题,涉及诸多权衡。你可能在不经意间向应用引入瀑布,例如:

  • 向子组件添加查询,却未意识到父组件已有查询
  • 向父组件添加查询,却未意识到子组件已有查询
  • 将带有后代查询的组件迁移至新父组件,而该新父组件的祖先组件本身已有查询
  • 等等

由于这种意外复杂性,主动关注瀑布并定期检查应用(例如不定期查看 Network 标签页)十分值得。你无需彻底消除所有瀑布以获得良好性能,但务必重点关注那些影响显著的瀑布。

在下一指南中,我们将借助预取与路由集成,探讨更多扁平化瀑布的方法。

本页为 TanStack Query 文档(Solid 版)的非官方中文翻译,原文由上游团队维护,遵循 MIT 许可。 译文如有偏差,以英文原文为准。

译自上游 59d11de