Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NodeJS and Electron - request-promise in back-end freezes CSS animation in front-end

Note: Additional information appended to end of original question as Edit #1, detailing how request-promise in the back-end is causing the UI freeze. Keep in mind that a pure CSS animation is hanging temporarily, and you can probably just skip to the edit (or read all for completeness)

The setup

I'm working on a desktop webapp, using Electron.

At one point, the user is required to enter and submit some data. When they click "submit", I use JS to show this css loading animation (bottom-right loader), and send data asynchronously to the back-end...

- HTML -

<button id="submitBtn" type="submit" disabled="true">Go!</button>

<div class="submit-loader">
    <div class="loader _hide"></div>
</div>

- JS -

form.addEventListener('submit', function(e) {
    e.preventDefault();

    loader.classList.remove('_hide');

    setTimeout(function() {
        ipcRenderer.send('credentials:submit', credentials);
    }, 0)
});

where ._hide is simply

._hide {
    visibility: hidden;
}

and where ipcRenderer.send() is an async method, without option to set otherwise.

The problem

Normally, the 0ms delay is sufficient to allow the DOM to be changed before the blocking event takes place. But not here. Whether using the setTimeout() or not, there is still a delay.

So, add a tiny delay...

loader.classList.remove('_hide');

setTimeout(function() {
    ipcRenderer.send('credentials:submit', credentials);
}, 100);

Great! The loader displays immediately upon submitting! But... after 100ms, the animation stops dead in its tracks, for about 500ms or so, and then gets back to chooching.

This working -> not working -> working pattern happens regardless of the delay length. As soon as the ipcRenderer starts doing stuff, everything is halted.

So... Why!?

This is the first time I've seen this kind of behavior. I'm pretty well-versed in HTML/CSS/JS, but am admittedly new to NodeJS and Electron. Why is my pure CSS animation being halted by the ipcRenderer, and what can I do to remedy this?

Edit #1 - Additional Info

In the back-end (NodeJS), I am using request-promise to make a call to an external API. This happens when the back-end receives the ipcRenderer message.

var rp = require('request-promise');

ipcMain.on('credentials:submit', function(e, credentials) {    

    var options = {
        headers : {
            ... api-key...
        },
        json: true,
        url : url,
        method : 'GET'
    };

    return rp(options).then(function(data) {
        ... send response to callback...
    }).catch(function(err) {
        ... send error to callback...
    });

}

The buggy freezing behavior only happens on the first API call. Successive API calls (i.e. refreshing the desktop app without restarting the NodeJS backend), do not cause the hang-up. Even if I call a different API method, there are no issues.

For now, I've implemented the following hacky workaround:

First, initialize the first BrowserWindow with show:false...

window = new BrowserWindow({
    show: false
});

When the window is ready, send a ping to the external API, and only display the window after a successful response...

window.on('ready-to-show', function() {
    apiWrapper.ping(function(response) {
        if(response.error) {
            app.quit();
        }else {
            window.show(true);
        }
    });
});

This extra step means that there is about 500ms delay before the window appears, but then all successive API calls (whether .ping() or otherwise) no longer block the UI. We're getting to the verge of callback hell, but this isn't too bad.

So... this is a request-promise issue (which is asynchronous, as far as I can tell from the docs). Not sure why this behavior is only showing-up on the first call, so please feel free to let me know if you know! Otherwise, the little hacky bit will have to do for now.

(Note: I'm the only person who will ever use this desktop app, so I'm not too worried about displaying a "ping failed" message. For a commercial release, I would alert the user to a failed API call.)

like image 271
Birrel Avatar asked Jan 24 '18 19:01

Birrel


1 Answers

Worth to check how does request-promise internally setups up module loading. reading it, it seems like there is kind of lazy loading (https://github.com/request/request-promise/blob/master/lib/rp.js#L10-L12) when request is being called. Quick try out

const convertHrtime = require('convert-hrtime');

const a = require('request-promise');

const start = process.hrtime();
a({uri: 'https://requestb.in/17on4me1'});
const end = process.hrtime(start);
console.log(convertHrtime(end));

const start2 = process.hrtime();
a({uri: 'https://requestb.in/17on4me1'});
const end2 = process.hrtime(start2);
console.log(convertHrtime(end2));

returns value like below:

{ seconds: 0.00421092,
  milliseconds: 4.21092,
  nanoseconds: 4210920 }
{ seconds: 0.000511664,
  milliseconds: 0.511664,
  nanoseconds: 511664 }

first call is obviously taking longer than subsequent. (number of course may vary, I ran this on bare node.js on relatively fast cpu) If module loading is major cost for first call, then it'll block main process until module is loaded (cause node.js require resolve is synchronous)

I'm not able to say this is concrete reason, but worth to check. As suggested in comment, try other lib or bare internal module (like Electron's net) to rule out.

like image 78
OJ Kwon Avatar answered Nov 03 '22 07:11

OJ Kwon