Using Workers in NativeScript

January 11th, 2018

I hate worrying about performance. It's the least fun part of development. It's hard enough writing code that works, are we really expected to make it work well on all devices?

In my app Daily Nanny, there's a feature that allows nannies and parents to manage the nanny's hours. In one of the views, it lists all their shifts in history, and I do some pretty complex data operations before I display the array:

I use moment.js to do some date related operations, like figuring out how long the shift was in minutes and hours based on a Date object for start time and end time, then do a bunch of math to calculate the amount earned based on their hourly rate, if any overtime pay should be applied, assemble shifts into weeks with a header including aggregate data, all based on an array of simple shift objects I get back from my API.

Over time, nannies are building up their history of shifts, some having almost a year of data, so hundreds of shifts. Doing this math for every record takes up time. When you execute complex data operations like this in NativeScript, by default it's run on the same thread as the UI. This means that while the device is doing these complex JavaScript operations, your UI is "frozen".

In my tests, this could take up to 10 seconds in the most extreme cases! Totally unacceptable. Enter Workers. A worker executes on an isolated background thread, allowing the UI to render uninhibited by complex JavaScript operations.

There's plenty of documentation on workers, but I found none of it went beyond the most basic example, so I'm writing this to provide a little more context. Let's get into the code.

Creating a Worker

Create a folder in your app folder called workers (or whatever you want), and create a file for your operation. In my case, I am processing shifts, so I called it process-shifts.js. In that file, throw this code in there:

// process-shifts.js require('globals'); global.onmessage = function(workerMsg) { var request = workerMsg.data; console.dir(request); }

require('globals') is necessary to get this working on a new thread. Then global.onmessage is what receives a message from your main thread. The terminology here is that the threads are communicating with each other through "messages". I find it helpful to think of it like a Promise. You do something, and when that thing is done, your code continues executing in the .then method. In the context of Workers, postMessage is like calling resolve() in a Promise.

// process-shifts.js require('globals'); global.onmessage = function(workerMsg) { var request = workerMsg.data; var shifts = request.shifts; console.dir(request); var sectonedShifts = processData(); var responseMsg = { success: true, sectionedShifts: sectonedShifts } global.postMessage(responseMsg) } function processData(shifts) { // do all the complicated work here. return shifts; }

So that receives data from your main thread in the message. In the above code, workerMsg.data contains the data you send to the worker from the main thread. Then it sends a message back to the main thread with postMessage. So let's look at how to call this from the main thread:

// shifts-view-model.js function processAllShifts(shifts) { var worker = new Worker('~/workers/process-shifts'); worker.postMessage({ shifts: shifts }); worker.onmessage = function(msg) { if (msg.data) { var resp = msg.data.response; var sectionedShifts = resp.sectionedShifts; // here is where i update my UI with the response from the worker, like set my Observable Array to the sectionedShifts returned from the worker. worker.terminate(); } } worker.onerror = function(err) { console.log(`An unhandled error occurred in worker: ${err.filename}, line: ${err.lineno} :`); console.log(err.message); } }

I'm using the worker to do the heavy lifting, and it returns an array with all the data that I need, then I just set the Observable Array that's the source for my ListView to the response from the worker.

When the heavy lifting is done, kill the background process using worker.terminate()

This approach is crucial for apps that do some computationally complex operations. It's not always necessary, but don't be afraid to employ workers. In my case, when developing the app I didn't really experience any lag because I wasn't yet working with a large data set. But since my app has a bunch of users now generating a bunch of data, it's more important to think about what operations may slow down the UI as the app scales.

Reach out to me with any questions/comments, and thanks for reading!