SPA Lazy Loading Pitfalls
SPA 懒加载陷阱
You should be lazy loading some of your routes. And if you are, you're probably messing up the data-fetching because it's an easy mistake to make.
您应该懒加载一些路由。如果您这样做了,您可能会搞乱数据获取,因为这是一个很容易犯的错误。

反应单页应用反应路由
See Our Public Workshops:
查看我们的公开研讨会:
Lazy loading your pages in an SPA is a great way to reduce your bundle size for new visitors. The problem is, if you're not careful, you'll be creating a much slower experience than if you didn't lazy load.
在单页应用程序中懒加载您的页面是减少新访客的包大小的好方法。问题是,如果您不小心,您将创造出比不使用懒加载时慢得多的体验。
Imagine this scenario: A user visits an SPA site which does no lazy loading, so all the UI they might want to visit is loaded into the browser in advance. Every time they visit a page, the user just has to wait for data and they see a loading indicator momentarily. This is the normal scenario for a lot of SPA apps.
想象一下这个场景: 一个用户访问一个不进行懒加载的 SPA 网站,因此他们可能想要访问的所有用户界面都预先加载到浏览器中。每当他们访问一个页面时,用户只需等待数据,然后他们会瞬间看到一个加载指示器。这是许多 SPA 应用程序的正常场景。
Now imagine this scenario with lazy loading: A user visits an SPA site which does not have all the UI they might want to see because we will lazy load components we don't have. The user visits a page, we start to lazy load UserProfile.tsx
. Once that page arrives, the useEffect
(or useQuery
, useSWR
, or other) strategy you have for loading data then starts to fetch. But because the user had to wait for the JavaScript file to load for the component, they'll have to wait in serial for the data to load next
现在想象一下懒加载的场景:用户访问一个不包含他们想要查看的所有 UI 的单页面应用(SPA)网站,因为我们将懒加载我们没有的组件。用户访问一个页面,我们开始懒加载 UserProfile.tsx
。一旦该页面加载完毕,你用于加载数据的 useEffect
(或 useQuery
, useSWR
等)策略就会开始获取数据。但由于用户必须等待组件的 JavaScript 文件加载,他们将不得不串行等待数据的加载。
GET /components/UserProfile.tsx ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓GET /api/users/1 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
Because of lazy loading, your users might get a smaller JavaScript payload initially but now they wait twice as long for pages to load because they wait for JS then wait for data.
由于懒加载,用户最初可能会获得更小的 JavaScript 批量负载,但现在他们加载页面的时间是原来的两倍,因为他们需要等待 JS,然后等待数据。
Conventional wisdom says to try a fetching strategy that exists outside your components like React Router loaders. The idea being that when you navigate to this page, React Router will fetch data from your loader function then feed it into your component:
传统智慧认为应该尝试一种存在于您的组件之外的获取策略,例如 React Router 加载器。这个想法是,当您导航到此页面时,React Router 将从您的加载函数获取数据,然后将其传递给您的组件:
import { loader, UserProfile } from '../components/UserProfile'const router = createBrowserRouter(createRoutesFromElements(<Route path="/" element={<RootLayout />}><Route index element={<HomePage />} /><Route path="/users/:userId" loader={loader} element={<UserProfile />} /></Route>,),)
If you haven't see loaders yet, the idea is that React Router knows what page you want to go to so it will call the async loader function you provide first. Then when it resolves React Router will give the data to your component. You can even use loaders with TanStack Query (formerly React Query).
如果您还没有看到加载器,那么这个概念是 React Router 知道您想要去哪个页面,因此它会首先调用您提供的异步加载器函数。然后当它解析时,React Router 将把数据传递给您的组件。您甚至可以 与 TanStack Query (前称 React Query)一起使用加载器。
🔥 Here's the big idea
🔥 这是个大想法
What if we can load the data with the loader in parallel to loading a lazy component. In theory this should be easy since React Router will fetch from the loader and React can download your component simultaneously. In other words, parallel loading for the fastest page transitions:
如果我们可以在加载懒组件的同时,通过加载器并行加载数据。理论上,这应该很简单,因为 React Router 会从加载器中获取数据,而 React 可以同时下载你的组件。换句话说,快速页面过渡的并行加载:
GET /components/UserProfile.tsx ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓GET /api/users/1 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
However, there is a subtle problem with this approach based on how you've organized your code.
然而,这种基于你如何组织代码的方法存在一个微妙的问题。
The problem with loaders and Lazy Loading
加载程序和懒加载的问题
One question you'll face when starting to use loaders is "where do I put them?". You'll discover that it is recommended to export them from the same place that your component is like this:
您在开始使用加载器时会面临一个问题,即“我该把它们放在哪里?”您会发现,建议从与组件相同的位置导出它们,如下所示:
// UserProfile.tsxexport async function loader({ params }) {const user = await fetch(`/users/${params.userId}`).then((r) => r.json())return user}export function UserProfile() {const user = useLoaderData() // returns the result of loader// ...}
It's great from an organizational standpoint, but this is where the problems start with lazy loading. Assuming the above loader
and UserProfile
are in the same file, what do you suppose happens when we lazy load the UserProfile
component but we load the loader into the main bundle with a standard import like this?
从组织的角度来看,这很好,但懒加载的问题就从这里开始。假设上面的loader
和UserProfile
在同一个文件中,当我们懒加载UserProfile
组件但通过标准导入像这样将加载器加载到主捆绑包中时,你觉得会发生什么?
import { loader } from '../components/UserProfile'const router = createBrowserRouter(createRoutesFromElements(<Route path="/" element={<RootLayout />}><Route index element={<HomePage />} /><Route path="/users/:userId" loader={loader} lazy={() => import('../components/UserProfile')} /></Route>,),)
Keep in mind that in order to use React Router's lazy
prop, you will have to rename your component from UserProfile
to Component
.
请记住,为了使用 React Router 的 lazy
属性,您需要将组件的名称从 UserProfile
更改为 Component
。
Here's what happens when you load two things from a file and one is lazily loaded. With Vite (and I'm not sure about others), it seems that the two functions (the component and loader) will be loaded into the main bundle with the above example, even though we're asking for the component to be lazily loaded. It kinda makes sense I guess, I'm sure there's implementation details with lazy loading that don't allow us to lazy load a part of a file but not other parts.
当你从文件中加载两个东西,而其中一个是惰性加载时,会发生这样的事情。使用 Vite(我不确定其他工具),在上述示例中,这两个函数(组件和加载器)似乎会被加载到主包中,尽管我们要求组件进行惰性加载。我想这也有些道理,我相信惰性加载的实现细节不允许我们对文件的一部分进行惰性加载,而不对其他部分进行加载。
I noticed when I checked the network tab and it looked like this with only data fetching and no lazy loading the component:
我注意到当我检查网络标签时,它看起来只有数据获取而没有懒加载组件:
GET /api/users/1 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
Upon further checks, I can clearly see that my component code is in the main bundle from the beginning so it's pretty concrete that the way I loaded these two things will not let me reach my goal of lazy loading and fetching in parallel.
经过进一步检查,我可以清楚地看到我的组件代码从一开始就包含在主包中,因此,我加载这两个内容的方式不太可能让我实现懒加载和并行获取的目标。
The solution 解决方案
In order to get lazy loading to work, while fetching in parallel, you'll need to separate your loader function from the file that has the component. The React Router docs even say this in the section on lazy loading:
为了让懒加载有效工作,并且并行获取,你需要将加载器函数与包含组件的文件分开。React Router 文档在懒加载章节中甚至提到了这一点:
Additionally, as an optimization, if you statically define a loader/action then it will be called in parallel with the lazy function. This is useful if you have slim loaders that you don't mind on the critical bundle, and would like to kick off their data fetches in parallel with the component download.
此外,作为一种优化,如果您静态定义了加载器/动作,它将与懒加载函数并行调用。如果您有轻量级加载器,而不介意它们在关键包中,并希望与组件下载并行启动数据获取,这将非常有用。
If we had written the code like this, we would get parallel component and data fetching as we wished:
如果我们像这样编写代码,我们就可以实现我们想要的并行组件和数据获取:
async function loader({ params }) {const user = await fetch(`/users/${params.userId}`).then((r) => r.json())return user}const router = createBrowserRouter(createRoutesFromElements(<Route path="/" element={<RootLayout />}><Route index element={<HomePage />} /><Route path="/users/:userId" loader={loader} lazy={() => import('../components/UserProfile')} /></Route>,),)
Take it for what it's worth, now back to that original question:
就当它有其价值,现在回到那个原始问题:
Where should I put my loaders?
我应该把我的装载机放在哪里?
I'll leave that for you to figure out based on your current organization :)
我会把这个留给你去根据你目前的组织来弄清楚 :)
照片由杰里米·托马斯在 Unsplash 上拍摄