Purs-backend-es v1.3.0 released

I’m super excited to announce the latest 1.3 release of purs-backend-es, an alternative backend for modern ES code based on the purescript-backend-optimizer pipeline. This release contains a host of ES-specific improvements to bring it up to parity with the current JS backend (and beyond).

The JS backend employs a few optimizations specific to Effect and ST for writing fairly low-level code which generates more performant imperative JS. Previous versions of purs-backend-es did not support these optimizations, making performance potentially regress in some cases.

  • Loop inlining (foreachE, forE, whileE, etc). By inlining loops and their bodies, we can avoid thunk allocations for Effect on each iteration.
  • STRef unboxing, which can eliminate the {value: ...} box for references in some specific cases.

purs-backend-es can now perform these optimizations, but also a lot more!

  • General Ref unboxing, which applies to both Effect and ST.
  • Inlining of STArray and STObject operations (array push, object update, etc).
  • More robust and reliable ST transformations (due to the powerful inliner in purs-backend-optimizer).

As an anecdote, several years ago I wrote halogen-vdom with the goal of writing as much in PureScript as possible while still retaining production quality performance. I was fairly successful with about 95% of the code being written in PureScript, however, the core diffing algorithms were written in FFI for performance. I can confidently say that is no longer necessary! Compare the original FFI, and code that can be generated by purs-backend-es:

This is equivalent, and likely faster given that for/of iteration over Object.keys is more performant than for/in iteration nowadays. My hope and goal is that “write FFI code for performance” should not be a thing.


A less intense example shows that lower level array operations could be implemented in ST and achieve comparable code to current FFI. Here is an implementation of Array’s bind:

test2 :: forall a b. (a -> Array b) -> Array a -> Array b
test2 f as = STArray.run do
  bs <- STArray.new
  ST.foreach as \a -> do
    let as' = f a
    ST.foreach as' (void <<< flip STArray.push bs)
  pure bs

And the generated code:

const test2 = f => as => {
  const bs = [];
  for (const a of as) {
    for (const $0 of f(a)) {
  return bs;

Which is exactly what I’d hope for :smile:


Curious: how much would be different had browser actually supported proper tail calls back in ECMAScript 2015?

1 Like

Proper tail calls can make functional abstractions safe (as long as they are CPS’ed) and composable, but not necessarily fast (though likely faster than userland trampolines). You could setup a comparison in JSC (which supports PTCs). Using JSC is one reason why bun is interesting to me.

1 Like

This is very exciting stuff, super happy that the community has the tools, time and effort available to focus on these kinds of things now.


This is sooo exciting, I’m jumping up and down.

Do you know if the ES backend is being used in any large-ish production environments yet?


We are hopefully integrating this into the next major release of the Arista NDR product, which I would consider a large environment.