JS Worker Performance - Parsing JSON

Viewed 39

I'm experimenting with Workers as my user interface is very slow due to big tasks running in the background.

I'm starting at the simplest tasks such as parsing JSON. See below for my very simple code to create an async function running on a Worke.

Performance wise there is a big difference between:

JSON.parse(jsonStr);

and

await parseJsonAsync(jsonStr);

JSON.parse() takes 1ms whereas parseJsonAsync takes 102ms!

So my question is: are the overheads really that big for running worker threads or am I missing something ?

const worker = new Worker(new URL('../workers/parseJson.js', import.meta.url));

export async function parseJsonAsync(jsonStr) {

    return new Promise(
        (resolve, reject) => {

            worker.onmessage = ({
                data: {
                    jsonObject
                }
            }) => {
                resolve(jsonObject);
            };

            worker.postMessage({
                jsonStr: jsonStr,
            });
        }
    );
}

parseJson.js

self.onmessage = ({
    data: {
        jsonStr
    }
}) => {

    let jsonObject = null;

    try {
        jsonObject = JSON.parse(jsonStr);
    } catch (ex) {

    } finally {
        self.postMessage({
            jsonObject: jsonObject
        });
    }
};
1 Answers

I can now confirm that the overhead of transferring message between threads is pretty big. But the raw performance of worker (at least in executing JSON.parse) is close to main thread.

TL;DR: Just compare numbers in 2 tables. Without sending big object via postMessage, worker perf is just fine.


For test payload jsonStr, I create a string of a long list of [{"foo":"bar"}, ...] repeat n times. The number of items in jsonStr can be tuned by changing Array.from({ length: number }).

I then do postMessage(jsonStr) to run JSON.parse in worker, when done parsing it sends back the parsed jsonObject. In main thread just I call JSON.parse(jsonStr) directly.

runTest(delay) use setTimeout to wait until the worker startup to run the actual test. runTest() without delay runs immediately so we can measure worker startup time.

Code for the test.

const blobURL = URL.createObjectURL(
  new Blob(
    [
      "(",
      function () {
        self.onmessage = ({ data: jsonStr }) => {
          let jsonObject = null;
          try {
            jsonObject = JSON.parse(jsonStr);
            self.postMessage(["done", jsonObject]);
          } catch (e) {
            self.postMessage(["error", e]);
          }
        };
      }.toString(),
      ")()",
    ],
    { type: "application/javascript" }
  )
);

const worker = new Worker(blobURL);
const jsonStr = "[" + Array.from({ length: 1_000 }, () => `{"foo":"bar"}`).join(",") + "]";

function test(payload) {
  worker.onmessage = ({ data }) => {
    const delta = performance.now() - t0;
    console.log("worker", delta);

    console.log("worker response", data[0]);
  };

  const t0 = performance.now();
  worker.postMessage(payload);

  testParseJsonInMain(payload);
}

function testParseJsonInMain(payload) {
  let obj;
  try {
    const t0 = performance.now();
    obj = JSON.parse(payload);
    const delta = performance.now() - t0;
    console.log("main", delta);
  } catch {}
}

function runTest(delay) {
  if (delay) {
    setTimeout(() => test(jsonStr), delay);
  } else {
    test(jsonStr);
  }
}

runTest(1000);

I observe that it takes around 30ms to start the worker on my machine. If test run after worker startup, I got these numbers (unit in milliseconds):

#items in payload main worker
1,000 0.2 2.1
10,000 1.3 9.8
100,000 15.4 73.5
1,000,000 165 854
10,000,000 2633 15312

When payload reaches 10 million items, the worker really struggles (takes 15 seconds). At 10 million items, the jsonStr is around 140MB.

But if the worker does not send back the parsed jsonObject, the numbers are so much better. Just make a little change to above test code:

// worker code changed from:
self.postMessage(["done", jsonObject]);

// to:
self.postMessage(["done", typeof jsonObject]);
#items in payload main worker
1,000 0.2 1.2
10,000 2.1 3.5
100,000 15.7 26.2
1,000,000 196 232
10,000,000 2249 2801

P.S. I've actually done another test. Instead of postMessage(jsonStr), I used TextEncoder to turn the string into ArrayBuffer, then postMessage(arrayBuffer, arrayBuffer) which supposedly transfers the underlying memory from main thread directly to worker.

I did not see real difference in terms of time consumed, in fact it gets a little bit slower. Guess sending large string isn't an issue.

Related