26

I've done an HTML form which has a lot of questions (coming from a database) in many different tabs. User then gives answers in those questions. Each time a user changes a tab my Javascript creates a save. The problem is that I have to loop through all questions each time the tab is changed and it freezes the form for about 5 seconds every time.

I've been searching for an answer how I can run my save function in the background. Apparently there is no real way to run something in the background and many recommend using setTimeout(); For example this one How to get a group of js function running in background

But none of these examples does explain or take into consideration that even if I use something like setTimeout(saveFunction, 2000); it doesn't solve my problem. It only postpones it by 2 seconds in this case.

Is there a way to solve this problem?

Community
  • 1
  • 1
kivikall
  • 786
  • 2
  • 13
  • 29

7 Answers7

18

You can use web workers. Some of the older answers here say that they're not widely supported (which I guess they weren't when those answers were written), but today they're supported by all major browsers.

To run a web worker, you need to create an instance of the built-in Worker class. The constructor takes one argument which is the URI of the javascript file containing the code you want to run in the background. For example:

let worker = new Worker("/path/to/script.js");

Web workers are subject to the same origin policy so if you pass a path like this the target script must be on the same domain as the page calling it.

If you don't want to create an new Javascript file just for this, you can also use a data URI:

let worker = new Worker(
    `data:text/javascript,
    //Enter Javascript code here
    `
);

Because of the same origin policy, you can't send an AJAX request from a data URI, so if you need to send an AJAX request in the web worker, you must use a separate Javascript file.

The code that you specify (either in a separate file or in a data URI) will be run as soon as you call the Worker constructor.

Unfortunately, web workers don't have access to neither outside Javascript variables, functions or classes, nor the DOM, but you can get around this by using the postMessage method and the onmessage event. In the outside code, these are members of the worker object (worker in the example above), and inside the worker, these are members of the global context (so they can be called either by using this or just like that with nothing in front).

postMessage and onmessage work both ways, so when worker.postMessage is called in the outside code, onmessage is fired in the worker, and when postMessage is called in the worker, worker.onmessage is fired in the outside code.

postMessage takes one argument, which is the variable you want to pass (but you can pass several variables by passing an array). Unfortunately, functions and DOM elements can't be passed, and when you try to pass an object, only its attributes will be passed, not its methods.

onmessage takes one argument, which is a MessageEvent object. The MessageEvent object has a data attribute, which contains the data sent using the first argument of postMessage.

Here is an example using web workers. In this example, we have a function, functionThatTakesLongTime, which takes one argument and returns a value depending on that argument, and we want to use web workers in order to find functionThatTakesLongTime(foo) without freezing the UI, where foo is some variable in the outside code.

let worker = new Worker(
    `data:text/javascript,
    function functionThatTakesLongTime(someArgument){
        //There are obviously faster ways to do this, I made this function slow on purpose just for the example.
        for(let i = 0; i < 1000000000; i++){
            someArgument++;
        }
        return someArgument;
    }
    onmessage = function(event){    //This will be called when worker.postMessage is called in the outside code.
        let foo = event.data;    //Get the argument that was passed from the outside code, in this case foo.
        let result = functionThatTakesLongTime(foo);    //Find the result. This will take long time but it doesn't matter since it's called in the worker.
        postMessage(result);    //Send the result to the outside code.
    };
    `
);

worker.onmessage = function(event){    //Get the result from the worker. This code will be called when postMessage is called in the worker.
    alert("The result is " + event.data);
}

worker.postMessage(foo);    //Send foo to the worker (here foo is just some variable that was defined somewhere previously).
Donald Duck
  • 8,409
  • 22
  • 75
  • 99
12

Apparently there is no real way to run something on background...

There is on most modern browsers (but not IE9 and earlier): Web Workers.

But I think you're trying to solve the problem at the wrong level: 1. It should be possible to loop through all of your controls in a lot less than five seconds, and 2. It shouldn't be necessary to loop through all controls when only one of them has changed.

I suggest looking to those problems before trying to offload that processing to the background.

For instance, you could have an object that contains the current value of each item, and then have the UI for each item update that object when the value changes. Then you'd have all the values in that object, without having to loop through all the controls again.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Totally agree. Even if you were able to run this in the background, you then have an issue where the user *thinks* the application is responsive but actually it's still in the middle of calculating things. The more efficient solution is to work out why your code takes 5 seconds – Gareth Sep 19 '13 at 12:49
  • I knew someone would point it out that my code might be little heavy, which propably is true I admit. But let's put that aside. I could have all the questions on one page and in worst case they all should be looped through then. When it comes to Web Workers (which I'm going to read about later today, thank you for pointing that out) I can't use them. We're using IE8 here and this solution must be "bullet proof". If I can't find out how to solve this smoothly I might just add a loading gif to the screen. – kivikall Sep 19 '13 at 12:50
  • @kivikall: *"But let's put that aside."* I wouldn't, if I were you. Five seconds is an *insanely* long period of time for a modern computer, even one running IE8. The right solution here is to identify the reasons for it taking that long, and deal with those reasons -- through caching previous answers to an object (as described), etc. – T.J. Crowder Sep 19 '13 at 12:59
  • 1
    Chrome has some great debugging tools to help you [see where most CPU time is being spent](https://developers.google.com/chrome-developer-tools/docs/cpu-profiling). Ignoring the performance issues *is* an option, but a very weak one. Javascript is [pretty powerful](http://labs.gooengine.com/videosphere/) so describing some form manipulation as a "little heavy" when it's taking 5 *seconds* to complete sounds honestly like an understatement – Gareth Sep 19 '13 at 13:01
  • @T.J.Crowder You're absolutely right and I'm gonna re-think it. But even if it would take only 0,1 sec using a setTimeout() would only postpone the execution right? And therefore I'm a bit confused because if it's fast enough to not catch user's attention why would I'd like to use async at all. And if it would be too slow I don't really care if it occurs before or after the tab is changed. So my original question is still on. – kivikall Sep 19 '13 at 13:08
  • @kivikall: My point is that if you're just gathering information, it should be so fast you don't need background processing. `setTimeout` solutions just work by making the page more responsive by breaking the work up into small chunks and allowing the browser a chance to do things between processing the chunks. Typically these will make the total time required to do the processing longer (sometimes a lot longer), the *only* advantage is that the browser isn't locked up between chunks. – T.J. Crowder Sep 19 '13 at 13:13
  • @T.J.Crowder Sure, that makes sense. What would be the right way to do "background processing" if not using Web Workers? I know it's possible to load some files in background by using jQuery's Ajax() function for example? What would be similar for running functions? I'm not talking about my form anymore, but more in general if I ever have to do such a thing. Or is it so that I pray on my knees that our browsers will be updated. – kivikall Sep 19 '13 at 13:23
  • 2
    Aside from web workers, Javascript in browsers is single threaded. Any time you have long-running code, you're blocking interaction with the page. – Gareth Sep 19 '13 at 13:26
  • 1
    @kivikall: Unfortunately, without web workers, you can't. [Which is why we have web workers. :-) ] The reason ajax runs in the background is specific to the underlying mechanism (the `XMLHttpRequest` object), it's not something jQuery does, and sadly it can't be applied to code running in the browser. Your JavaScript functions will always run on the UI thread, unless you use web workers, which sadly you can't because they're not in IE8. :-( – T.J. Crowder Sep 19 '13 at 13:27
2

You could take a look at HTML5 web workers, they're not all that widely supported though.

Praveen
  • 55,303
  • 33
  • 133
  • 164
Fenixp
  • 645
  • 5
  • 22
1

This works in background:

setInterval(function(){ d=new Date();console.log(d.getTime()); }, 500);
vimuth
  • 5,064
  • 33
  • 79
  • 116
pasquale
  • 101
  • 1
  • 2
    It doesn't really, though. The only thing that's doing "in the background" is _sleeping_; the function is actually _run_ in the "foreground", and if it were doing something more heavyweight than simply `d=new Date();console.log(d.getTime());` (for example, if it were doing a huge tree search or factoring prime numbers or something) then you'd see your UI freezing up while it ran. – Quuxplusone Aug 28 '22 at 22:30
0

If you can't use web workers because you need to access the DOM, you can also use async functions. The idea is to create an async refreshUI function that refreshes the UI, and then call that function regularly in your function that takes long time.

The refreshUI function would look like this:

async function refreshUI(){
    await new Promise(r => setTimeout(r, 0));
}

In general, if you put await new Promise(r => setTimeout(r, ms)); in an async function, it will run all the code before that line, then wait for ms milliseconds without freezing the UI, then continues running the code after that line. See this answer for more information.

The refreshUI function above does the same thing except that it waits zero milliseconds without freezing the UI before continuing, which in practice means that it refreshes the UI and then continues.

If you use this function to refresh the UI often enough, the user won't notice the UI freezing.

Refreshing the UI takes time though (not enough time for you to notice if you just do it once, but enough time for you to notice if you do it at every iteration of a long for loop). So if you want the function to run as fast as possible while still not freezing the UI, you need to make sure not to refresh the UI too often. So you need to find a balance between refreshing the UI often enough for the UI not to freeze, but not so often that it makes your code significantly slower. In my use case I found that refreshing the UI every 20 milliseconds is a good balance.

You can rewrite the refreshUI function from above using performance.now() so that it only refreshes the UI once every 20 milliseconds (you can adjust that number in your own code if you want) no matter how often you call it:

let startTime = performance.now();
async function refreshUI(){
    if(performance.now() > startTime + 20){    //You can change the 20 to how often you want to refresh the UI in milliseconds
        startTime = performance.now();
        await new Promise(r => setTimeout(r, 0));
    }
}

If you do this, you don't need to worry about calling refreshUI to often (but you still need to make sure to call it often enough).

Since refreshUI is an async function, you need to call it using await refreshUI() and the function calling it must also be an async function.

Here is an example that does the same thing as the example at the end of my other answer, but using this method instead:

let startTime = performance.now();
async function refreshUI(){
    if(performance.now() > startTime + 20){    //You can change the 20 to how often you want to refresh the UI in milliseconds
        startTime = performance.now();
        await new Promise(r => setTimeout(r, 0));
    }
}

async function functionThatTakesLongTime(someArgument){
    //There are obviously faster ways to do this, I made this function slow on purpose just for the example.
    for(let i = 0; i < 1000000000; i++){
        someArgument++;
        await refreshUI();    //Refresh the UI if needed
    }
    return someArgument;
}

alert("The result is " + await functionThatTakesLongTime(3));
Donald Duck
  • 8,409
  • 22
  • 75
  • 99
-1

This library helped me out a lot for a very similar problem that you describe: https://github.com/kmalakoff/background

It basically a sequential background queue based on the WorkerQueue library.

alejo802
  • 2,107
  • 1
  • 12
  • 9
  • Thanks. I'll take a look at it but I'm afraid I can't use it either. Not because it's unusable but in this current project our policy is to use only very well known extensions (which basically means only jQuery) or native components. Can't help it. But I'll take a look at that anyway and let you know if that could solve my problem. – kivikall Sep 19 '13 at 12:55
-6

Just create a hidden button. pass the function to its onclick event. Whenever you want to call that function (in background), call the button's click event.

<html>
<body>
    <button id="bgfoo" style="display:none;"></button>

    <script>
    function bgfoo()
    {
        var params = JSON.parse(event.target.innerHTML);
    }

    var params = {"params":"in JSON format"};
    $("#bgfoo").html(JSON.stringify(params));
    $("#bgfoo").click(bgfoo);
    $("#bgfoo").click(bgfoo);
    $("#bgfoo").click(bgfoo);
    </script>
</body>
</html>
ramu
  • 1
  • 1