Creating Infinite Scroll in AngularJS

Implementing Infinite Scroll in AngularJS for Mobile

I wanted to outline some of the implementations, difficulties, and challenges we ran into while building an infinite scroll for mobile using AngularJS.

We went from only being able to handle around 200 images to now handling over 3,000 images and counting.

A couple of good references that helped along the way:

Introduction

infinite

Infinite scroll is basically pagination on crack.

It allows for a better UX by reducing the number of clicks needed to access more data. This is especially useful when optimizing for mobile, where you can generally assume the user is holding the phone with one hand while scrolling and tapping with the other.

In fact, infinite scroll is pretty easy to implement in AngularJS if the list isn't very long.

The problem starts when the list gets really long.

AngularJS 1.3 introduced one-time binding, which helps move some of the performance bottleneck away from AngularJS's digest cycle. But once you solve some of those AngularJS performance problems, the DOM itself starts becoming the bottleneck.

Zuckerberg notoriously announced that relying heavily on HTML5 for mobile was one of the biggest mistakes Facebook made.

If Zuck couldn't do it, why should we?

Well, for one, we've come a long way since that announcement.


Table of Contents

Beware

Implementing a true infinite scroll isn't for the faint of heart.

It's hard, and there are a lot of things to consider and tradeoffs to be aware of before moving forward.

Here are a few reasons why you should not use infinite scroll:

  • Performance

    • Even with all the performance tweaks in the world, you're still going to hit bottlenecks on certain devices.
    • If your target device range includes Android 2.3, I would probably listen to Zuck on this one.
  • Uncanny Valley

    • When deploying a SPA, you're already trying to make a web application behave more like a native application.
    • Infinite scroll pushes you even closer to that native feel, which means users are going to notice even more when scrolling doesn't feel smooth.
  • Not the right tool for the job

    • Infinite scroll isn't automatically better UX.
    • If users need to easily return to a specific position, compare items, bookmark pages, or navigate through a structured list, traditional pagination may still be a better solution.
  • It's hard to implement

    • A simple infinite scroll is easy.
    • A smooth infinite scroll that works on older mobile devices without memory or performance issues is a completely different problem.

Implementation Is About Being a Magician

Creating an infinite scroll can almost be thought of like creating a magic trick.

You're creating the illusion that the user is scrolling through one extremely long list, while behind the scenes you're doing a lot of tricks to keep the application running smoothly.

I'll go over a few of the approaches we experimented with.

Hide the Elements

One approach is to simply hide DOM elements that are no longer visible.

Something like:

display: none;  

This can reduce some of the CPU work involved in rendering elements that are no longer visible.

The problem is that you quickly start running into memory issues.

Even though the DOM isn't visible anymore, the elements still exist. The browser still has those objects in memory, and eventually that memory usage can become large enough to slow down or even crash the browser.

There is another problem.

When you hide an element using display: none, its height disappears from the document. Everything else shifts to fill that space.

For an infinite scroll, that's a pretty big problem because the user's scroll position suddenly changes.

We'll address that issue later.

Use a Small Image in Place of Images No Longer in the Viewport

Another approach is to replace images that are no longer visible with a very small placeholder image.

This allows us to remove the larger image from memory and gives garbage collection a chance to clean some things up.

When the user scrolls back to that section, we can reload the original image.

Instead of requesting the same image from the server again, we can usually take advantage of the browser cache to reload it quickly.

By explicitly setting the width and height of the placeholder, we also avoid the layout shifting problem caused by simply hiding the element.

For many projects, this approach may actually be good enough.

Unfortunately, it wasn't good enough for ours.

Since our target devices included Android 2.3+, we started seeing issues pretty quickly on actual devices.

Garbage collection was kicking in and memory was being released because we were replacing larger images with smaller ones.

However, CPU usage continued climbing the farther we scrolled down the page.

For the user, this resulted in lag and terrible FPS.

Even though the images were smaller, the browser still had to deal with the same number of DOM elements.

In other words, we solved the memory bottleneck but moved the performance problem over to the CPU.

Remove the Elements

Finally, let's go over the approach that this post is really about.

Actually removing DOM elements.

Removing elements is probably one of the last things you want to do because it introduces a lot of additional problems.

I would only go with this approach if you absolutely need to.

That being said, removing elements solves both of the major problems we were running into.

If the elements are removed, objects can be dereferenced and garbage collection can properly clean them up.

Chrome DevTools has some very useful tools for watching memory allocation and deallocation while testing this.

Also, because elements outside of the viewport no longer exist, the browser doesn't have to continue rendering them. This helps keep CPU usage much more consistent as the user continues scrolling.

When the user scrolls back up, the removed elements can be recreated and the images can once again take advantage of the browser cache.

Of course, removing elements introduces another major problem.

The DOM collapses.

If you remove 1,000 pixels worth of content above the user, the browser is going to shift everything upward by 1,000 pixels.

So we need some kind of filler element that takes the place of the removed DOM and preserves the overall height of the page.

There's another issue as well.

Previously, the DOM itself may have been holding some of our state.

Once we're removing and recreating DOM elements, we can no longer depend on the DOM for that state.

The state now has to be tracked somewhere else.


Implementation

Implementing DOM Removal

There are a couple of important things we need to account for.

  • Displace the height of the removed DOM

    • When elements are removed, we need to replace their height with some kind of spacer or filler element.
    • Otherwise, the page will jump and the user's scroll position will change.
  • Keep track of DOM dimensions

    • This sounds obvious, but it becomes much more complicated when you're dealing with responsive layouts.
    • A device changing from landscape to portrait can completely change the size of the images and the total height of the content.

When the user rotates their smartphone from landscape to portrait, we have to recalculate the dimensions of the content while also keeping the user at roughly the same place in the list.

It was pretty important that we kept the scrolling experience feeling natural.

User experience comes first, right?

A perfect implementation would mean the user never notices anything happening in the background.

They shouldn't know that we're removing elements, rebuilding elements, swapping images, or recalculating the size of the page.

There are a few things that need to be considered when removing and recreating elements:

  • Height displacement
  • Time required to rerender
  • Maintaining scroll position
  • Mobile device orientation changes
  • Image loading and browser caching
  • Garbage collection timing

Gotchas

There are quite a few things that can go wrong once you start removing parts of the DOM.

  • Page placement

    • You need to make sure the user's position doesn't suddenly jump when elements are removed or recreated.
  • Responsive design

    • Changing the browser size or rotating a mobile device means the dimensions of previously removed content may no longer be correct.
  • No longer relying entirely on AngularJS

    • Once you're manually managing what exists in the DOM, you're stepping outside some of the normal AngularJS behavior.
    • You have to be much more careful about how state is managed and how elements are recreated.
  • Garbage collection

    • Garbage collection can itself cause pauses on slower mobile devices.
    • Depending on the rate of memory consumption, you may want to increase the delay between loading additional data so the browser has more opportunities to perform garbage collection.

I noticed Facebook doing something similar with their infinite scrolling.

Another thing to remember is that predictability is good UX.

If you're going to introduce a slightly longer loading delay, try to keep that timing consistent. A predictable pause feels much better than scrolling that randomly speeds up and slows down.

  • Adding other types of content
    • Things get even more complicated if the infinite list isn't made up of identical items.
    • Adding things like carousels, advertisements, or different-sized content means your height calculations become much harder.

Summary

If you can help it, don't include infinite scroll in your project.

It can be a huge pain to implement correctly and an even bigger pain to maintain.

For smaller lists, AngularJS makes infinite scrolling pretty straightforward. But once you're dealing with thousands of images and supporting older mobile devices, you have to start thinking about much more than simply loading the next page of data.

Memory, CPU usage, garbage collection, DOM size, scroll position, responsive design, and device orientation all start becoming part of the problem.

In our case, removing elements outside of the viewport and recreating them when necessary was the approach that finally allowed us to go from roughly 200 images to more than 3,000.

It's definitely not the simplest solution.

But sometimes creating a smooth infinite scroll really is about being a magician.

Comments powered by Disqus