Creating a Web Worker with Google Apps Script

Google Sheets
Add-ins
A guide on implementing background processing in Google Apps Script using the inline web worker pattern to keep your extension’s UI responsive.
Published

April 18, 2024

Learn how to create a web worker with Google Apps Script to enable background processing while keeping the UI responsive. This guide covers an inline worker pattern that keeps the worker source in the Apps Script project, injects it into the sidebar page, and starts a module worker from a Blob URL.

Background

In Google Sheets extensions, Google Apps Script is used to serve the HTML page for the sidebar in the UI. To do machine learning (or any other intensive processing) client-side, ideally you want to use a web worker so the UI can remain responsive (e.g. to report on progress, cancel the operation, etc.) and also to enable multi-threaded processing.

Problem

Files in an Apps Script project are not exposed by HtmlService as standalone JavaScript URLs that a sidebar can pass directly to the Worker constructor. Apps Script’s Content Service can serve JavaScript from a separately deployed web app, but for worker source kept with the sidebar project an inline Blob is a simpler option.

Solution

We’ll use the inline worker pattern. The first step is to put the worker code in an html file in the Google Apps Script project, e.g. worker.html, with no html tags as shown below.

self.onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
  console.log('Posting message back to main script');
  self.postMessage(workerResult);
}

Next, we inline the worker code in the main HTML file using a Google Apps scriptlet to assign the text contents of the worker.html to a variable. Note the <?!= syntax with the exclamation mark to prevent escaping of the script. After that we create a blob and object URL, and then a new Worker using the object URL. In our case we use the type: 'module' option on the Worker constructor, which creates a module worker, which we need for the ESM-only library we are using in the worker.

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <script type="module">
    var workerScript = `<?!= HtmlService.createTemplateFromFile('worker').evaluate().getContent() ?>`
    var blob = new Blob([workerScript], {type: 'application/javascript'});
    var workerUrl = URL.createObjectURL(blob);
    var worker = new Worker(workerUrl, {type: 'module'});
    worker.onmessage = function(e) {
      console.log(e.data);
    };
    worker.postMessage([6, 7]);
    </script>
  </body>
</html>

One problem with this approach is that worker.html, without script tags, is not treated by a code editor as JavaScript, so no syntax highlighting, etc. You could add script tags in the worker.html file to fix this and then strip them back out again from the string in workerScript before passing it into the blob. In our case we’re developing the worker code in a plain web app first before copying it into the Google Apps Script project, so this isn’t an issue.