aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-02 13:42:24 -0700
committerAdam Soltys <[email protected]>2026-09-02 13:42:24 -0700
commit11a7919939f30318c6f2bf81058e57e2bb604b67 (patch)
tree86fd103580bcda07099d08967a69e3f2affbfd7a /src
parent79ebdaa0426cc9dc474a61876207ff19fbc5aa22 (diff)
downloadcoinosv3-11a7919939f30318c6f2bf81058e57e2bb604b67.tar.gz
coinosv3-11a7919939f30318c6f2bf81058e57e2bb604b67.zip
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
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
Diffstat (limited to 'src')
-rw-r--r--src/features/ark.js13
-rw-r--r--src/features/messages.js22
-rw-r--r--src/qr.js12
3 files changed, 44 insertions, 3 deletions
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 `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges" class="qr">
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" shape-rendering="crispEdges" class="qr">
<rect width="${size}" height="${size}" fill="#fff"/>
<path d="${path}" fill="#111"/>
</svg>`;
+ _memo.set(key, svg);
+ if (_memo.size > 24) _memo.delete(_memo.keys().next().value);
+ return svg;
}