In another post, @DaveGifford said
Assuming others don’t want the plumber but do want to try this themselves, or worse, want to help the plumber1, this is a simple high-level explanation of what EventCatcher does. Most of this is informed by long experience with JavaScript event delegation, and while I think most everything carries over to TiddlyWiki, I might have something wrong. If so, please chime in.
The problem
Here’s a pretty simple tiddler:
title: Multiple Handlers
log: Last-pressed button shows here
<pre>{{!!log}}</pre>
<$button><$action-setfield $field="log" $value="alpha"/>alpha</$button>
<$button><$action-setfield $field="log" $value="bravo"/>bravo</$button>
<$button><$action-setfield $field="log" $value="charlie"/>charlie</$button>
<$button><$action-setfield $field="log" $value="delta"/>delta</$button>
<$button><$action-setfield $field="log" $value="echo"/>echo</$button>
(This example and the next ones are in EventCatcherSample.json (1.5 KB).)
When you click on one of the buttons, its text value is shown in the <pre>
box at the top.
Let’s examine something of what must happen in memory for this to work. Each <$button ...>
widget has a dedicated <$action-setfield ...>
widget which sets the log
field to a specific value. This eventually creates Document Object Model (DOM) <button...>
element on the page. Each of those elements has a handler attached to it.
I have not dug in to see how the handlers are implemented. They may be plain functions, they may be functions with associated closures, or they may be plain configuration objects which will be fed to a function. For the sake of argument, I’m going to assume they’re functions with closures. The same points apply for the other possibilities.
It’s not clear exactly how many bytes of memory a function will use, and it’s dependent on the browser implementation. But it will include the actually executable code, the text of the function, bindings for the parameters, closure variables, and anything else the vendor needs to implement a function. At a bare minimum, this will require many dozens of bytes.
This is not a big deal for our five buttons. But what if we had five thousand? Or five hundred thousand? All those bytes can add up to something our browser can’t handle.
This will happen any time there are long lists of elements with associated actions. They can eat up memory… with the invariable effect of slowing down the browser.
Not related to the markup
We should note that this has nothing to do with the static markup. It’s all about runtime behavior. The following tiddler, which creates the buttons and their actions with a <$list ...>
widget, and therefore has shorter code, has exactly the same behavior, and exactly the same problem:
title: Multiple handlers using list
log:Last-pressed button shows here
<pre>{{!!log}}</pre>
<$list filter="alpha bravo charlie delta echo" variable="text">
<$button><$action-setfield $field="log" $value=<<text>> /><<text>></$button>
</$list>
There will still be in memory a handler for every single entry in the list, whether there are five or five hundred thousand.
Solution using EventCatcher
Now let’s look at another version:
title: Single EventCatcher
log: Last-pressed button shows here
\define my-actions()
<$action-setfield $field="log" $value=<<dom-text>> />
\end
<pre>{{!!log}}</pre>
<$eventcatcher selector="button" $click=<<my-actions>> >
<button text="alpha">alpha</button>
<button text="bravo">bravo</button>
<button text="charlie">charlie</button>
<button text="delta">delta</button>
<button text="echo">echo</button>
</$eventcatcher>
This looks the same and has the same behavior. But the implementation is significantly different. Here we define an action to set the value of the field based on <<dom-text>>
, which, when the action is fired from an element, is filled from that element’s text
property.
We have a number of buttons (which, yes, we could have done in a <$list...>
, but again that has no effect on the issue at hand) which have text
attributes matching the button text.
Note that the buttons do not include any <$action-* ...>
widgets. Instead they are wrapped in an <$eventcatcher ...>
widget. The widget creates a container (<span class=" tc-eventcatcher"> ... </span>
) and is listening for click
events on any button
elements inside this container, forwarding them to our <<my-actions>>
macro.
This means that there is now a single event handler for our five (or five million) buttons.
That is the point. This allows us to save large amounts of memory and keep our wikis speedy.
Problems of Scale
This is a problem seen often in software and elsewhere. Things that work well at smaller scales start to choke as you grow larger. Event handlers cause no problems when you have a few or a few hundred on a page. But at some point they start to consume too many resources. Often, like here, there are ways around it. But the main thing is to be vigilant about it. If your wiki starts to slow down as it grows, it pays to try to imagine what’s happening with memory. If it looks to be getting crowded, you might have to find imaginative ways to clean things up.
Further Reading
- EventCatcherWidget documentation
- JavaScript Event delegation reference
- JavaScript Event delegation tutorial
- @saqimtiaz’s talk.tiddlywiki post Some thoughts on performance
1You know the old semi-joke for the plumber? “What’s your rate?” "$50/hour for the work, $100/hour if you watch, $200/hour if you help.