admin管理员组

文章数量:1401681

My website is too heavy because it downloads 200-400 images after fetching data from the server (Google's Firebase Firestore).

I came up with two solutions and I hope somebody answers one of them:

  • I want to set each img to have a loading state and enable visitors to see the placeholder image until it is loaded. As I don't know how many images I get until fetching data from the server, I find it hard to initialize image loading statuses by useState. Is this possible? Then, how?
  • How can I Lazy load images? Images are initialized with a placeholder. When a scroll es near an image, the image starts to download replacing the placeholder.
function sample() {}{
  const [items, setItems] = useState([])
  const [imgLoading, setImgLoading] = useState(true)  // imgLoading might have to be boolean[]
  useEffect(() => {
    axios.get(url).
    .then(response => setItems(response.data))
  }, [])
  return (
    items.map(item => <img src={item.imageUrl} onLoad={setImgLoading(false)} />)
  )
}

My website is too heavy because it downloads 200-400 images after fetching data from the server (Google's Firebase Firestore).

I came up with two solutions and I hope somebody answers one of them:

  • I want to set each img to have a loading state and enable visitors to see the placeholder image until it is loaded. As I don't know how many images I get until fetching data from the server, I find it hard to initialize image loading statuses by useState. Is this possible? Then, how?
  • How can I Lazy load images? Images are initialized with a placeholder. When a scroll es near an image, the image starts to download replacing the placeholder.
function sample() {}{
  const [items, setItems] = useState([])
  const [imgLoading, setImgLoading] = useState(true)  // imgLoading might have to be boolean[]
  useEffect(() => {
    axios.get(url).
    .then(response => setItems(response.data))
  }, [])
  return (
    items.map(item => <img src={item.imageUrl} onLoad={setImgLoading(false)} />)
  )
}
Share Improve this question edited Aug 14, 2020 at 2:17 Wt.N asked Aug 14, 2020 at 0:41 Wt.NWt.N 1,6562 gold badges22 silver badges42 bronze badges 6
  • You may also want to associate some loading state with each image versus just a single overall loading state, or is that what you are actually asking about? – Drew Reese Commented Aug 14, 2020 at 0:46
  • I want to make imgLoading[] whose length is the array length in the response from the server, but I don't know the length until I get server response. – Wt.N Commented Aug 14, 2020 at 1:01
  • Answer to your 2nd bullet is I think this library npmjs./package/react-lazy-load – Jacob Commented Aug 14, 2020 at 1:45
  • You fetch all your image urls, and your mapping each of them to display in image? – bertdida Commented Aug 14, 2020 at 2:00
  • 1 Something like this? – bertdida Commented Aug 14, 2020 at 2:18
 |  Show 1 more ment

3 Answers 3

Reset to default 3

I would create an Image ponent that would handle it's own relevant states. Then inside this ponent, I would use IntersectionObserver API to tell if the image's container is visible on user's browser or not.

I would have isLoading and isInview states, isLoading will be always true until isInview updates to true.

And while isLoading is true, I would use null as src for the image and will display the placeholder.

Load only the src when container is visible on user's browser.

function Image({ src }) {
  const [isLoading, setIsLoading] = useState(true);
  const [isInView, setIsInView] = useState(false);
  const root = useRef(); // the container

  useEffect(() => {
    // sets `isInView` to true until root is visible on users browser

    const observer = new IntersectionObserver(onIntersection, { threshold: 0 });
    observer.observe(root.current);

    function onIntersection(entries) {
      const { isIntersecting } = entries[0];

      if (isIntersecting) { // is in view
        observer.disconnect();
      }

      setIsInView(isIntersecting);
    }
  }, []);

  function onLoad() {
    setIsLoading((prev) => !prev);
  }

  return (
    <div
      ref={root}
      className={`imgWrapper` + (isLoading ? " imgWrapper--isLoading" : "")}
    >
      <div className="imgLoader" />
      <img className="img" src={isInView ? src : null} alt="" onLoad={onLoad} />
    </div>
  );
}

I would also have CSS styles that will toggle the placeholder and image's display property.

.App {
  --image-height: 150px;
  --image-width: var(--image-height);
}

.imgWrapper {
  margin-bottom: 10px;
}

.img {
  height: var(--image-height);
  width: var(--image-width);
}

.imgLoader {
  height: 150px;
  width: 150px;
  background-color: red;
}

/* container is loading, hide the img */
.imgWrapper--isLoading .img {
  display: none;
}

/* container not loading, display img */
.imgWrapper:not(.imgWrapper--isLoading) .img {
  display: block;
}

/* container not loading, hide placeholder */
.imgWrapper:not(.imgWrapper--isLoading) .imgLoader {
  display: none;
}

Now my Parent ponent, will do the requests for all the image urls. It would also have its own isLoading state that when set true would display its own placeholder. When the image url's request resolves, I would then map on each url to render my Image ponents.

export default function App() {
  const [imageUrls, setImageUrls] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetchImages().then((response) => {
      setImageUrls(response);
      setIsLoading((prev) => !prev);
    });
  }, []);

  const images = imageUrls.map((url, index) => <Image key={index} src={url} />);

  return <div className="App">{isLoading ? "Please wait..." : images}</div>;
}

There are libraries for this, but if you want to roll your own, you can use an IntersectionObserver, something like this:

const { useState, useRef, useEffect } = React;

const LazyImage = (imageProps) => {
  const [shouldLoad, setShouldLoad] = useState(false);
  const placeholderRef = useRef(null);

  useEffect(() => {
    if (!shouldLoad && placeholderRef.current) {
      const observer = new IntersectionObserver(([{ intersectionRatio }]) => {
        if (intersectionRatio > 0) {
          setShouldLoad(true);
        }
      });
      observer.observe(placeholderRef.current);
      return () => observer.disconnect();
    }
  }, [shouldLoad, placeholderRef]);

  return (shouldLoad 
    ? <img {...imageProps}/> 
    : <div className="img-placeholder" ref={placeholderRef}/>
  );
};

ReactDOM.render(
  <div className="scroll-list">
    <LazyImage src='https://i.insider./536a52d9ecad042e1fb1a778?width=1100&format=jpeg&auto=webp'/>
    <LazyImage src='https://www.denofgeek./wp-content/uploads/2019/12/power-rangers-beast-morphers-season-2-scaled.jpg?fit=2560%2C1440'/>
    <LazyImage src='https://i1.wp./www.theilluminerdi./wp-content/uploads/2020/02/mighty-morphin-power-rangers-reunion.jpg?resize=1200%2C640&ssl=1'/>
    <LazyImage src='https://m.media-amazon./images/M/MV5BNTFiODY1NDItODc1Zi00MjE2LTk0MzQtNjExY2I1NTU3MzdiXkEyXkFqcGdeQXVyNzU1NzE3NTg@._V1_CR0,45,480,270_AL_UX477_CR0,0,477,268_AL_.jpg'/>
  </div>,
  document.getElementById('app')
);
.scroll-list > * {
  margin-top: 400px;
}

.img-placeholder {
  content: 'Placeholder!';
  width: 400px;
  height: 300px;
  border: 1px solid black;
  background-color: silver;
}
<div id="app"></div>

<script src="https://cdnjs.cloudflare./ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

This code is having them load as soon as the placeholder is visible on the screen, but if you want a larger detection margin, you can tweak the rootMargin option of the IntersectionObserver so it starts loading while still slightly off screen.

Map the response data to an array of "isLoading" booleans, and update the callback to take the index and update the specific "isLoading" boolean.

function Sample() {
  const [items, setItems] = useState([]);
  const [imgLoading, setImgLoading] = useState([]);

  useEffect(() => {
    axios.get(url).then((response) => {
      const { data } = response;
      setItems(data);
      setImgLoading(data.map(() => true));
    });
  }, []);

  return items.map((item, index) => (
    <img
      src={item.imageUrl}
      onLoad={() =>
        setImgLoading((loading) =>
          loading.map((el, i) => (i === index ? false : el))
        )
      }
    />
  ));
}

本文标签: javascriptReact How do you lazyload image from API responseStack Overflow