search
HomeWeb Front-endCSS TutorialPre-Caching Images with React Suspense

Pre-Caching Images with React Suspense

React’s Suspense feature is exciting, and it’s coming, and will allow developers to easily delay rendering of their components until they are “ready”, resulting in a smoother user experience. "Prepare" can refer to many aspects here. For example, your data loading utility can be used in conjunction with Suspense to allow consistent loading status to be displayed while any data is being transferred without manually tracking the load status for each query. Then, when your data is available and your component is "ready", it will render. This is the topic that is most often discussed with Suspense, and I have written about it before; however, data loading is just one of the many use cases where Suspense can improve the user experience. Another use case I want to talk about today is image preloading.

Have you ever made or used a web app where your position jitters and jumps as the image is downloaded and rendered upon reaching the screen? We call it content rearrangement, and it’s both shocking and unpleasant. Suspense can help solve this problem. Did you know I said that the whole point of Suspense is to prevent component rendering until it is ready? Fortunately, "ready" is very open here - for our purposes, it can include "the preloaded images we need". Let's see how to do it!

Suspense Quick Start

Before digging into the details, let's quickly understand how Suspense works. It has two main parts. First of all, there is the concept of component hang. This means React is trying to render our component, but it is not "ready". When this happens, the closest "fallback" in the component tree will be rendered. We'll see how to make a fallback soon (which is fairly simple), but the component tells React that it's not ready yet is to throw a promise. React will capture the Promise, realize that the component is not ready yet, and render the fallback. When Promise parses, React will try rendering again. Repeat this process. Yes, I'm a little oversimplified, but that's the point of how Suspense works, and we'll expand on some of these concepts as we go.

The second part of Suspense is the introduction of a "transition" status update. This means we set the state, but tells React that state changes may cause the component to hang, and if this happens, the fallback is not rendered. Instead, we want to continue viewing the current screen until the status update is ready, at which point it will render. Of course, React provides us with a "pending" boolean indicator that lets developers know that this process is in progress so that we can provide inline load feedback.

Let's preload some images!

First, I want to point out that there is a full demo we are making at the end of this article. If you just want to jump into the code, feel free to open the demo now. It will show how Suspense is used in conjunction with transition state updates to preload images. The rest of this post will build the code step by step and explain how and why along the way.

OK, let's get started!

We want our components to hang until all images are preloaded. To simplify the operation as much as possible, let's create a<suspenseimage></suspenseimage> Component, which receives the src attribute, preloads the image, handles exception throws, and then renders a <img src="/static/imghwm/default1.png" data-src="https://img.php.cn/upload/article/000/000/000/174358885649082.png?x-oss-process=image/resize,p_40" class="lazy" alt="Pre-Caching Images with React Suspense"> Use images in HTML, but we can also create images in imperative manner using Image() object in JavaScript; in addition, the image we create this way has an onload callback that fires when the image... is loaded. It looks like this:

 const img = new Image();
img.onload = () => {
  // The image is loaded};

But how do we combine it with exception throwing? If you are like me, you may first think of something like this:

 const SuspenseImg = ({ src, ...rest }) => {
  throw new Promise((resolve) => {
    const img = new Image();
    img.onload = () => {
      resolve();
    };
    img.src = src;
  });
  Return<img alt="" src="%7Bsrc%7D"> ;
};

The problem, of course, is that this will always throw a promise. Every time React attempts to render<suspenseimg></suspenseimg> When an instance is in progress, a new promise is created and thrown immediately. Instead, we just want to throw a promise before the image loads. There is an old saying that every problem in computer science can be solved by adding a layer of indirection (except for the problem of too many layers of indirection), so let's do it and build an image cache. When we read src, the cache will check if it has loaded the image, and if not, it will start preloading and throw an exception. And, if the image is preloaded, it will just return true and let React continue rendering our image.

This is ours<suspenseimage></suspenseimage> What the component looks like:

 export const SuspenseImg = ({ src, ...rest }) => {
  imgCache.read(src);
  Return<img  src="%7Bsrc%7D" alt="Pre-Caching Images with React Suspense" > ;
};

Here is what the minimum version we cached looks like:

 const imgCache = {
  __cache: {},
  read(src) {
    if (!this.__cache[src]) {
      this.__cache[src] = new Promise((resolve) => {
        const img = new Image();
        img.onload = () => {
          this.__cache[src] = true;
          resolve(this.__cache[src]);
        };
        img.src = src;
      });
    }
    if (this.__cache[src] instanceof Promise) {
      throw this.__cache[src];
    }
    return this.__cache[src];
  }
};

It's not perfect, but it's enough for now. Let's keep using it.

accomplish

Remember, below is a link to a full working demo, so don't despair if I move too fast in any particular step. We will explain as we go.

Let's start by defining our fallback. We define the fallback by placing a Suspense tag in the component tree and pass our fallback attribute. Any pending component will search up the most recent Suspense tag and render its fallback (but if the Suspense tag is not found, an error will be thrown). A real application may have many Suspense tags throughout the process, defining specific fallbacks for its individual modules, but for this demonstration we only need a tag that wraps our root application.

 function App() {
  Return (
    <suspense fallback="{<Loading"></suspense> }>
      <showimages></showimages>
    
  );
}

<loading></loading> Components are a basic spinner, but in real applications you might want to render some kind of empty shell of the component that you are actually trying to render to provide a more seamless experience.

With this, ours<showimages></showimages> The component will eventually render our image using the following:

<div>
  {images.map((img) => (
    <div key="{img}">
      <suspenseimg alt="" src="%7Bimg%7D"></suspenseimg>
    </div>
  ))}
</div>

On initial loading, our loading spinners will be displayed until our initial images are ready, at which point they will be displayed simultaneously without any interlaced rearrangement lags.

Transition status update

Once the images are in place, when we load the next batch of images, we want them to appear after loading, of course, keeping the existing image on the screen as they load. We use the useTransition hook to do this. This returns a startTransition function and an isPending boolean which indicates that our status update is in progress but has been suspended (or even if it has not been suspended, it may still be true if the status update just takes too long). Finally, when useTransition is called, you need to pass a timeoutMs value, which is the maximum amount of time the isPending flag can be true, after which React will abandon and render the fallback (note that timeoutMs parameter may be deleted in the near future, and when an existing content is updated, the transition state update only needs to wait as long as possible).

This is what I look like:

 const [startTransition, isPending] = useTransition({ timeoutMs: 10000 });

We will allow 10 seconds to pass before our fallback display, which may be too long in real life, but is appropriate for this demonstration, especially if you may deliberately slow down the network in DevTools for experimentation.

Here is how to use it. When you click the button that loads more images, the code looks like this:

 startTransition(() => {
  setPage((p) => p 1);
});

This status update will trigger a new data load using my GraphQL client micro-graphql-react, which is compatible with Suspense and will throw a promise for us when the query is in progress. Once the data is returned, our component will attempt to render and hang again when our image is preloaded. While all of this happens, our isPending value will be true, which will allow us to display the loading spinner at the top of the existing content.

Avoid Internet waterfalls

You may be wondering how React can block rendering while image preloading is in progress. Using the above code, when we do this:

 {images.map((img) => (

...and what is rendered in it<suspenseimage></suspenseimage> , Will React try to render the first image, hang, and then retry the list, exceed the first image (now in our cache), and then hang the second image, then the third, 4th, etc. If you have read about Suspense before, you might be wondering if we need to manually preload all images in the list before all these renderings happen.

It turns out there is no need to worry or do awkward preloading, as React is pretty clever about how it renders things in the Suspense world. When React traverses our component tree, it does not stop when it encounters a pending. Instead, it continues to try to render all other paths in our component tree. So yes, when it tries to render image 0, a hang will occur, but React will continue to try to render image 1 to N before it hangs.

You can view this by viewing the Network tab in the full demo when a new image set is loaded. You should see the entire image bucket immediately displayed in the network list, parsed one by one, and when done, the results should be displayed on the screen. To really amplify this effect, you may want to reduce your network speed to "fast 3G".

For fun, we can force Suspense to traverse our images by manually reading each image from our cache before React tries to render our components, traversing every path in the component tree.

 images.forEach((img) => imgCache.read(img));

I created a demo to illustrate this. If you also view the Network tab when loading a new image set, you will see that they are added to the network list in order (but don't run this with slowing down the network).

Delayed hang

When using Suspense, you need to remember one inference: hang as late as possible in the rendering and the lower level of the component tree. If you have some rendering a bunch of suspended images<imagelist></imagelist> , make sure each image is suspended in its own component so React can access it separately so that no images block other images, resulting in a waterfall.

The data loading version of this rule is that the data should be loaded as much as possible by the components that actually require it. This means we should avoid doing the following in a component:

 const { data1 } = useSuspenseQuery(QUERY1, vars1);
const { data2 } = useSuspenseQuery(QUERY2, vars2);

The reason we want to avoid this is that query one will hang, and then query two, causing the waterfall. If this is not avoidable at all, we will need to manually preload both queries before hanging.

Demo

Here is a demonstration of my commitment. It's the same as the demo I linked above.

Open the demo If you run it and open the development tool, make sure to uncheck the Disable Cache box displayed in the DevTools Network tab or you will break the entire demo.

The code is almost the same as the one I showed before. One improvement in the demo is that our cache read method has the following lines:

 setTimeout(() => resolve({}), 7000);

Preload all images well, but in real life we ​​may not want to block rendering indefinitely simply because one or two behind images load slowly. So after a while we just give the green light, even if the image is not ready. Users will see an image or two flashing, but this is better than enduring the frustration of software freezing. I also want to point out that seven seconds may be too long, but for this demo, I assume that users may slow down the network in DevTools to see Suspense functionality more clearly and hopefully support this.

The demo also has a precache image check box. It is selected by default, but you can uncheck it to use normal<img alt="Pre-Caching Images with React Suspense" > Tag replacement<suspenseimage></suspenseimage> Component, if you want to compare the Suspense version to "normal React" (just don't check it when the result appears, otherwise the entire UI may hang and render the fallback).

Finally, like CodeSandbox, some states may sometimes be out of sync, so if things start to look weird or corrupt, click the refresh button.

Miscellaneous

I accidentally made a huge mistake when putting this demo together. I don't want the demo to lose its effect because the browser caches the image it has downloaded. So I manually modify all URLs using a cache breaker:

 const [cacheBuster, setCacheBuster] = useState(INITIAL_TIME);

const { data } = useSuspenseQuery(GET_IMAGES_QUERY, { page });
const images = data.allBooks.Books.map(
  (b) => b.smallImage `?cachebust=${cacheBuster}`
);

INITIAL_TIME is defined at the module level (i.e., globally):

 const INITIAL_TIME = new Date();

If you're wondering why I didn't do this:

 const [cacheBuster, setCacheBuster] = useState( new Date());

...This is because it can have terrible and terrible consequences. On the first rendering, the image attempts to render. Cache causes hang, React cancels rendering and displays our fallback. When all the Promises have been parsed, React will attempt to re-initial rendering and our initial useState call will be re-run , which means:

 const [cacheBuster, setCacheBuster] = useState( new Date());

... will be rerun with a new initial value resulting in a brand new set of image URLs, which will hang indefinitely again. The components will never run, and the CodeSandbox demo will stop running (which makes debugging frustrating).

This seems to be a strange special issue caused by the unique requirements of this particular demonstration, but there is a bigger lesson: the rendering should be pure and has no side effects. React should be able to try to re-render your component multiple times and (given the same initial prop) should get the same exact state from the other end.

The above is the detailed content of Pre-Caching Images with React Suspense. For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
How much specificity do @rules have, like @keyframes and @media?How much specificity do @rules have, like @keyframes and @media?Apr 18, 2025 am 11:34 AM

I got this question the other day. My first thought is: weird question! Specificity is about selectors, and at-rules are not selectors, so... irrelevant?

Can you nest @media and @support queries?Can you nest @media and @support queries?Apr 18, 2025 am 11:32 AM

Yes, you can, and it doesn't really matter in what order. A CSS preprocessor is not required. It works in regular CSS.

Quick Gulp Cache BustingQuick Gulp Cache BustingApr 18, 2025 am 11:23 AM

You should for sure be setting far-out cache headers on your assets like CSS and JavaScript (and images and fonts and whatever else). That tells the browser

In Search of a Stack That Monitors the Quality and Complexity of CSSIn Search of a Stack That Monitors the Quality and Complexity of CSSApr 18, 2025 am 11:22 AM

Many developers write about how to maintain a CSS codebase, yet not a lot of them write about how they measure the quality of that codebase. Sure, we have

Datalist is for suggesting values without enforcing valuesDatalist is for suggesting values without enforcing valuesApr 18, 2025 am 11:08 AM

Have you ever had a form that needed to accept a short, arbitrary bit of text? Like a name or whatever. That's exactly what is for. There are lots of

Front Conference in ZürichFront Conference in ZürichApr 18, 2025 am 11:03 AM

I'm so excited to be heading to Zürich, Switzerland for Front Conference (Love that name and URL!). I've never been to Switzerland before, so I'm excited

Building a Full-Stack Serverless Application with Cloudflare WorkersBuilding a Full-Stack Serverless Application with Cloudflare WorkersApr 18, 2025 am 10:58 AM

One of my favorite developments in software development has been the advent of serverless. As a developer who has a tendency to get bogged down in the details

Creating Dynamic Routes in a Nuxt ApplicationCreating Dynamic Routes in a Nuxt ApplicationApr 18, 2025 am 10:53 AM

In this post, we’ll be using an ecommerce store demo I built and deployed to Netlify to show how we can make dynamic routes for incoming data. It’s a fairly

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

AI Hentai Generator

AI Hentai Generator

Generate AI Hentai for free.

Hot Article

R.E.P.O. Energy Crystals Explained and What They Do (Yellow Crystal)
1 months agoBy尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Best Graphic Settings
1 months agoBy尊渡假赌尊渡假赌尊渡假赌
Will R.E.P.O. Have Crossplay?
1 months agoBy尊渡假赌尊渡假赌尊渡假赌

Hot Tools

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

This project is in the process of being migrated to osdn.net/projects/mingw, you can continue to follow us there. MinGW: A native Windows port of the GNU Compiler Collection (GCC), freely distributable import libraries and header files for building native Windows applications; includes extensions to the MSVC runtime to support C99 functionality. All MinGW software can run on 64-bit Windows platforms.

DVWA

DVWA

Damn Vulnerable Web App (DVWA) is a PHP/MySQL web application that is very vulnerable. Its main goals are to be an aid for security professionals to test their skills and tools in a legal environment, to help web developers better understand the process of securing web applications, and to help teachers/students teach/learn in a classroom environment Web application security. The goal of DVWA is to practice some of the most common web vulnerabilities through a simple and straightforward interface, with varying degrees of difficulty. Please note that this software

SecLists

SecLists

SecLists is the ultimate security tester's companion. It is a collection of various types of lists that are frequently used during security assessments, all in one place. SecLists helps make security testing more efficient and productive by conveniently providing all the lists a security tester might need. List types include usernames, passwords, URLs, fuzzing payloads, sensitive data patterns, web shells, and more. The tester can simply pull this repository onto a new test machine and he will have access to every type of list he needs.

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor