HomeAbout Me

React Suspense: Optimizations

By Daniel Nguyen
Published in React JS
June 26, 2025
2 min read
React Suspense: Optimizations

🚀 Optimizing React Suspense: Crush Waterfalls, Preload Images, and Leverage Server Caching

React Suspense opens up a powerful pattern for colocating data with the components that need it. But like any great power, it comes with risks—and one of the biggest is the dreaded waterfall.

If you’ve ever wondered why your app feels slower than expected, you might be triggering unnecessary delays in your data loading. Let’s explore how to avoid that.


💧 What’s a Waterfall?

If you open your browser DevTools and look at the Network tab, you’ll see a column called “Waterfall.” That name isn’t just visual—it refers to how your app’s requests are being sequenced.

A bad waterfall looks like this:

Request A --------> Response A
Request B --------> Response B
Request C --------> Response C

Each request starts after the previous one finishes.

A good (parallel) waterfall looks like this:

Request A --------> Response A
Request B --------> Response B
Request C --------> Response C

All requests are made at the same time, reducing total wait time.


⚠️ Why React Suspense Makes Waterfalls Easy (Too Easy)

React Suspense works by suspending the component as soon as a Promise is used. If your component triggers multiple use() calls in sequence, you’ve already created a waterfall.

🧨 Bad: Sequential Fetching (Triggers Waterfall)

function ProfileDetails({ username }: { username: string }) {
const favoritesCount = use(getFavoritesCount(username))
const friends = use(getFriends(username))
return <div>{/* ... */}</div>
}

The second fetch won’t even begin until the first one resolves. Ouch.


✅ Solution: Trigger Promises Before use

Trigger all your data requests before suspending:

function ProfileDetails({ username }: { username: string }) {
const favoritesCountPromise = getFavoritesCount(username)
const friendsPromise = getFriends(username)
const favoritesCount = use(favoritesCountPromise)
const friends = use(friendsPromise)
return <div>{/* ... */}</div>
}

Now both network requests begin at the same time. 🚀


🧱 Hidden Waterfalls in Nested Components

Sometimes waterfalls sneak in when you separate concerns between parent and child components.

function ProfilePage({ username }: { username: string }) {
const userAvatar = use(getUserAvatar(username))
return (
<div>
<Avatar url={userAvatar} />
<ProfileDetails username={username} />
</div>
)
}

The ProfileDetails component’s data won’t even begin to fetch until the ProfilePage finishes suspending on getUserAvatar.

✅ Fix: Preload Promises in the Parent

function ProfilePage({ username }: { username: string }) {
getFavoritesCount(username) // start request early
getFriends(username)
const userAvatar = use(getUserAvatar(username))
return (
<div>
<Avatar url={userAvatar} />
<ProfileDetails username={username} />
</div>
)
}

By calling the data functions early (which return cached Promises), we avoid the waterfall.


🛠 Bonus: Build a loadData Helper

To make your parent code cleaner, attach a loadData method to your component:

ProfileDetails.loadData = (username) => ({
favoritesCountPromise: getFavoritesCount(username),
friendsPromise: getFriends(username),
})

Use it like this:

ProfileDetails.loadData(username)

This keeps your preloading strategy reusable and maintainable.


🧠 Why Not Just Use Props?

Yes, you could also pass Promises via props:

<ProfileDetails
favoritesCountPromise={favoritesCountPromise}
friendsPromise={friendsPromise}
/>

But that spreads your data fetching logic all over the place. Instead, triggering fetches early—then using use() where needed—gives you both colocated logic and optimized performance.


🖼️ Preloading Images to Avoid Visual Waterfalls

Another kind of waterfall happens with images.

Imagine this scenario:

  • You click a new ship.
  • React fetches the ship’s data.
  • After that, React renders <img src="ship.jpg" />, and only then does the browser start loading the image.

That’s a visual waterfall.

✅ Fix: Preload the Image in Parallel

function ShipDetails({ name }: { name: string }) {
preloadImage(`/img/${name}.jpg`)
const ship = use(getShip(name))
return (
<>
<ShipImg src={`/img/${name}.jpg`} />
{/* other ship data */}
</>
)
}

Now the image download begins as soon as we know the ship’s name—not after rendering the image tag.


🌐 Server-Side: Cache-Control Headers

Suspense helps us optimize frontend performance. But don’t forget your backend.

You can avoid redundant requests and improve refresh speed by leveraging HTTP caching.

✅ Add Cache Headers to API Responses

Cache-Control: max-age=3600

This instructs the browser to cache the response for an hour.

If you have API endpoints like /api/ship-details, simply set this header on the response server-side:

res.setHeader("Cache-Control", "max-age=3600")

Just be sure the data is okay to cache and doesn’t change too frequently.


🧪 Putting It All Together: Avoid Waterfalls Like a Pro

Let’s recap what you’ve learned:

ProblemCauseFix
Data waterfallsFetch inside use() callsTrigger Promises early
Nested waterfallsChild fetches after parent suspendsPreload in parent
Image loading delays<img src> starts lateUse preloadImage()
Lost cache on refreshCache lost on reloadUse Cache-Control headers

🤖 What About React 19?

Good news: React 19 improves automatic parallel loading. Some of the waterfall issues described here have been fixed behind the scenes.

But it’s still important to understand:

  • How waterfalls happen
  • How to spot them in DevTools
  • How to preload resources like images
  • How to leverage server-side cache for persistency

🚢 TL;DR: Build Faster React Apps

React Suspense gives you incredible power—but if you don’t understand waterfalls, you might end up building a slower app than intended.

Avoid waterfalls by:

  • Triggering all data fetches early
  • Preloading images
  • Using Cache-Control headers on your API
  • Using tooling like Remix that avoids waterfalls for you

Stay sharp, preload smart, and render fast. 🧠💥


📚 Learn more:

  • Render-as-you-fetch strategy
  • React 19 Suspense enhancements
  • Remix: Load data without waterfalls

Tags

#React

Share

Previous Article
React Suspense: Responsive

Table Of Contents

1
💧 What's a Waterfall?
2
⚠️ Why React Suspense Makes Waterfalls Easy (Too Easy)
3
✅ Solution: Trigger Promises Before use
4
🧱 Hidden Waterfalls in Nested Components
5
🛠 Bonus: Build a loadData Helper
6
🧠 Why Not Just Use Props?
7
🖼️ Preloading Images to Avoid Visual Waterfalls
8
🌐 Server-Side: Cache-Control Headers
9
🧪 Putting It All Together: Avoid Waterfalls Like a Pro
10
🤖 What About React 19?
11
🚢 TL;DR: Build Faster React Apps

Related Posts

React Suspense: Responsive
June 25, 2025
2 min
© 2025, All Rights Reserved.
Powered By

Quick Links

About Me

Legal Stuff

Social Media