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.
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 callingrefreshSelf
dropzone.js— checksif(count(changedAttributes) > 0)— i.e. any attribute at all
encrypt.js— returnsfalseunconditionally (not for interactive use)
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 LOW — FieldIndexer 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.