Using web application downtime for background tasks
I love when my apps run at 60 fps, even on mobile devices. 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, work with it could be continued later from the same place, including another device.
This is all wonderful, 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 the user account. Here are just a record in localstorage and AJAX, the update request in the database is asynchronous and they always try to run right during complex animations, stealing a couple of frames from me, which is especially noticeable on mobile devices. Obviously, I would like the data to be saved after the animation is completed, and not at the critical moment of my application, but how?
Adding a stack to the top by setTimeout (doPostponedStuff) does not help, since the animation itself is asynchronous and calling doPostponedStuffmixed up with calls to animation frames. Can hardcode the animation duration as the second parameter to setTimeout , i.e. setTimeout (doPostponedStuff, 680) ? No, thank you, I do not want to burn in a programming hell. Wait a minute, but something reminds me of that. Two resource-intensive tasks, one of which is secondary, and it should not interfere with the primary ... Oh yes! It is very similar to calculating the coordinates of an element with position: absolute or position: fixed when resizing the browser window or scrolling. If approaching this task is trivial, we get something like:
But with this method, we will be disappointed: when scrolling, the browser will start dropping frames very quickly, since onScroll events are thrown out 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 with the DOM and redrawing are quite expensive. Fortunately, a solution to this problem has long been invented.
So, as mentioned above, the solution has been invented a long time ago, its name is debouncer (eng. Debouce ), and it consists in calling the callback not at every event, but at the end of the action that generates the events, that is, in the case of scrolling, the handler will be called upon completion of scrolling, and not with every scrolled pixel. More specifically, the debouncer calls the callback if a specified amount of time has passed since the last event. The debouncer code itself is very simple, and easily fits in a few lines:
and you can use it as follows:
Now repositionElement will be called after a hundred milliseconds from the end of the scroll, which in the most positive way will affect fps.
This is all fine, but how can it help us identify application downtime and use this time to perform secondary background tasks? And for this, we first wrap our code for such tasks in a function, and pass it through a debouncer, for example, like this:
and in critical places of our application we will call
Thus, we will postpone the execution of saveAppState until the application is "stopped" and critical resources become free, and this will work even in asynchronous code. In my particular case, I call saveAppStateWhenIdle with every offcanvas animation frame, as well as in other places where you need to save the state of the application, and the count goes to frames.
As another example, we can assume that we have the most ordinary (micro) blog, in which new posts are loaded by AJAX, and even nicely animated at the same time, only if the update interval coincides in time with scrolling, the application . A trifle, but unpleasant. To solve this problem, you can write the following solution:
Thus, if the application is idle, new posts will be loaded approximately every 11 seconds, if the user scrolls the page just at that time, then the download 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 elapse between them, that is, the principle "not 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:
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 time is suitable for that task.
This is all wonderful, 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 the user account. Here are just a record in localstorage and AJAX, the update request in the database is asynchronous and they always try to run right during complex animations, stealing a couple of frames from me, which is especially noticeable on mobile devices. Obviously, I would like the data to be saved after the animation is completed, and not at the critical moment of my application, but how?
Adding a stack to the top by setTimeout (doPostponedStuff) does not help, since the animation itself is asynchronous and calling doPostponedStuffmixed up with calls to animation frames. Can hardcode the animation duration as the second parameter to setTimeout , i.e. setTimeout (doPostponedStuff, 680) ? No, thank you, I do not want to burn in a programming hell. Wait a minute, but something reminds me of that. Two resource-intensive tasks, one of which is secondary, and it should not interfere with the primary ... Oh yes! It is very similar to calculating the coordinates of an element with position: absolute or position: fixed when resizing the browser window or scrolling. If approaching this task is trivial, we get 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, since onScroll events are thrown out 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 with the DOM and redrawing are quite expensive. Fortunately, a solution to this problem has long been invented.
Debouncer
So, as mentioned above, the solution has been invented a long time ago, its name is debouncer (eng. Debouce ), and it consists in calling the callback not at every event, but at the end of the action that generates the events, that is, in the case of scrolling, the handler will be called upon completion of scrolling, and not with every scrolled pixel. More specifically, the debouncer calls the callback if a specified amount of time has passed since the last event. The debouncer code itself is very simple, and easily fits in a few 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 repositionElement will be called after a hundred milliseconds from the end of the scroll, which in the most positive way will affect fps.
Debugging critical application code
This is all fine, but how can it help us identify application downtime and use this time to perform secondary background tasks? And for this, we first wrap our code for such tasks in 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 "stopped" and critical resources become free, and this will work even in asynchronous code. In my particular case, I call saveAppStateWhenIdle with every offcanvas animation frame, as well as in other places where you need to save the state of the application, and the count goes to frames.
Debounce Periodically Repeated Code
As another example, we can assume that we have the most ordinary (micro) blog, in which new posts are loaded by AJAX, and even nicely animated at the same time, only if the update interval coincides in time with scrolling, the application . A 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 scrolls the page just at that time, then the download 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 elapse between them, that is, the principle "not 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 time is suitable for that task.