Full-source analysis of TiddlyWiki 5.4.0 refresh pipeline — performance findings and suggested fixes

This analysis was derived by reading the complete TiddlyWiki 5.4.0 empty.html source bundle — every widget module, wiki.js, boot.js, render.js, and all storyview modules. Findings differ from partial-source analyses in several places.

:page_facing_up: Full HTML report: Gist HTML Preview


Full refresh pipeline

wiki.addTiddler / wiki.deleteTiddler
├→ clearCache(title) ← per-title cache only (correct)
├→ clearGlobalCache() ← wipes ALL global caches (root cause of Issue 4)
├→ indexer.update(descriptor) ← notifies each registered indexer (correct)
└→ enqueueTiddlerEvent(title)
└→ utils.nextTick (coalesce)
└→ dispatchEvent(“change”, changedTiddlers)
├→ render.js throttledRefresh
│ ├→ pageWidgetNode.refresh(changedTiddlers) ← pass 1
│ │ └→ rootWidget.refreshChildren()
│ │ └→ for EVERY child widget:
│ │ ├→ computeAttributes()
│ │ │ └→ [filtered attrs] wiki.filterTiddlers()
│ │ ├→ refreshSelf() ← full destroy+render
│ │ └→ refreshChildren() ← recurse
│ ├→ styleWidgetNode.refresh(changedTiddlers) ← pass 2
│ └→ titleWidgetNode.refresh(changedTiddlers) ← pass 3


Key findings not visible from partial source

1. Three-pass problem

render.js registers three separate wiki.addEventListener("change", ...) listeners — one each for the page widget tree, the stylesheet widget, and the page title widget. Every coalesced change batch therefore triggers three independent O(W) refresh walks before a single pixel updates.

2. clearGlobalCache is unconditional

Both addTiddler and deleteTiddler in boot.js call this.clearGlobalCache() unconditionally after the per-title clearCache(title). This wipes every cached filter result even for completely unrelated changes — it is the upstream cause that makes filtered-attribute evaluation so expensive.

3. Widget refresh policy inconsistency

Reading every subclass reveals a spectrum:

  • draggable.js — checks specific named attributes before calling refreshSelf :white_check_mark:
  • dropzone.js — checks if(count(changedAttributes) > 0) — i.e. any attribute at all :cross_mark:
  • encrypt.js — returns false unconditionally (not for interactive use) :white_check_mark:

The problem is not a single design flaw but inconsistent application of the pattern across ~30 widget files.

4. Index cautions

The existing indexer system (addIndexer, indexer.update(descriptor)) is already called synchronously on every tiddler write. Adding widget-dependency lookups there would be a layering violation — widget state does not belong in the data layer. A monotonic change generation counter (3 lines in wiki.js) provides most of the cache-invalidation benefit at zero architectural risk.


Divergences from other analyses

Topic Other analyses This analysis (full source)
Refresh passes per change 1 assumed 3 confirmed (page + style + title)
clearGlobalCache Described as probable Confirmed unconditional in boot.js
Widget refresh policy Described as uniform problem Inconsistent per subclass — some correct, some broken
Index improvements Recommended Cautioned — widget-layer coupling risk
findDraft O(N) Flagged as high Confirmed but rated LOWFieldIndexer already exists

Recommended fixes (by impact/effort)

# Fix File(s) Impact Risk Effort
1 Empty changeset guard in refreshChildren widget.js High Minimal ~3 lines
2 Tighten dropzone.js / macrocall.js refresh policy 4 widget files High Low ~30 lines
3 Merge three render.js change listeners into one render.js Medium Low ~10 lines
4 Filtered attribute result cache (gen-counter based) widget.js, wiki.js High Medium ~25 lines
5 invokeActions: skip refreshSelf if gen unchanged widget.js Medium Medium ~15 lines
6 TagIndexer / FieldIndexer incremental update tag-indexer.js, field-indexer.js Medium Low ~30 lines
7 tiddlerTitles incremental insert-sort boot.js Low Low ~10 lines

Fixes 1–3 require no new data structures or API changes and can be applied and tested independently. Fix 4 needs a three-line gen-counter addition to wiki.js but remains architecturally clean.


This analysis was made by Claude Sonnet 4.6 (Anthropic). I fed it the full empty.html source bundle and asked it to read every widget subclass, boot.js, wiki.js, and render.js before drawing conclusions. Happy to discuss any of the findings or the proposed fix designs.