Dev-Raldin
Dev-Raldin

Reputation: 33

Vuejs v-for so laggy when infinite scrolling

I have this weird vuejs effect where when I am adding a new object data, the v-for re-renders all the object even if its already rendered.

I am implementing an infinite scroll like facebook.

The Code

To explain this code, I am fetching a new data from firebase and then push the data into the data object when it reaches the bottom of the screen

var vueApp = new Vue({

    el: '#root',
    data: {
        postList: [],
        isOkaytoLoad: false,
        isRoomPostEmpty: false,
    },
    mounted: function() {
        // Everytime user scroll, call handleScroll() method
        window.addEventListener('scroll', this.handleScroll);
    },
    methods: {
        handleScroll: function()
        {
            var d = document.documentElement;
            var offset = d.scrollTop + window.innerHeight;
            var height = d.offsetHeight - 200;
            
            // If the user is near the bottom and it's okay to load new data, get new data from firebase
            if (this.isOkaytoLoad && offset >= height)
            {
                this.isOkaytoLoad = false;
                (async()=>{
                    const lastPost = this.postList[this.postList.length - 1];
                    const room_id = PARAMETERS.get('room');

                    const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
                    const roomSnapshot = await getDocs(q);
                    roomSnapshot.forEach((doc) => {
                        const postID = doc.id;
                        (async()=>{
                            // Put the new data at the postList object
                            this.postList = [...this.postList, doc];
                            const q = query(collection(db, 'comments'), where('post_id', '==', postID));
                            const commentSnapshot = await getDocs(q);
                            doc['commentCount'] = commentSnapshot.size;
                            //this.postList.push(doc);
                            console.log(this.postList);
                            setTimeout(()=>{ this.isOkaytoLoad = true }, 1000);
                        })();
                    });
                    
                    
                })();
            }
        }
    }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div v-if="postList.length > 0" class="card-containers">
   <!-- I have a component `Postcard` in my js file and it works well -->
   <Postcard 
      v-for="post in postList" 
      :key="post.id" 
      :post-id="post.id" 
      :owner-name="post.data().owner_displayName" 
      :owner-uid="post.data().owner_id" 
      :post-type="post.data().post_type"
      :image-url="post.data().image_url"
      :post-content="truncateString(linkify(post.data().post_content))"
      :room="post.data().room" 
      :time="post.data().time.toDate()"
      :likers="post.data().likers"
      :comment-count="post.commentCount"
      :file-url="post.data().file_url"
      :file-name="post.data().file_name"
      :downloads="post.data().downloads">
   </Postcard>
</div>

Now, the problem is here ...

Look at this screen record, FOCUS AT THE MOUSE, it's lagging and I can't even click on those buttons when vuejs is adding and loading the new data

I can't click when new data is loading

Here is the code That I used

What I suspect

I am suspecting that everytime I add a new data, the VueJS re-renders it all, which does that effect. How can I force vueJS to not re-render those data that is already rendered in the screen?

Upvotes: 0

Views: 1310

Answers (1)

Decade Moon
Decade Moon

Reputation: 34286

You've got two unnecessary async IIFE; the second one inside the forEach is particularly problematic because the async code inside it will be executed concurrently for each loop iteration, which has implications:

  1. getDocs() will be fired all at once for each loop ieration, potentially spamming the server (assuming this is performing a network request). Was this your intention? It looks like you're only fetching at most 5 new posts, so this is probably OK.
  2. The async function updates some state which will trigger Vue to re-render for each doc. This should be batched together at the end so Vue does as minimal updates as possible.

Also don't use var; use const or let instead. There's almost no good reason to use var anymore, let it die.

I can't say this will improve your performance substantially, but I recommend making the following changes (untested):

async handleScroll() {
  const d = document.documentElement;
  const offset = d.scrollTop + window.innerHeight;
  const height = d.offsetHeight - 200;
  
  // If the user is near the bottom and it's okay to load new data, get new data from firebase
  if (this.isOkaytoLoad && offset >= height) {
    // Prevent loading while we load more posts
    this.isOkaytoLoad = false;

    try {
      // Get new posts
      const lastPost = this.postList[this.postList.length - 1];
      const room_id = PARAMETERS.get('room');
      const q = query(collection(db, 'posts'), where('room', '==', room_id), orderBy("time", "desc"), limit(5), startAfter(lastPost));
      const roomSnapshot = await getDocs(q);

      // Fetch comments of each post. Do this all at once for each post.
      // TODO: This can probably be optimized into a single query
      // for all the posts, instead of one query per post.
      await Promise.all(roomSnapshot.docs.map(async doc => {
        const postID = doc.id;
        const q = query(collection(db, 'comments'), where('post_id', '==', postID));
        const commentSnapshot = await getDocs(q);
        doc.commentCount = commentSnapshot.size;
      }));

      // Append the new posts to the list
      this.postList.push(...roomSnapshot.docs);
    } catch (ex) {
      // TODO: Handle error
    } finally {
      // Wait a bit to re-enable loading
      setTimeout(() => { this.isOkaytoLoad = true }, 1000);
    }
  }
}

Doing :post-content="truncateString(linkify(post.data().post_content))" in the template means linkify will be executed during each re-render. I suspect linkify may be potentially slow for long lists? Can this be pre-calculated for each post ahead of time?

You're registering a window scroll event listener when the component is mounted. If the component is ever destroyed, you need to unregister the event listener otherwise it'll still fire whenever the window is scrolled. This may not be an issue in your case, but for reusable components it must be done.

Upvotes: 2

Related Questions