concur: Loading 1100 clickEl's is very slow

I have a list of 1100 users and I tried to make a button for each one (using clickEl) that returned the userId to a parent element, which I would use to jump to a new route. But it’s extremely slow loading that many click elements. It could handle a couple hundred well, but approaching 1000 the performance really plummets. Once they load up, it’s pretty responsive, but loading them take at least 10 seconds.

For now I just made a fake button function that uses a href instead of Concur’s events machinery:

linkButton :: String -> String -> Widget HTML ()
linkButton label url =  el_ E.a [ A.href . JS.pack $ url ]
                        $ el E.button [ A.class_ "pure-button" ] [ text label ]

which is really fast and fine for now since I’m just jumping to an URL, but at some point I’m sure it would be nice/essential to be able to use clickEl’s or some other event listener for a big list.

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Comments: 20 (14 by maintainers)

Commits related to this issue

Most upvoted comments

Hah well, it’s really a new feature, and I’m glad it solved your problem! The changes are all here - https://github.com/ajnsit/concur/commit/78ac2b224d1d2454ad4ddb014bc48950cc46481a.

Gist

Basically I added support for blocking IO to Concur. Then used it for a very common use case where async io was a bottleneck.

Background

All actions like liftIO and liftSTM are run async, and are effectively full widgets.

A common pattern with a lot of widgets (like button) was to use liftSTM to procure a Notify (a very quick operation), and then used the Notify to link the view click and the handler. This means that a single Button was effectively a sequence of two widgets, liftSTM and then the actual button UI.

Also all widgets are encapsulated and run async, and any changes to the widget tree causes a full dom render cycle (albeit with virtual-dom diffing).

Problem

So when a button is first added to the widget tree, it actually renders the liftSTM widget (i.e. no UI), which quickly finishes, and then renders the actual button UI.

When you add two buttons in quick succession, the first liftSTM widget renders and finishes, then the second liftSTM widget renders and finishes, and then the first button UI renders, and then the second button UI renders. The order can vary depending on how long it takes for a liftSTM to finish.

You see where this is going.

With 1100 button widgets, you have 1100 quick dom rerender cycles in a row which takes ~10 seconds on your machine, including the overhead for all the rapid widget sequencing.

Solution

I added a special function called awaitViewAction which provides a Notify to a widget, but does so with blocking IO. Blocking IO prevents DOM updates so cannot be used for long running IO actions, and hence I am reluctant to make it available to clients. However this specific use case of constructing a Notify in STM is guaranteed to be extremely short and can safely be exposed to client widgets.

With this change, on adding 1100 button widgets, the IO actions are quickly run in succession without touching the DOM, and finally each widget constructs its own UI which is populated into the DOM at once (one render total for all 1100 buttons). The same as what happens with your “fake” linkButton code.

Okay I’m pretty certain I understand why this is happening. Will push a fix soon-ish.