This started off as a reply to @stobot asking about performance related matters in the following thread: Help with listops and spaces - #5 by stobot
As such it might ramble a bit and this is mostly what I could think of off the top of my head, so please do correct and add as necessary.
Performance has been prominently on my mind recently as I have finally started the process of rewriting my Notation editor to use the new features from 5.1.23 and 5.2.0, many of which were directly inspired by that needs that arose while creating that editor. Now sometimes there are several new ways of doing things for which I previously used custom JavaScript, and I have to stop and consider the performance implications of each.
I will try to summarize some general thoughts on performance that might be helpful.
When thinking of performance, I tend to divide it into three areas:
- render performance - related to how long it takes to display something the first time
- refresh performance - related to how long it takes to refresh when underlying data (i.e. tiddlers) changes. Poorly implemented UI code can re-render entirely each time instead of selectively refreshing where needed.
- action performance - related to performance in response to use interaction like clicking a button or a keyboard shortcut. The realm of action widgets.
Render and refresh performance
Considerations like using fewer widgets and having a less complex widget tree apply to both refresh and render, though often can be felt more so during refreshing. In general this is often the area that you want to optimize heavily as the impact is most acutely perceived by users. When I did some heavy refactoring of Streams earlier this year, there was an almost 5 fold performance benefit in render time for very large streams and it came from the following:
- fewer widgets and DOM elements (DOM elements are also widgets)
- filtered transclusions to apply classes to change presentation instead of hiding and showing different DIVs/Widgets.
- more performant filter constructions
- avoiding macros that perform substitution
- avoiding the wikify widget. There are extremely few situations where the use of this widget is warranted, and using it to set variable values in most situations is an antipattern. Use filters and transclusions instead. If you need a good pattern to follow, the entire TiddlyWiki UI has been built with wikitext and wikify has been used only a tiny handful of times.
Note that even a relatively simple looking template can have a high render and refresh overhead if you are transcluding it many times, for example for many bullets.
Most widgets that create DOM nodes, such as Button or Reveal, accept tag and class attributes allowing you to control what type of HTML element they create and with which class name. Use this to avoid extra clutter in the HTML and the widget tree.
Instead of:
<$reveal type="match" state="$:/state/SampleReveal1" text="show">
<div class="myclass">
! This is the revealed content
And this is some text
</div>
</$reveal>
consider this:
<$reveal tag="div" class="myclass" type="match" state="$:/state/SampleReveal1" text="show">
! This is the revealed content
And this is some text
</$reveal>
Also worth noting here are techniques like the DynaView Widget to help with initial render times for very large data sets, and the new counter variable for the <$list>
widget which you want to avoid for any lists where the number of items will change frequently.
Refresh performance
There are some extra considerations here that add more nuance to what was just discussed above.
Fewer vars widgets or more?
Above I mentioned using fewer widgets, so the temptation can be to set your variables at the very beginning of a template if they will be used in several places. The vars
and set
(and wikify
but you don’t use that right?) widget will refresh, i.e. re-render their entire content, every time the calculated value of the variable changes. Therefore, often you will get vastly better refresh performance by using multiple widgets as needed to assign the same variable, placing those widgets as close to where they will be needed as possible.
Filtered transclusion for HTML element attributes.
All HTML elements in TW are widgets, that is why you can use transclusions and filtered transclusions for their attributes. Where possible, assign attributes directly from such transclusions instead of first assigning a variable and then using it. Attributes for all HTML elements are just updated as needed when the result of the transclusion changes, whereas any widget setting the variable will force a re-render of its contents when the variable changes.
So this:
<div class={{{ [...myfilter...] }}}></div>
instead of:
<$vars className={{{ [...myfilter...] }}}>
<div class=<<className>> ></div>
</$vars>
For widgets that generate DOM nodes like the ButtonWidget, I have slowly been optimizing the core code so that classes and similar attributes are simply updated instead of re-rendering the node and its contents. However, there are still some widgets where this isn’t true.
Macros to specify filters for list widget
Sometimes there can be a temptation to specify the filter for a list widget via a macro, something like:
<$list filter=<<getListFilter>> />
In such situations, if the output of the macro changes, the entire list is re-rendered since the filter attribute has changed. However, if your filter uses variables, the filter attribute does not change, only its output does and the list widget is heavily optimized to refresh its list items to match the new filter output.
Action performance
Always use the actions attribute of a widget to specify actions via a variable or macro, do not embed actions in the body of a widget. Not only does this open the door to using the new action refresh policy in a reliable manner - which makes for more intuitive code - but the resulting parse tree is much simpler and the action widgets aren’t part of the widget tree (and refresh cycle) when not being executed.
Of the three areas for performance consideration this is probably the least critical in that it will usually result in the least noticeable gains. I still haven’t gone back and improved the actions in Streams because there is no actual need, apart from a desire to tidy up and optimize code just for the sake of it.
Firstly, any widget tree created no matter how long or complex, is only transient. (It will be interesting to see if the new action refresh policy makes a difference here, though I doubt it will be noticeable in real world usage).
Secondly when dealing with a large list of items, a template needs to be rendered and refreshed for every single item, whereas actions often deal with a subset of the list only.
Thirdly, if there is a noticeable delay in response to a user action - which should only happen in the most complex of use cases - this is something users find less noticeable and are more accustomed to and accepting of, than render/refresh delays.
My recommendation is that especially during development, be very verbose with your action string to help with debugging and getting the logic right and don’t worry about using too many widgets. Optimize later if need be.
Set vs Vars widget
My general recommendation would be to always use <$vars>
unless:
- you need to return a list of titles from a filter and not just the first title.
Vars
will always return just the first title.Set
returns a title list with all the titles returned from the filter. - you really need the extra flexibility offered by the
set
widget where you can assign values (potentially via filtered transclusions) depending on what the filter attribute returns. In most cases you can do the same thing withvars
.
As of TiddlyWiki v5.2.1, $let should be used instead of $vars. You can assign a variable and then use that updated variable value to assign another variable in the same widget, further reducing the number of widgets needed.
<$let a="1" b={{{ [<a>add[1]] }}}/>
=> b will now have the value 2.
Eventcatcher and really long lists
When dealing with very long lists, using the $eventcatcher widget can help to vastly simplify the widget tree and improve both render and refresh performance.
Imagine a $list widget that generates a list with 500 items, and for each of them we are creating a list of buttons that invoke some actions when clicked. This means there are 500 $button widgets in our list that need to be rendered and refreshed. Using a single $eventcatcher widget around the list can allow us to replace these 500 $button widgets with the much lighter alternative of 500 DIVs, delegating the responsibility for intercepting clicks and triggering actions to the $eventcatcher widget.
As an example imagine a long list generated by a $list widget that creates a button for each list item:
<$list filter="....">
<$button tag="div" class="myclass" actions=<<myactions>> />
</$list>
Alternatively you can do something along these lines, which tells the $eventcatcher to listen for clicks inside DIVs with the class “myclass” and invoke actions in response.
<$eventcatcher $click=<<myactions>> selector="div.myclass">
<$list filter="....">
<div class="myclass" />
</$list>
</$eventcatcher>
Wikitext in CSS
TiddlyWiki allows us to use wikitext in CSS and the core already uses this for things like modifying the CSS based on whether the sidebar is hidden or shown, and to transclude colors from the palette. Overall, care should be taken with using wikitext in CSS as the stylesheets have to be recalculated on every keystroke to check if they need to be refreshed, and a simple list widget used in a stylesheet can result in a very large chunk of CSS.
However, in some situations this can be a powerful technique that if used properly can provide significant performance gains. Consider the above example of a list with 500 list items again. Now imagine that one of them should have a different class designating a “selected” state as specified by a state tiddler.
We could use a filtered transclusion to specify the class name, e.g:
<div class={{{ [[mystatetiddler]get[text]match<currentTiddler>then[activeClass]] }}}>
.
However, as fast as this simple filter is to execute, in a very large list it adds up. Instead, using dynamic CSS that transcludes the title of the active tiddler from the state tiddler and targets the list item via a data-
property can be significantly faster.
Example:
<div data-item-title=<<currentTiddler>>/>
and:
[data-item-title="{{{[{$:/temp/Notation/search/active}escapecss[]]}}}"] {
background-color: #5778d8;
color: #ffffff;
}