Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Lazy loading React components
Christian Ascone
Christian Ascone

Posted on • Originally published atdingdongbug.hashnode.dev on

     

Lazy loading React components

Over the life of an application, performance optimization can play a crucial role in its success or failure and I recently encountered an issue that severely impacted the user experience.

Providing some context, my requirement was to show a list of N items using React. Unfortunately, I couldn’t use infinite scrolling, and each item needed to fetch its own data via separate HTTP calls.

Initially, I successfully implemented a component able to fetch data during initialization, nothing difficult, but performance was really bad when there were many items on screen and as previously mentioned I could not use infinite scrolling or pagination due to backend limitations, leaving frontend adjustments as the sole option.

Setup

Let’s walk through a sample project that illustrates the problem and demonstrates a solution.

I chose:

  • #"https://jsonplaceholder.typicode.com/posts">https://jsonplaceholder.typicode.com: For fetching sample data. It's a "Free fake and reliable API for testing and prototyping."

  • Vite: Recommended after create-react-app deprecation.

  • npm: So that everyone can feel comfortable.

You’ll find the link to the complete project at the end of this post, but let’s cover all the steps here.

Our goal is to initiate the (potentially) resource-intensive fetch only when the component becomes visible to the user, otherwise we would be performing unnecessary work that could compromise the overall performance.

Let's start creating a new react project using Vite.

❯ npm create vite@latest✔ Project name: … react-render-visible✔ Select a framework: › React✔ Select a variant: › JavaScriptScaffolding projectinreact-render-visible...
Enter fullscreen modeExit fullscreen mode

First, we need a very simple component that fetches data from APIs and renders it in DOM. This is how it initially looks:

importReact,{useState,useEffect}from'react';constNinjaComponent=()=>{const[data,setData]=useState(null);useEffect(()=>{// fetch data},[]);return(<div>{/* Show data */}</div>);};exportdefaultNinjaComponent;
Enter fullscreen modeExit fullscreen mode

Now, let's add some logic retrieving fake data fromhttps://jsonplaceholder.typicode.com. It offers a set of basic CRUD operations hence we are going to implement a GET by id provided as input parameter of the component.

Example:https://jsonplaceholder.typicode.com/photos/1

// 1) Add `id` parameterconstNinjaComponent=({id})=>{const[data,setData]=useState(null);// 2) Fetch data using `id` in url and update `data`useEffect(()=>{fetch(`https://jsonplaceholder.typicode.com/photos/${id}`).then((response)=>response.json()).then((data)=>setData(data)).catch((error)=>console.error("Error:",error));},[]);// 3) If `data` is set we render id, title and an image, else it shows a loading textreturn(<divstyle={{border:'1px dashed yellow'}}>{data?(<><p>{data.id}</p><p>{data.title}</p><imgsrc={data.thumbnailUrl}/></>):(<p>Loading...</p>)}</div>);};
Enter fullscreen modeExit fullscreen mode

We can add it to our home page to check how it works. Let's make the necessary edits inApp.jsx.

import"./App.css";importNinjaComponentfrom"./components/NinjaComponent";functionApp(){return(<><h1>DingDongBug</h1><divclassName="card"><NinjaComponentid={1}/></div></>);}exportdefaultApp;
Enter fullscreen modeExit fullscreen mode

With the initial setup complete, we’ve built our data component and integrated it into the homepage to display sample data. Now, let’s address a performance concern.

Time to start the project:npm run dev

Project startup

Good, but I've got 1000 elements!

While the NinjaComponent works well, it’s not exactly stealthy. What if we need to render a ton of that?

Try updating theApp.jsx with 500 or 1000 instances of NinjaComponent.

...{Array.from({length:1000},(_,index)=>(<NinjaComponentid={index+1}/>))}...
Enter fullscreen modeExit fullscreen mode

After relaunching the project, check the console:

Requests and resources with 1000 elements

This is an insane amount of wasted resources especially if you are not "using" all those components because only the firstone or two are on screen.

Solution: IntersectionObserver

Screen boundaries and elements

The green area represents what the user actually sees on the screen, while the red area includes elements already in DOM, but currently off-screen.

To manage on-screen visibility, theIntersectionObserver API comes to our aid. It "provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport" so basically it can determine exactly what we need.

More in detail we will use two methods provided by IntersectionObserver:

  • Theobserve() method is like telling the IntersectionObserver to start keeping an eye on a specific element. When the element’s visibility changes in relation to the viewport or a specified ancestor element, the IntersectionObserver will take note and execute its callback function. This function will receive an array of IntersectionObserverEntry objects, each representing a visibility change event.

  • Thedisconnect() method tells the IntersectionObserver to stop watching all of its target elements. It’s like saying, “You can rest now, no need to track any more changes.” This is particularly useful when you no longer need to monitor the visibility changes of the target elements.

With this in mind it is possible to proceed creating a custom hook that will be integrated in NinjaComponent so that it knows when fetch can start effectively filling data.

import{useEffect,useState}from"react";constuseFetchOnVisible=(ref,fetchFn)=>{const[data,setData]=useState(null);// the state to store the fetched dataconst[loading,setLoading]=useState(false);// the state to indicate the loading statususeEffect(()=>{// create a new intersection observerconstobserver=newIntersectionObserver((entries)=>{// get the first entryconstentry=entries[0];// if the entry is visible and not loading (and we still don't have data)if(entry.isIntersecting&&!loading&&!data){// set the loading state to truesetLoading(true);// call the fetch function and set the data statefetchFn().then((data)=>{console.log("data",data);setData(data);setLoading(false);});}},{threshold:0.1},// the ratio of the element's area that is visible);// observe the ref elementobserver.observe(ref.current);// unobserve the ref element when the component unmountsreturn()=>{observer.disconnect();};},[ref,data,fetchFn,loading]);// the dependencies of the effect// return the data and loading statereturn[data,loading];};exportdefaultuseFetchOnVisible;
Enter fullscreen modeExit fullscreen mode

Thethreshold in IntersectionObserver is like a visibility marker for your target element. It tells the IntersectionObserver when to notify you about the visibility changes of the target element.

  • Athreshold of 0.0 means that even a single visible pixel counts as the target being visible.

  • Athreshold of 1.0 means that the entire target element must be visible for the callback to be invoked.

With hook available we just need to update the NinjaComponent behaviour moving the fetch function inside the hook itself.

importReact,{useRef}from"react";importuseFetchOnVisiblefrom"../hooks/useFetchOnVisible";constNinjaComponent=({id})=>{// `ref` places a target on object to trackconstref=useRef(null);// fetch function has been moved out of the useEffect hook.// it just fetches data and parse json.constgetSource=()=>fetch(`https://jsonplaceholder.typicode.com/photos/${id}`)// replace with your API endpoint.then((response)=>response.json());// !!! use the custom hook with `ref` and fetch functionconst[data,loading]=useFetchOnVisible(ref,getSource);// UI does not change, it just add the `ref` tag in order to track divreturn(<divref={ref}style={{border:"1px dashed yellow"}}>{data&&!loading?(<><p>{data.id}</p><p>{data.title}</p><imgsrc={data.thumbnailUrl}/></>):(<p>Loading...</p>)}</div>);};exportdefaultNinjaComponent;
Enter fullscreen modeExit fullscreen mode

Ready to test?

Lazy loading in action

Assuming everything is properly configured, let’s open the page and observe what happens in the console. We’ll pay close attention to loading times and downloaded resources.
Request and resources with lazy loading

The page opening now requires few requests, consumes fewer resources, and the total loading time has dramatically improved — from around 10 seconds to a mere 0.5 seconds!

Lazy loading while scrolling

As we scroll down the page, theIntersectionObserver detects the moment when a component becomes visible and triggers the fetch function to retrieve data as needed.

Even increasing the number of components to 2000, the request count at startup remains unchanged and performance does not suffer. All thanks to fetching data precisely when it’s required, rather than preemptively! 🎉

GitHub: Source code

Top comments(2)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
ezpieco profile image
Ezpie
Creator of the open-source social media app lambda.
  • Joined
• Edited on• Edited

That's quite useful, especially if your app takes almost 15 seconds to load because of fetching data. Thanks a lot for this post@christianascone!

CollapseExpand
 
christianascone profile image
Christian Ascone
Hi, it's Christian. I develop software for work since 2014 and I'm very proud of my bugs.
  • Location
    Bologna, Italy
  • Joined

You're welcome. This really saved me from 5 minutes of loading time.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Hi, it's Christian. I develop software for work since 2014 and I'm very proud of my bugs.
  • Location
    Bologna, Italy
  • Joined

More fromChristian Ascone

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp