I had an idea that spawned from interest that the community had towards Elm and WASM. I thought Web Worker could be a much more approachable solution, but still offer great benefits that could roll into WASM eventually. If you don’t know what Web Workers are, read Using Web Workers. Essentially, they give us access to move work that is usually done on the main JS thread to a separate worker thread. This gives us the benefit of freeing up the main thread to do other things while heavy tasks are being processed. In elm’s case, the app’s view
and diff
blocks felt very appropriate for this since it doesn’t need access to the DOM in order to process.
I tested this out by modifying the compiled output of one of my projects so I had 2 files. index.js
is the main elm app and worker.js
is the Web Worker. They are basically just copies of the compiled output so they have access to all necessary functions. In theory, we could reduce the amount of total code down to only what is needing by either.
index.js
:
I add the following to the global scope
var worker = new Worker("worker.js");
and modify the _Browser_element function
var _Browser_element = _Debugger_element || F4(function(impl, flagDecoder, debugMetadata, args)
{
return _Platform_initialize(
flagDecoder,
args,
impl.init,
impl.update,
impl.subscriptions,
function(sendToApp, initialModel) {
var view = impl.view;
var domNode = args && args['node'] ? args['node'] : _Debug_crash(0);
var sendCurrNode = true;
worker.onmessage = function(e) {
var currNode = e.data[0];
var patches = e.data[1];
deserializeFunctions(currNode);
deserializeFunctions(patches);
domNode = _VirtualDom_applyPatches(domNode, currNode, patches, sendToApp);
};
return _Browser_makeAnimator(initialModel, function(model)
{
worker.postMessage([serializeFunctions(model), sendCurrNode ? serializeFunctions(_VirtualDom_virtualize(domNode)) : null]);
sendCurrNode = false;
});
}
);
});
This sets up the worker to send the currNode
only once and the new model
. We receive the previous virtual dom currNode
and the patches
in order to update it.
worker.js
:
var currNode;
onmessage = function(e) {
var initialNode = e.data[1];
if (initialNode) {
deserializeFunctions(initialNode);
currNode = initialNode;
}
var model = e.data[0];
deserializeFunctions(model);
var nextNode = A2(elm$core$Basics$composeR, author$project$Main$view, rtfeldman$elm_css$Html$Styled$toUnstyled)(model);
var patches = _VirtualDom_diff(currNode, nextNode);
postMessage([serializeFunctions(currNode), serializeFunctions(patches)]);
currNode = nextNode;
};
This sets up our message to process our model
and send the previous virtual dom currNode
and the patches
.
Before:
The view and diff took about 14ms, not a long time but can be longer for large apps or certain view logic. The main one I’m looking at is FP(First Paint) and L(Load). These are improved with Web Worker changes as you’ll see.