From 11a7919939f30318c6f2bf81058e57e2bb604b67 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 2 Sep 2026 13:42:24 -0700 Subject: Three render hogs put on a leash for the sluggish-tabs hunt: qrSvg is memoized (make() runs eight mask trials over a version-20 grid — 30ms+ per call on a phone, paid on EVERY render of the receive pane), the header's unread count reuses its answer behind a revision counter bumped only when messages or read-marks actually change, and ark's history list rebuilds at most every 1.5s instead of per repaint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr --- src/features/ark.js | 13 +++++++++++++ src/features/messages.js | 22 ++++++++++++++++++++-- src/qr.js | 12 +++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/features/ark.js b/src/features/ark.js index 97f730a..72624b4 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -2913,6 +2913,19 @@ export function arkFeature(ctx) { historyEntries() { const s = arkStateNow(); if (!s) return []; + // Memoized briefly: this rebuilds, sorts, and dedupes the whole + // movements list plus the accounted-inputs set — fine once, wasteful + // on every repaint. Movements only change through saves that trigger + // a render themselves, so a short reuse window costs nothing visible. + const key = `${(s.movements || []).length}:${(s.actions || []).length}`; + if (this._histMemo && this._histMemo.key === key && Date.now() - this._histMemo.t < 1500) { + return this._histMemo.val; + } + const val = this._histBuild(s); + this._histMemo = { key, t: Date.now(), val }; + return val; + }, + _histBuild(s) { // In-flight exits are payments the user made — they belong in history // as pending records, not on a monitoring page nobody thinks to visit. const exits = (s.actions || []) diff --git a/src/features/messages.js b/src/features/messages.js index c1f6d19..ce1975e 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -100,6 +100,7 @@ export function messagesFeature(ctx) { const s = st(); if (!ts || (s.read[key] || 0) >= ts) return; s.read[key] = ts; + bumpMsgRev(); save(s); // read markers ride their own sync domain (registered below) — nudging // the cache is what gets the watermark onto the relays for other devices @@ -117,7 +118,7 @@ export function messagesFeature(ctx) { for (const room of rooms.values()) for (const [id, msgs] of room.byChannel) bump(chRead(id), newestFrom(msgs.values(), (m) => !my.includes(m.author))); - if (changed) { save(s); try { wallet.saveCache(); } catch {} } + if (changed) { bumpMsgRev(); save(s); try { wallet.saveCache(); } catch {} } render(); } @@ -147,7 +148,7 @@ export function messagesFeature(ctx) { for (const [k, v] of Object.entries(d.msgRead)) { if (typeof v === 'number' && (s.read[k] || 0) < v) { s.read[k] = v; changed = true; } } - if (changed) wallet.saveFeatureState('messages', s); + if (changed) { bumpMsgRev(); wallet.saveFeatureState('messages', s); } }, }); } @@ -167,7 +168,20 @@ export function messagesFeature(ctx) { // How many conversations are waiting on us — the header only draws a dot, but // a count keeps the door open for a number later. Floored by the list-screen // watermark: once you've seen the list, the header stops repeating it. + // + // Memoized on a revision counter: the header asks on EVERY render, and the + // walk touches every cached message in every thread and room — a phone + // paid that for each toast and repaint. Message arrivals and read-marks + // bump the revision; everything else reuses the answer. + let msgRev = 0; + const bumpMsgRev = () => { msgRev++; }; + let _unreadMemo = { rev: -1, val: 0 }; function unreadCount() { + if (_unreadMemo.rev === msgRev) return _unreadMemo.val; + _unreadMemo = { rev: msgRev, val: unreadCountNow() }; + return _unreadMemo.val; + } + function unreadCountNow() { const s = st(); const my = myPubkeys(); const seen = s.read[HOME_READ] || 0; @@ -369,6 +383,7 @@ export function messagesFeature(ctx) { const msgs = room.byChannel.get(c.id) || room.byChannel.set(c.id, new Map()).get(c.id); if (!msgs.has(m.rumor.id)) msgs.set(m.rumor.id, m); } + bumpMsgRev(); return room; } @@ -406,6 +421,7 @@ export function messagesFeature(ctx) { if (rumor.kind === 9) { const msgs = room.byChannel.get(channelId) || room.byChannel.set(channelId, new Map()).get(channelId); msgs.set(rumor.id, { rumor, author }); + bumpMsgRev(); room.typing.delete(author); // the message itself ends the "typing…" } else if (rumor.kind === 5) { for (const e of rumor.tags.filter((x) => x[0] === 'e')) { @@ -1084,6 +1100,7 @@ export function messagesFeature(ctx) { function noteDM(peer, rumor, mine) { if (!peer || !rumor.id) return; threadOf(peer).set(rumor.id, { rumor, mine }); + bumpMsgRev(); scheduleRepaint(); } @@ -1216,6 +1233,7 @@ export function messagesFeature(ctx) { for (const m of list) threadOf(peer).set(m.id, { rumor: { id: m.id, pubkey: m.from, content: m.text, created_at: m.t, kind: 14 }, mine: isMe(m.from) }); } + bumpMsgRev(); if (swept) save(s); allUnsubs.push(subscribeOn(DM_RELAYS, { kinds: [1059], '#p': pks, limit: 400 }, (wrap) => { handleInboxWrap(wrap).catch(() => {}); diff --git a/src/qr.js b/src/qr.js index d02aed3..416252d 100644 --- a/src/qr.js +++ b/src/qr.js @@ -9,7 +9,14 @@ import qrcode from 'qrcode-generator'; // mode: pass 'Alphanumeric' for an all-uppercase QR-alphanumeric string (0-9 A-Z // space $%*+-./:) to pack it at 5.5 bits/char instead of byte mode's 8 — a much // smaller grid for the same data. Falls back to byte mode if the text doesn't fit. +// Memoized: a QR is a pure function of its input, and make() runs eight +// mask-penalty trials over the whole grid — 30ms+ on a phone for a long +// bitcoin: URI. The receive pane used to pay that on EVERY render. +const _memo = new Map(); export function qrSvg(text, { margin = 2, ec = 'M', mode } = {}) { + const key = `${ec}|${mode || ''}|${margin}|${text}`; + const hit = _memo.get(key); + if (hit) return hit; let qr; try { qr = qrcode(0, ec); @@ -33,8 +40,11 @@ export function qrSvg(text, { margin = 2, ec = 'M', mode } = {}) { } } - return ` + const svg = ` `; + _memo.set(key, svg); + if (_memo.size > 24) _memo.delete(_memo.keys().next().value); + return svg; } -- cgit v1.2.3