I'm updating a table header and its first column positions programatically based on how the user scrolls around to keep them aligned.
The issue I'm experiencing is that as soon as my data sets gets big enough, the scrolling gets more and more choppy/less smooth.
The relevant code is at the very bottom of the fiddle:
iScroll.on('scroll', function(){
var pos = $('#scroller').position();
$('#pos').text('pos.left=' + pos.left + ' pos.top=' + pos.top);
// code to hold first row and first column
$('#scroller th:nth-child(1)').css({top: (-pos.top), left: (-pos.left), position:'relative'});
$('#scroller th:nth-child(n+1)').css({top: (-pos.top), position:'relative'});
// this seems to be the most expensive operation:
$('#scroller td:nth-child(1)').css({left: (-pos.left), position:'relative'});
});
I know that this can be written a lot more efficent by caching the elements and so on. For example, I have tried saving the elements in to an array and updating their position in a more "vanilla" fashion:
headerElements[i].style.left = left + 'px'; // etc...
No matter how fast I make the callback, I'm still not happy about the result. Do you have any suggestions?
You're experiencing choppy / non-smooth scrolling because the
scroll
event fires at a very high pace.And every time it fires you're adjusting the position of many elements: this is expensive and furthermore until the browser has completed the repaint it's unresponsive (here the choppy scrolling).
I see two options:
Option number one: display only the visible subset of the whole data set (this has been already suggested in another answer so I won't go futher)
Option number two (easier)
First, let animations on
left
andtop
css changes occurr via transitions. This is more efficient, is non-blocking and often let the browser take advantage of the gpuThen instead of repeteadly adjust
left
andtop
, do it once a while; for example 0.5 seconds. This is done by the functionScrollWorker()
(see code below) that recalls itself via asetTimeout()
.Finally use the callback invoked by the scroll event to keep the
#scroller
position (stored in a variable) updated.Here is the Fiddle
I set the Worker pace and the transition time to 0.5 secs. You may adjust the value with higher or lower timing, eventually in a dinamic way based on the number of elements in the table.
Is it necessary that you create your own scroller? Why don't you just style the data in HTML/CSS and just use the
overflow
attribute? JavaScript needs work on it's ability to adjust framerates. I was using your jFiddle earlier and it worked just fine with the native overflow handler.To be able to handle big amounts of data you need data virtualization. It has some restrictions, though.
First you need to decide the size of a view port. Let's say you want to render 10 items in a row and 20 items in column. It would be 10x20 items then. In you fiddle it's div with id wrapper.
Then you need to know total amount of data you have. From your fiddle it would be 100x100 items. And, also you need to know height and width of a item (cell). Let's take 40x120 (in px).
So div#wrapper is a view port, it should have fixed sized like 10x20 items. Then you need to set up correct width and height for table. The height of table would be equal to total amount of data in column including head by item height. Width for table would be total amount of items in single row by item width.
Once you set up these, div#wrapper will receive horizontal and vertical scrolls. Now you able to scroll left and bottom, but it will be just empty space. However this empty space is able to hold exact amount of data you have.
Then you need to take scroll data left and top (position), which comes in pixels and normalize it to amount of items, so you could know not how many pixels you've scrolled, but how many items you've scrolled(or rows if we scroll from top to bottom).
It could be done by division of pixels scrolled on item height. For example, you scrolled to left by 80px, that's 2 items. It means these items should be invisible because you've scrolled past them. So you know that you scrolled past 2 items, and you know that you should see 10 items in a row. That means you take your data array which has data for row with 100 items, and slice it like this:
It will give you items which should be visible in viewport at current scroll position. Once you have these items you need to construct html and append it to table.
Also on each scroll event you need to set top and left position for tbody and thead so they would move with scroll, otherwise you will have your data, but it will be at (0; 0) inside a viewport.
Anyway, code speaks thousand of words, so here's the fiddle: https://jsfiddle.net/Ldfjrg81/9/
Note, that this approach requires heights and widths to be precise, otherwise it will work incorrectly. Also if you have items of different sizes, this also should be taken into consideration, so better if you have fixed and equal sizes of items. In jsfiddle, I commented out the code which forces first column to stay in place, but you can render it separately.
It's a good solution to stick to some library as suggested in comments, since it handles a lot of cases for you.
You can make rendering even faster if use react.js or vue.js
Found this in the manual. Probably not what you wanna hear but it's the way it is:
IScroll is a class that needs to be initiated for each scrolling area. There's no limit to the number of iScrolls you can have in each page if not that imposed by the device CPU/Memory. Try to keep the DOM as simple as possible. iScroll uses the hardware compositing layer but there's a limit to the elements the hardware can handle.
Yes! Here are some improvements to the code from your JS Fiddle. You can view my edits at: https://jsfiddle.net/briankueck/u63maywa/
Some suggested improvements are:
position:relative
values in the JS layer toposition:fixed
in the CSS layer..find()
off of that element, which creates shorter trees & jQuery can traverse those branches faster.let
instead ofvar
will make your code look more modern.<th>
tag, which you'll want to look at.position:fixed
, the top<th>
tags only need to have thetop
property applied to them. The left<td>
tags only need to have theleft
property applied to them. The corner<th>
needs to have both thetop
&left
property applied to it..css()
methods for the actual.style.left= -pos.left + 'px';
and.style.top= -pos.top + 'px';
properties in the JS code.Try using a diff tool like WinMerge or Beyond Compare to compare the code from your version to what's in my edits, so that you can easily see the differences.
Hopefully, this will make the scrolling smoother, as the scroll event doesn't have to process anything that it doesn't need to do... like 5 full DOM traversing look-ups, rather than 3 short-tree searches.
Enjoy! :)
HTML:
CSS:
JS:
The reason the performance degradation is happening is that your scroll event handler is firing again and again and again instead of waiting for a reasonable and imperceptible interval.
The screenshot shows what happened when I tracked how many times the event handler fired, while scrolling for just a few seconds. The computationally-heavy event handler was fired over 600 times!!! This is more than 60 times per second!!!
It may seem counter-intuitive, but reducing the frequency that the table is updated will vastly increase perceived response times. If your user scrolls for fraction of a second, about 150 milliseconds, and the table is updated ten times, freezing the display during the scrolling, the net result is far worse than if the table were updated only three times and moved fluidly rather than freezing. It is just wasted processor burn to update more times than the browser can handle without freezing.
So, how do you make an event handler that fires at a maximum frequency, for example 25 times per second, even it is triggered much more often, like 100 times per second?
The naive way of doing it is to run a setInterval event. That is better, but horribly inefficient as well. There is a better way of doing it, by setting a delayed event handler, and clearing it on subsequent invocations before setting it again, until the minimum time interval has passed. This way it only runs no more often than at the maximum desired frequency. This is one major case for why the ``clearInterval'' method was invented.
Here is live working code:
https://jsfiddle.net/pgjvf7pb/7/
Note: when refreshing continuously like this, the header column may appear out of position.
I advise to do the update only when the scrolling has paused for about 25ms or so, rather than continuously. This way, it appears to the user that the header column is dynamically calculated as well as being fixed in place, because it appears instantly after scrolling rather than seeming to scroll with the data.
https://jsfiddle.net/5vcqv7nq/2/
The logic is like this:
variables outside your event handler
operations inside your event handler
Demo: https://jsfiddle.net/pgjvf7pb/7/
Once again, I recommend that you remove the line of code that refreshes the data instantly, and the else condition after that, and simply use one line
This will appear to instantly calculate the header column the moment any scrolling is finished, and saves even more operations. My second link to JS Fiddle shows what this looks like, here: https://jsfiddle.net/5vcqv7nq/2/