I love it when my apps run at 60 fps, even on mobile devices. And I also like to save the state of my application, for example, open windows or entered text in localstorage and user metadata (if it is registered), so that by closing it, you can continue working with it later from the same place, including other device.
This is all great, only today I ran into one problem. The fact is that I have one side menu, offcanvas, and I would also like to save its state (open / closed) in the browser and user account. That's just an entry in the localstorage and AJAX request for updating to the database is asynchronous and they always strive to run right during a complex animation, stealing from me a couple of frames, which is especially noticeable on mobile devices. Obviously, I would like the data to be saved after the animation ends, and not at the critical moment of my application, but how?
Adding to the up stack by
setTimeout (doPostponedStuff) does not help, since the animation itself is asynchronous, and the call to
doPostponedStuff is mixed with the animation frame calls. Can the duration of the animation be cached as the second parameter of
setTimeout , that is,
setTimeout (doPostponedStuff, 680) ? No, thank you, I do not want to burn in a programmer hell. Wait, this reminds me of something. Two resource-intensive tasks, of which one is minor, and it should not interfere with the paramount ... Oh, yes! Very similar to the calculation of the coordinates of an element with
position: absolute or
position: fixed when resizing the browser window or scrolling. If we approach this task trivially, we will have something like:
$(window).resize(function(){ $target.css('top', x).css('left', y).css('width', z) });
But with this method, we will be disappointed: when scrolling, the browser will start dropping frames very quickly, because onScroll events are thrown almost at every scrolled pixel, well, maybe not at every, but very often, a couple of dozen times per second, that's for sure , and the operations themselves with DOM and redrawing are quite expensive. Fortunately, the solution to this problem has long been invented.
')
Debauser
So, as mentioned above, the solution was invented long ago, the name of his debouncer (English
debouce ), and it consists in calling the Coleck not at every event, but when the action generating the event is completed, that is, in the case of scrolling, the handler is called on completion of scrolling, but not on every scrolled pixel. If it is even more specific, the debouncer causes a callback if a specified amount of time has passed since the last event. The Debunser code itself is very simple, and easily fits into several lines:
function debounce(ms, cb){ var timeout = null; return function(){ if(timeout) clearTimeout(timeout); timeout = setTimeout(cb, ms); } }
and you can use it as follows:
$(window).scroll(debounce(100, repositionElement);
Now the
repositionElement will be called up within a hundred milliseconds since the end of the scroll, which in the most positive way will affect fps.
Debouncing critical application code
This is all fine, but how will it help us identify the application downtime and use this time to perform background background tasks? To do this, we first wrap our code for such tasks into a function, and pass it through a debouncer, for example, like this:
var saveAppStateWhenIdle = debounce(1000, saveAppState);
and in critical places of our application we will call
saveAppStateWhenIdle ();
thus, we will
postpone the execution of
saveAppState until the application is “fixed”, and critical resources become free, and this will work even in asynchronous code. In my particular case, I call saveAppStateWhenIdle on every frame of the offcanvas animation, as well as in other places where the state of the application needs to be saved, and the account goes into frames.
Debouncing recurring code
As another example, we can assume that we have a very ordinary (micro) blog, in which new posts are loaded with AJAX, and also beautifully animated, but if the update interval coincides with the scrolling time, the application crawls a little . Trifle, but unpleasant. To solve this problem, you can write the following solution:
var loadPostsWhenIdle = debounce(1000, loadMorePosts); setInterval(loadPostsWhenIdle, 10000); $(window).scroll(loadPostsWhenIdle);
Thus, if the application is idle, new posts will be loaded approximately every 11 seconds, if the user just at that time scrolls the page, then the upload will be postponed until the user finishes scrolling.
Unfortunately, this method has one drawback, namely,
loadPostsWhenIdle will be called after each completion of scrolling, even if less than 11 seconds passed between them, that is, the principle “no more than once every 11 seconds” is not respected. To solve this problem, you can use a boolean switch, in which “true” means that the application is busy, and “false”, that it is idle, and you can perform a secondary task:
var appIsBusy = false; var onAppIsIdle = debounce(1000, function(){ appIsBusy = false; }); var doingPerformanceHeavyStuff = function(){ appIsBusy = true; onAppIsIdle(); } setInterval(function (){ if(!appIsBusy) loadMorePosts(); }, 10000); $window.scroll(doingPerformanceHeavyStuff);
Now new posts will be loaded no more than once every 10 seconds, but not during scrolling.
In principle,
doingPerformanceHeavyStuff can be inserted into any critical part of the application, as well as using the
appIsBusy switch in any background or periodic task to find out if this is the right time for that task.