RAD.js is a framework that allows to build
mobile applications faster.
Optimized for iOS, Android, and
Windows Phone 8, as well as supporting all
major Web browsers.

View on GitHub Download
All articles:

infinite scroll

infinite scroll

Such term as infinite scroll can be rather frequently met in front-end development. First we’d like to describe this term in a more precise way, describe how we understand it, and find out the consequences of misinterpreting the term. Then we’d like to analyze means of its implementation, techniques, patterns and mistakes.

What do web developers usually understand as infinite scroll? As a rule, the most frequently mentioned explanation can be divided into two steps (for simplicity, we shall examine vertical scroll):

Step 1. The parent container is filled with a predefined number of list items, which exceeds the screen size.

Step 2. When a user sees the last existing list items, a portion of new list items is uploaded to the parent container, placed after the existing ones. This allows to scroll the new portion of items to the end; this step is repeated the necessary number of times.

list2

The drawback of this approach is that with each step 2 repeated the number of list items grows. This leads to growth of the load on the device’s memory and processor. If memory usage is a crucial factor (for example, complex markup or mobile devices with limited performance), this way of implementation becomes almost inapplicable.

In this case we can apply an upgraded variant of the approach described above – when the upper list items become invisible, they are deleted from DOM. And if the user tries to scroll down the container to see the content deleted from the list, the needed list items are uploaded again. Hereby we mean uploading the items to DOM. The key moment of this technique is the limited number of list items in DOM.

list1

The bottleneck of this upgrade is regular creation and deletion of list items. If list items are ‘heavy’, (for example, contain complex markup), instead of the first case, when there’s a gradual leak of memory and processing power, we’ll have two cases when lags appear:

1. When new list items are being uploaded;

2. When garbage collector is clearing the garbage after the deleted items.

Unfortunately, whereas the first lag (which is related to creation of items, their compose and reflow) can be decorated with a progress bar, the moment, when garbage collector is launched, cannot be tracked; as well we cannot show JavaScript or CSS animation while it’s running – that’s because garbage collector stops all execution flows.

To overcome these drawbacks, can be applied an approach with reuse of the existing list items, which are invisible to the user.

Functionally it looks like this: the item that becomes hidden from the user along the scroll, is moved beyond the opposite side of the screen (from the top down or from the bottom up). A special object or function (adapter) changes the content in the list item. Thus the items are reused instead of being re-created.

list3

It’s exactly this infinite scroll that we’d like to discuss from the standpoint of implementation and optimization.

But we should start with formalization of requirements to infinite scroll, because it is going to play a key part further on:

  • smooth animation of scroll and fling;
  • quick learning for developers, possibility of use with any framework;
  • it must work on mobile devices, including low-performance ones and older OS versions (such as Android 2.x).

To start the development, we divided scroll into functional parts, which were developed separately:

  • gesture adapter – recognition and work with gestures;
  • object pool – a bit later about that;
  • scroll view;
  • animator;
  • list view – view logic, related to placement and reuse of list items;
  • scroll bar – decoration functionality. What’s worth mentioning is that we rejected the use of scroll bar as a part of infinite scroll, and made it just a decoration. This allows any developer not only to customize its style, but also complement it with necessary functionality without bothering other parts of infinite scroll.

We pursued the goal of creating a toolkit, where each tool can be separately used for implementation of its specific tasks, without referencing others. Thus we can make a good start for the development of a toolkit (including frequently used UI widgets) for most frequent tasks in mobile, web, and cross-platform development.

gesture adapter

gesture adapter recognizes and transmits the following events to the listener: “tap” “longtap” “pointerdown” “pointerup” “pointermove”, “fling”. There is an issue here that’s very important for mobile devices – how to transmit events and their parameters to the listener.

The problem is that if we generate custom events in DOM, like it’s done in many libraries – this results in a very undesired effect on mobile devices – lag. The thing is, we have to create a custom event on the JavaScript side and send it to DOM, and it has to bypass a significant part of DOM tree. Only when it reaches the item with the listener element, it’s sent back to JavaScript.

As a solution to the lag issue, we can advise not to use custom DOM events, but rather to send events directly to JavaScript, with the help of the objects containing the needed attributes. It’s desirable to transmit the event object, not the coordinates, type etc.; or you can use different callback methods for different events, because the quantity of attributes may vary. This helps organize low coupling between independent modules.

But here’s the other side of the medal: events from touchscreen or mouse appear very frequently, and under this solution we will constantly generate large numbers of small JavaScript objects. In the end these objects will have to be gathered by garbage collector, but as we previously said, its work stops all execution flows, which leads to undesired effects for the user.

object pool

The solution to this situation can be object pool. When it’s needed to send a new event, it’s requested from the pool:

    event = eventPool.get();

The event is filled and sent to the listener. When it’s processed, it simply gets released:

   
    mView.handleEvent = function (e) {
        . . .
        e.release();
    };

If there are no free objects in the pool, a new one will be created, using the constructor object transmitted to the pool.

This approach solves the problem of the garbage collector by the work with a stream of numerous same-type small objects, because after the release of an event it returns to the unused pool objects, and becomes available for further filling and use, instead of being created again.

scroll view

scroll view – module which is responsible for scrolling content. A lot has been written on optimization of this operation. We’d like to address a few issues:

  • You don’t have to use overflow: auto or scroll for the following reasons: on different mobile platforms it’s rather hard to write an observer which correctly observes the scroll coordinates. On Android (versions 2.3.x and older) this does not work for nested containers, and there’s no guarantee that this won’t happen again on mobile platforms that are yet to be released in the future. Besides, native scroll on some platforms might not trigger scroll events.
  • It’s necessary to use window.requestAnimationFrame – enough has been written about this, and some subtleties of its use will be discussed a bit later, in animation section.
  • It’s necessary to use GPU power on mobile devices: using CSS, one can move scroll content to a separate layer, accelerated by graphical coprocessor.

Afterwards setting position for inner container looks like this:

    var mTransitionArray = [];
    . . .
    mTransitionArray[0] = "translate3d(0, ";
    mTransitionArray[2] = "px, 0) scale(1)";
    . . .
    mView.setPosition = function (position) {
        mTransitionArray[1] = position;
        mScrollingWrapper.style[mTransformName] = mTransitionArray.join("");
    };

We’d like to pay attention to how the line is concatenated for setting the content transformation. It’s a disputable question but as far as we presume, had only the lines translate3d(0, “ and “px, 0) scale(1)” been used in the function – they would transform into anonymous variables, which would remain in memory by every call of this function, and in the end would be cleared by the garbage collector. Our suggestion is way easier. I’d like to say once again that the question of anonymous variables within functions is disputable and depends on the realization of JavaScript engine.

In the end we get scroll view - with smooth scroll of the content, but without inertia. Here we need to discuss animation more closely.

animator

animator is responsible for animation. There is little reason to discuss requestAnimationFrame, which has been discussed in many articles. The one thing that requires attention is building animation cycle and control over elapsed time:

    function animate(...) {
        var firstIteration = true, startTimestamp, ...;

        function animationStep(currentTimestamp) {
            if (self.isAnimating) {window.requestAnimationFrame(animationStep, null);}

            // if it is first iteration, save timestamp and return
            if (firstIteration) {
                startTimestamp = currentTimestamp;
                firstIteration = false;
                return;
            }

        // do animation
        ...

        }

        window.requestAnimationFrame(animationStep, null);
    }

The reason for building animation cycle this way is: at the beginning of the calculation of animation parameters we post new iteration, and only then starts recalculation of animation. If done inversely (calculations first, followed by setting animation call for execution), it brings to a time gap between frames – the time that was spent on animation calculations. For more precise calculation of shift coordinates you may use time delta, calculated not by values of window.perfomance.now() or Date.now(), but by the input parameter of callback function, which is transmitted to requestAnimationFrame. This value is incomparably more precise than by the mentioned methods.

Here we approached the most interesting issue from our point of view – placement of items in the list.

list view

list view – like we described above, the basis of placement of items in the list lies in the following approach. At the moment when the user scrolls down the list by when a new item is about to be shown, the item that has become invisible to user is moved to a new place, and its content is changed by adapter.

The original approach we tried to apply was: during each frame the coordinates of each list item were calculated, bearing in mind that the height of each item is equal. The problem of this approach is not that we need to perform excessive calculations – they can be optimized, and the frame budget will be enough for that. The problem is that rendering of these items will not be visually perfect: if we make a 1-pixel line between items, there will be a tremor seen during scrolling.

As the solution was used a container which shifted during scrolling. List items were positioned relating to this container, according to their index in the list. The size of this container does not have to be recalculated. Thus we avoided excessive calculations for all items on every frame. An item is positioned just once after its content is changed – this has removed slight tremor at the edges during scrolling, and allowed to apply scroll view as the basis for infinite scroll.

Unfortunately, having applied all the described approaches and optimizations, we observed the fact that our infinite scroll isn’t perfect. No matter how we tried, there was a problem on weak mobile devices – with relatively complex markup of a list item there was a limit of fling speed, beyond which the list started twitching and lagging.

The problem was, that after reaching a certain scrolling speed, the time for shifting reused items and changing their content started to exceed the frame budget.

We had to think of something to help avoid this dead end.

Here to our rescue came the psychology of image perception. Human eye and brain very differently react to changes in brightness, colour and content. Our brain can tell a change in content not more frequently than once a 1/18 sec, while a movement (a change in brightness) can be perceived 50-60 times a second by an average person, and even more.

We decided to divide scroll functionality into separate flows. During rearrangement of list items the content wasn’t placed at once, but rather in 80 ms. It’s the duration of 2 frames by 25 frames per second. setTimeout identifier was kept, and over the given 80 ms the change in item’s content was required, i.e. the list was scrolled down by one screen or more (quite possible theoretically, depending on easing function, screen size of a mobile device etc.). The task was dismissed with clearTimeout. This solved the problem of scrolling speed on weak mobile devices, but added a problem on desktops.

On desktops, it was made possible to accelerate scrolling to such a speed, that at some moments in long lists, the eye discerned that hundreds and thousands in the output number of list item doesn’t change by fast scrolling. And by slowing down the list, there was a value bounce. One problem was solved, another one was created.

We had to reject setTimeout by setting the content.

Afterwards we thought of using setTimeout or setInterval not for setting content, but for placing items in the list. Therefore the container with items would scroll, but the items would change their position as related to the container not at the moment when another item was about to appear, but rather at the moment of check, which happened about 18-20 times per second. But why introducing a new flow, if it’s rather easy to check how much time has passed after the last check of placement of items in the list?

This variant made the logic of placement of list items more complex. We had to introduce 2 arrays of visible and invisible list items, between which items are exchanged. Depending on the scroll, the number of visible items on the screen changes, and the scroll can just drift away during the animation before the moment when the placement of list items is recalculated; thus we’ll have blank space which will not be discerned by the user’s brain.

There were fewer twitches, yet they still remained.

We outlined 3 key points in optimization of infinite scroll:

  • animation of container scroll;
  • rearrangement of list items in the container;
  • change of content in list item.

Since the last task requires most resources, we anyway had to assign tasks for setting content in an item. However, it was done not with setTimeout: at the moment when a list item is moved, when its content is about to change, the inner object (the helper) acquires the task of changing the content in the item, and the time when the task was formed is pointed out. Periodically, during the scroll animation, a check occurs; the content of the item is changed, in case the change of content, since the task was set, took more time than it takes an average person to perceive the content (18-25 fps).

After the optimization of these 3 key points we received quite a satisfactory result: rather smooth infinite scroll, available on weak mobile devices as well.

In conclusion we’d like to say a few words about setting content in list items:

1. Use textContent instead of innerHTML, because it doesn’t require parsing and recalculating new content of an item.

2. If the pictures in the list are recurring and known beforehand, it’s desirable to use CSS class for the item with the needed image as a background, because this will allow the browser to cache the image. If unique images are required for each list item, it’s desirable (by setting a new src for the tag img) to make opacity = 0, and after the loading opacity = 1.

3. Use the mechanism of handlers – JavaScript obejcts containing pointers to inner parts of list items, which change depending on the index. In our case 3 parameters are transmitted to the adapter: index of the item, the item itself for reuse (or null in the very first instance) and handler, which contains height/width of the item (in case they will be required for calculations), and where any information for further use can be saved.

In the future we plan to add tweaking to the top of the item and support of several types (heights and content) of list items. We already had this kind of implementation, but we had to reject it due to changes in algorithm of placement of list items. Then we had to implement other frequently used UI widgets (band – list of items with absolutely different heights, side menu with swipe etc.) as parts of an independent set of modules, optimized for work on mobile devices.

You can get acquainted with the first tools of our kit on GitHub. They are ready for use, and we have already started work with them on your projects.

No Comments Yet.

Leave a comment