Amongst every JS person who has some experience, the single threaded nature of JS is seen as a limitation; although in parallel this simplification avoids alot of hard to fix errors. Some people like using JS enough to do audio with it 1 2 3 4. This seems like the wrong solution, as JS executes slowly; but in any case I have little experience in making audio via software that I wrote. Sound is rarely used, and when used rarely improves UX in information systems, in my experience. I will ignore this set of options in the rest of this article.
The internet being what it is, has created a plethora of options to support concurrent threads in JS:
- ServiceWorkers are widely supported, except the usual suspects 5. They are limited from accessing the DOM 6, and are intended for proxying, caching, pre-caching, transcoding, transport manipulation type responsibilities e.g. 7 8. Quite a lot of this work historically would have been on the server. People who like specs should read 9 (many words..).
- WebWorkers are the mostly widely supported technology mentioned in this article 10. As a mostly graphics centred person; Webworkers are more useful, as they can modify Canvas when they are not part of the DOM, and talk with remote API 11. Someone is building a shopping list of use-cases for WebWorkers 12
- SharedWorker are basically Webworkers, but with additional [c-level] process to allow them to be shared between tabs, of the same origin host 13. They are poorly supported by the browser range, but by a good percentage of the user population 14. Its spec is 15. Everything that applies to WebWorkers also applies to SharedWorkers, but you can build complex structures with less Workers when you share them. If your solution has no iframes, frames or parallel tabs ~ like a blog; SharedWorkers offer little benefit.
- Worklets are another possible API, which is basically a JS thread. They are marked as experiemental 16, and at time of writing pretty much only in Chrome 17. With wide support they would be really useful for poly-filling all the CSS that MSIE doesn't support, as they are designed to interact with the rendering pipeline on the page 18.
The “happy path” usage is very comparable to many networking and eventing libraries in JS. In aggregate, the process for all these libraries briefly is: 1) create the Worker, 2) postMessage the Worker your event, 3) addEventListener(“message” the response, and apply it to the DOM. This is documented in many blogs 19 20 21 (and more). Simple code structures destroy the worker after each work unit; however it seems smarter create/ destroy inline with the surrounding DOM, as it will have lower execution costs. Each Worker type technically a separate OS-level process thread 22, and has a separate memory space (this is normal in other languages). According to 23 (updated 10th August 2019) onmessage lacks some features, and so I'm avoiding it.
In terms of execution time, a Worker will have the same execution time, as the main thread. If you want to use the main thread for user activities (i.e. normal things), reducing the other work that this is doing is clearly good for event responsiveness. Most devices these days have more than one CPU core 24 (notably aside from low end phones), which means your app now seems faster. In some situations you may wish to avoid the additional file that having a Worker requires. There are workrounds 25 (I can imagine this is mostly interesting to serverless people). Although the scope the work regulates how many workers are desirable, a naïve guess would be not not exceed the number of CPU cores present. A worked example 26 27 states the best frequency is 8 (this will be dependant on the machine hosting it).
As noted above the communication is done via postMessage, and fortunately testing has been published 28 29. Examples of canvas code 30.
The value of this article is in analysis, not listing links to documentation. Ideas that should be considered when writing a medium to large project with large amounts of behaviour implemented in JS::
- A guy who did quite alot of testing 1 says any UI events need to take less than 16ms to not be noticeable by users 2, in terms a frame buffer refresh. This duration generally excludes anything over the network (so user tracking tools that a fairly-synchrounous Crown user profiling are annoyingly slow).
- [Untested] It looks like postMessage to the same process is faster to exec than Vue Events, Angular's $watch, angular-notify, or React state changes. This would be broadcast to all the registered classes in the same process; but you wouldn't have to wait to the $nextUpdate to execute. Testing is required to see if this works at traffic volume.
- Using multiple threads should discourage the use of global objects (as the thing you want will be in the other thread).
- Pulling SocketService type activities out of the DOM manipulation code will lead to cleaner and more readable code. If you have a client side Model fed by postMessage responses, this makes integrating to View/Rasteriser pipelines easy/clear.
- With regards to networking, the biggest benefit of using a Worker is on communication with slow synchronous API (see many middle-wares with heavy encryption suites such as Websphere, XML platforms 3 or anything SOAP 4 ), as the GUI isn't blocked by them.
- A SocketService can be wrapped/encapsulated to form a Queue; like a simple version of Kafka/ZeroMQ/RabbitMQ. People who like GraphQL will think this is important (can only bundle requests when you know what all the requests are).
- It is mentioned in most articles about Workers, that Assets can be downloaded out of the way; and spooned to users as they scroll. This should lead to a smoother scroll; although will be fiddly to organise round web-browser features doing the same thing.
- I like the idea of having an identity service thread, for federalised identification with a dedicated micro-service. This would be alot of CORs management; but is clean SingleSignon. In the separate thread, Session refresh requests can be issued to keep the Session current.
- Referencing 5, the fact you could do decompression/unzipping in JS doesn't mean that you should. Any web-browser has these features (in C/C++), and will run it much faster.
- With threads, there is the ability to send messages outside of your execution thread. The ability to catch any exceptions in the main JS thread and archive them on a logging service is quite useful 6 7 8 9 10 11 12 13 (and many others). Sending the data to a shared agent on a remote server has the massive gain that you can over see a 1000+ clients easily; at the loss that the operating context of a message is [likely to be] lost.
- In terms of computation, a JSON API response could be converted to a string holding an SVG blob 14 15 16 in a Worker thread; not blocking user activities. Locally-rendered SVGs would be setup to user preferences (high contrast etc) at less effort. This step normally costs about 200ms, building a graph, on my work laptop.
- Alternatively, an OffscreenCanvs can be built, then shipped back to the UI thread, for similar results. In a previous project, which used Canvas to make graphs; the execution time, on the same laptop as slightly higher (I think due to the shading/ fill options I was using).
- Some use cases may render to HTML tables, but that is less interesting.
- For apps like spreadsheets (with many inputs towards a large singular outcome); the use of threads allows more stable and less conspicuous server synchronisation. State changes are buffered and periodically uploaded, when there is a good connection; but the UI isn't affected. LocalStorage? can be used as a temporary cache if necessary. According to 17, the in browser indexed storage indexDB can be managed via a thread. I haven't seen any volume of data in indexDB, so I'm not sure this is useful.