Elm in an embeded Web Worker (Elm in a Blob)

hi there

I’ve been on a bit of a journey. I have a performance problem that I think would be in a better spot if I could do some work in a Web Worker (do some work off the main thread). Of course I could write the Web Worker in JavaScript but what would be the fun of that.

I found this excellent article on putting Elm in a web worker.

But for my purposes I would prefer to have an “inline” web worker. (At least I think I would). So I found this article plus some Stack overflow answers of various ways to do that.

The thing to do seems to be to put the Javascript for the Web Worker into a Blob.

I’ve hacked together a little script that compiles the Elm, puts it the Javascript glue code it’s needs. And then… well… I seem to have gotten my self into “escape hell”.

I need to get the Elm code into a Javascript string. I tried putting it in a back-tick string, but I had to escape the back-tick in the generated javascript code. Now that code is failing because… the generated javascript code contains some regex stuff with escape characters in it, and I tried a couple different things but once I start dealing with multiple levels of character escaping… well… I’m just throwing spaghetti at the wall.

Has anyone out there done anything like this? Is there maybe a tool out there that will put javascript (unmolested) into a Blob? Maybe there’s a tool that will turn the Elm javascript into some sort of encoded format where escape characters are no longer an issue?

I will keep trying things, and if I find an answer I will be sure to post it.

2 Likes

Hey after more :man_playing_handball::spaghetti::european_castle:. I came up with something that seems to be working.

I did the embedded script thing (here is a better article).

I’m putting this web worker inside of a web component.

So I have a :scream: template… template…

const getMuchSelectTemplate = (styleTag) => {
  const templateTag = document.createElement("template");
  templateTag.innerHTML = `
    <div>
      $${styleTag}
      <slot name="select-input"></slot>
      <div id="mount-node"></div>
      <script id="filter-worker" type="javascript/worker">
        $elm

        $js
      </script>
    </div>
  `;
  return templateTag;
};

export default getMuchSelectTemplate;

And my build script is in python and looks like this…

#!/usr/bin/env python3

import os
from string import Template

os.system("npx elm make src/FilterWorker.elm --output src/gen/filter-worker-elm.js")

filter_worker_elm_js_file = open("src/gen/filter-worker-elm.js",'r')
filter_worker_elm_js_string = filter_worker_elm_js_file.read()

table = str.maketrans({
    "`": r"",
})

filter_worker_elm_js_string = filter_worker_elm_js_string.translate(table)
filter_worker_elm_js_string = filter_worker_elm_js_string.encode('unicode-escape').decode()

filter_worker_js_file = open("src/filter-worker.js",'r')
filter_worker_js_string = filter_worker_js_file.read()

filter_worker_template_js_file = open("src/much-select-template-template.js",'r')
filter_worker_template_js_string = filter_worker_template_js_file.read()

t = Template(filter_worker_template_js_string)
bundle_string = t.substitute(elm=filter_worker_elm_js_string, js=filter_worker_js_string)

filter_worker_bundle_js_file = open("src/gen/much-select-template.js",'w')
filter_worker_bundle_js_file.write(bundle_string)

The 2 key things here, I’m just stripping out all the backticks from the generated javascript. :crossed_fingers:
Seems like a bad idea to just “do that” but… we’ll see.

And the second thing is the bit where we encode and then decode. Not really sure what’s going on there, but it seem to leave the regular expressions in the generated javascript alone.

There are other pieces of course… I’m hoping to write up something more detailed about the problems I have had to solve and the solutions I’ve found.

1 Like

Hey @jachin what is the overall goal you are trying to achieve? Do you need to have everything inside one single html file and your server cannot load any other resource afterwards? Because if not what’s the point of an inline worker for you?

Oh sure, I’m making a web component. It’s a multi select (everything and the kitchen sink sort of deal). You can see it at https://muchselect.dev (although the site is currently very… rough). One of my goals for the web component is that it would be self contained (an npm package that “just works”… at least… after you style it).

I’m running into performance issues when there are a lot of options. They get filtered down as the user types and I was/am getting a lot of “jank”. Based on some profiling I did it seemed like if I did the filtering in a web worker that might keep the UI more responsive (this is actually my first time writing a web worker though).

The implementation of the web worker I managed to get working does seem to have helped (but I could be barking up the wrong tree too).

I see, so your goal is to build a performant multi select web component. And you want to write it in Elm, with the search feature (performance bottleneck) done in a web worker.

So I see two challenges:

  • writing a web component in Elm
  • putting the search feature into a web worker

In addition, you are writing the logic for the search feature in Elm. So you have multiple steps to arrive at your final web compontent:

  1. Compile the search logic (Elm code, as a Platform.worker) into a JS string
  2. Add the JS side wiring the elm ports
  3. Add the JS code executed at startup of the web worker and the wiring of web worker messages into the elm ports
  4. Combine all JS parts above into a single JS string (I’d use simple string concatenation to avoid string template stuff)
  5. Load all that combined JS string into a <script> tag somewhere in your html file so that you can later extract that as a string when the component is loaded.

I might be wrong on some points so don’t hesitate to clarify my misunderstandings.

With a webworker you add a lot of overhead. Dual update functions + the overhead of sending data back and fourth. + encoding and decoding in both ends. (you can only pass strings to and from web workers)
I have searched through a million objects, searching on multiple attributes at once on debounced keystrokes. Without any issues, no worker involved.
I have a hard time believing elm is not fast enough for a multi select with less than 100.000 options.

Usually it is the rendering and dom manipulation that makes the ui not responsive. Not the filtering logic.
So maybe virtualisation of scrollbar is the solution if it has many elements when only a single letter is in the “search”
(Every element is rendered, even if it is not visible on screen)

Also HTML lazy can make a ton of difference, depending on the issue.

2 Likes

@mattpiz I think that outline is correct. Using string concatenation is a great idea.

@Atlewee Thanks for the advise. The path you mention is what I tried first, in fact, I did just find some silly extra work I was doing that was making things slow.

I picked this package early on and it works really well.

https://package.elm-lang.org/packages/tripokey/elm-fuzzy/latest/

But maybe it’s too slow. I’m doing a fuzzy search across 3 different fields in a whole bunch of records.

It’s possible I’m miss reading the profiling I’m doing but it seemed like doing the fuzzy search was the thing that was locking up the browser.

There is a lot of work it’s doing on the rendering too, but I’ve been able to get some performance improvements with Html.Lazy.

I also got some performance gains by doing the fuzzy search, the only rendering the top… 100-sh results or so (I’m still experimenting with the number).

I’ve done some experiments with only trying to render a “window” of the available options. I should probably do more with that, but at the time the complexity was starting to bog me down. But… just the way you’ve said that: “virtualization of the scrollbar” gives me more ideas though. :thinking:

I’ve also added a lot of debouncing, but maybe I need even more. I even have a dynamic debouncer that lengthens the time of the filtering debounce based on how many options there are.

I believe you when you say the web worker adds overhead. My hope was that the overhead happen on a non-blocking thread so the UI would stay responsive. Better not to have the overhead at all and it’s quite possible I’m just doing something(s) in an inefficient way.

I would gladly drop some of my deboucing and the web worker if I found performance gains in a simpler and more straight forward manner. I do need fuzzy searching though, if that’s just slow… then I’ll probably just have a best practice where we limit the number of options it tracks to some smaller number and swap in and out options from some other source.

Speaking of fuzzy searching, I thought I was in good shape in terms of performance, but I was only testing with dinosaur names as my options. Once I made each option a sentence of Lorem Ipsum, things got, super slow. That was one of the pieces of evidence I used to decide that I should try this path.

aha… Fuzzy is probably way more expensive calculation than substring for sure :slight_smile:
And if you keep the dataset on the worker there is not a lot of data moving back and forth for every keystroke. (Only search input and results)
So maybe it is a good idea with a worker here.

I had an idea when Evan wrote about exploring compact serialization for elm-data, what if we could use something like that for type safe data transfer between elm-workers.

That would be magical, that you define webworkers in elm.
(No manual inline worker, no ports or custom elements, just amazing pure elm) :slight_smile:
Inside the worker you could do all the heavy lifing… Large Http requests, heavy parsing of the result + storing in model. Everything off the main thread.
(Even indexedDB and websockets is supported in workers)
The only thing one would need on the main thread is handle UI stuff.
I guess lamdera could actually make something like that work, its more or less the same that it does between client and server already.
Tagging @supermario here in case he like the idea :slight_smile:

2 Likes

I’m using a web worker (written in TypeScript) for Firebase stuff in my Elm app.

@Atlewee Whoah, that would be awesome. I wonder if the Elm runtime could just “do it all” for you. So the view function ran in the main thread and everything else ran in workers. Just poking around (I’ve never looked at this part of the web worker API before) there’s stuff about “shared worker” and “shared global scope”. The support for it looks fairly limited though, probably a work in progress, but if something like that became widely supported that might work as the place to keep the model.

So many exciting possibilities.

@Laurent That’s interesting. I did an Elm project with Firebase and ports too, but it never occurred to me to put them in web workers. I was only dealing with tiny amount of data though.

Firebase libraries are super heavy compared to my app Elm bundle. A web worker allows me to use the Firebase REST API in Elm (no Firebase on the client needed) while the big Firebase libs are downloaded, parsed and run asynchronously in the worker during the first load of the app. So I get a quick first startup time and still get the benefits of a full Firebase setup (offline capabilities, optimistic updates and more).

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.