From c67593ccfa69a8f6f41d279893d3fb4fa1a827b1 Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 16 Jul 2026 10:56:26 -0400 Subject: [PATCH] feat(perf): use lighter subtrees --- plugins/AdminBackend/renderer.js | 36 ++++++++--- plugins/LastSeen/renderer.js | 58 +++++++++++++---- plugins/PrivateChannelMapper/renderer.js | 25 +++++++- plugins/SlimMessageBox/renderer.js | 44 ++++++++++--- plugins/StreamerMode/renderer.js | 79 ++++++++++++++++++------ plugins/UserPronouns/renderer.js | 17 ++++- plugins/WhoReacted/renderer.js | 35 ++++++++++- 7 files changed, 240 insertions(+), 54 deletions(-) diff --git a/plugins/AdminBackend/renderer.js b/plugins/AdminBackend/renderer.js index 4e11e63..1d0f6df 100644 --- a/plugins/AdminBackend/renderer.js +++ b/plugins/AdminBackend/renderer.js @@ -122,25 +122,45 @@ itemRow(reference, menu).after(...rows); } - function x() { - for (const root of document.querySelectorAll(SELECTOR)) { - const id = visible(root) && userIdOf(root); + function within(root, selector) { + const found = []; + if (root.nodeType === Node.ELEMENT_NODE && root.matches(selector)) found.push(root); + if (root.querySelectorAll) found.push(...root.querySelectorAll(selector)); + return found; + } + + function x(root = document) { + for (const profile of within(root, SELECTOR)) { + const id = visible(profile) && userIdOf(profile); if (id) profileId = id; } - for (const item of document.querySelectorAll('button,[role="menuitem"]')) { - if (!visible(item) || item.textContent.replace(/\s+/g, ' ').trim() !== 'Copy link to profile') continue; + for (const item of within(root, 'button,[role="menuitem"]')) { + if (item.textContent.replace(/\s+/g, ' ').trim() !== 'Copy link to profile' || !visible(item)) continue; const menu = item.closest('[role="menu"],.c-menu'); if (menu) injectMenu(menu, item); } } let timer = null; - const observer = new MutationObserver(() => { + const pendingRoots = new Set(); + function queue(root) { + if (root.nodeType !== Node.ELEMENT_NODE) return; + for (const pending of pendingRoots) { + if (pending.contains(root)) return; + if (root.contains(pending)) pendingRoots.delete(pending); + } + pendingRoots.add(root); + } + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => mutation.addedNodes.forEach(queue)); + if (!pendingRoots.size) return; if (timer) return; timer = setTimeout(() => { timer = null; - x(); - }, 50); + const roots = [...pendingRoots]; + pendingRoots.clear(); + roots.forEach(x); + }, 100); }); function boot() { diff --git a/plugins/LastSeen/renderer.js b/plugins/LastSeen/renderer.js index 41d8318..1897384 100644 --- a/plugins/LastSeen/renderer.js +++ b/plugins/LastSeen/renderer.js @@ -322,9 +322,12 @@ ); return { ts: ts[0], user: sender ? uid(sender) : uid(el) }; } - function scanMessages() { + function scanMessages(root = document) { if (!S().showLastMessage) return; - document.querySelectorAll(ROW_SEL).forEach((row) => { + const rows = []; + if (root.nodeType === Node.ELEMENT_NODE && root.matches(ROW_SEL)) rows.push(root); + if (root.querySelectorAll) rows.push(...root.querySelectorAll(ROW_SEL)); + rows.forEach((row) => { const m = rowMsg(row); if (m) markMsg(m.user, m.ts); }); @@ -345,7 +348,7 @@ return null; } - function roots() { + function roots(scope = document) { const r = new Set(); for (const sel of [ '[data-qa="member_profile_pane"]', @@ -365,7 +368,9 @@ ]) { let f; try { - f = document.querySelectorAll(sel); + f = []; + if (scope.nodeType === Node.ELEMENT_NODE && scope.matches(sel)) f.push(scope); + if (scope.querySelectorAll) f.push(...scope.querySelectorAll(sel)); } catch { continue; } @@ -531,9 +536,9 @@ (orig.trim() || 'Away'); } - function paint() { + function paint(scope = document) { const s = S(); - roots().forEach((root) => { + roots(scope).forEach((root) => { const id = uid(root); if (!id || id === 'USLACKBOT') return; if (s.trackWatchlist) watch(id); @@ -560,11 +565,18 @@ } catch {} } let pT = 0; - const sched = () => { + let fullScan = false; + const sched = (full = false) => { + fullScan ||= full; if (pT) return; pT = setTimeout(() => { pT = 0; - pall(); + if (fullScan) pall(); + else { + style(); + paint(); + } + fullScan = false; }, 200); }; @@ -574,14 +586,34 @@ if (!document.body) return setTimeout(boot, 200); patchWS(); pall(); + const pendingRoots = new Set(); + let mutationTimer = 0; + function queue(root) { + if (!root || root.nodeType !== Node.ELEMENT_NODE || root.closest('[data-slick-ls]')) return; + for (const pending of pendingRoots) { + if (pending.contains(root)) return; + if (root.contains(pending)) pendingRoots.delete(pending); + } + pendingRoots.add(root); + } new MutationObserver((mutations) => { - const external = mutations.some((mutation) => { - const target = mutation.target; - return target.nodeType !== Node.ELEMENT_NODE || !target.closest('[data-slick-ls]'); + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) queue(node.closest(SURF) || node); + }); }); - if (external) sched(); + if (!pendingRoots.size || mutationTimer) return; + mutationTimer = setTimeout(() => { + mutationTimer = 0; + const scopes = [...pendingRoots]; + pendingRoots.clear(); + scopes.forEach((scope) => { + scanMessages(scope); + paint(scope); + }); + }, 150); }).observe(document.body, { subtree: true, childList: true }); - addEventListener('slick:plugin-settings', sched); + addEventListener('slick:plugin-settings', () => sched(true)); addEventListener('storage', (e) => { if (e.key === 'slick:lastseen:cache') { C = load(); diff --git a/plugins/PrivateChannelMapper/renderer.js b/plugins/PrivateChannelMapper/renderer.js index b6d4314..46f781c 100644 --- a/plugins/PrivateChannelMapper/renderer.js +++ b/plugins/PrivateChannelMapper/renderer.js @@ -135,6 +135,11 @@ document.querySelectorAll(SEL).forEach(apply); } + function applyWithin(root) { + if (root.nodeType === Node.ELEMENT_NODE && root.matches(SEL)) apply(root); + if (root.querySelectorAll) root.querySelectorAll(SEL).forEach(apply); + } + let overlay = null; function closeEditor() { if (overlay) { @@ -185,11 +190,27 @@ }); let t = null; - const obs = new MutationObserver(() => { + const pendingRoots = new Set(); + function queue(root) { + if (!root || root.nodeType !== Node.ELEMENT_NODE) return; + for (const pending of pendingRoots) { + if (pending.contains(root)) return; + if (root.contains(pending)) pendingRoots.delete(pending); + } + pendingRoots.add(root); + } + const obs = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'characterData') queue(mutation.target.parentElement?.closest(SEL)); + mutation.addedNodes.forEach(queue); + }); + if (!pendingRoots.size) return; if (t) return; t = setTimeout(() => { t = null; - applyAll(); + const roots = [...pendingRoots]; + pendingRoots.clear(); + roots.forEach(applyWithin); }, 150); }); function boot() { diff --git a/plugins/SlimMessageBox/renderer.js b/plugins/SlimMessageBox/renderer.js index 6a47361..003e92b 100644 --- a/plugins/SlimMessageBox/renderer.js +++ b/plugins/SlimMessageBox/renderer.js @@ -46,20 +46,32 @@ scope.classList.remove('slick-smb-stacked'); } } - function applyDomHides() { + function applyDomHides(root) { var s = settings(); for (var key in HIDE_DOM) { if (!s[key]) continue; - document.querySelectorAll(SCOPE + ' ' + HIDE_DOM[key]).forEach(function (el) { + var insideScope = root.nodeType === Node.ELEMENT_NODE && root.closest(SCOPE); + var selector = insideScope ? HIDE_DOM[key] : SCOPE + ' ' + HIDE_DOM[key]; + var elements = []; + if (root.nodeType === Node.ELEMENT_NODE && root.matches(selector)) elements.push(root); + if (root.querySelectorAll) elements.push.apply(elements, root.querySelectorAll(selector)); + elements.forEach(function (el) { el.style.setProperty('display', 'none', 'important'); }); } } + function scan(root) { + var selector = SCOPE + ' .ql-editor, ' + SCOPE + ' [contenteditable="true"][role="textbox"]'; + var editors = []; + if (root.nodeType === Node.ELEMENT_NODE && root.matches(selector)) editors.push(root); + if (root.querySelectorAll) editors.push.apply(editors, root.querySelectorAll(selector)); + editors.forEach(evaluate); + var scope = root.nodeType === Node.ELEMENT_NODE && root.closest(SCOPE); + if (scope) scope.querySelectorAll('.ql-editor,[contenteditable="true"][role="textbox"]').forEach(evaluate); + applyDomHides(root); + } function scanAll() { - document - .querySelectorAll(SCOPE + ' .ql-editor, ' + SCOPE + ' [contenteditable="true"][role="textbox"]') - .forEach(evaluate); - applyDomHides(); + scan(document); } document.addEventListener( 'input', @@ -78,12 +90,28 @@ window.addEventListener('resize', scanAll); window.addEventListener('slick:plugin-settings', scanAll); var pending = false; - new MutationObserver(function () { + var pendingRoots = new Set(); + function queue(root) { + if (!root || root.nodeType !== Node.ELEMENT_NODE) return; + for (var existing of pendingRoots) { + if (existing.contains(root)) return; + if (root.contains(existing)) pendingRoots.delete(existing); + } + pendingRoots.add(root); + } + new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + queue(mutation.target.closest && mutation.target.closest(SCOPE)); + mutation.addedNodes.forEach(queue); + }); + if (!pendingRoots.size) return; if (pending) return; pending = true; setTimeout(function () { pending = false; - scanAll(); + var roots = Array.from(pendingRoots); + pendingRoots.clear(); + roots.forEach(scan); }, 150); }).observe(document.documentElement, { childList: true, subtree: true }); scanAll(); diff --git a/plugins/StreamerMode/renderer.js b/plugins/StreamerMode/renderer.js index b0ddff1..b65f3f7 100644 --- a/plugins/StreamerMode/renderer.js +++ b/plugins/StreamerMode/renderer.js @@ -175,8 +175,8 @@ if (el && el.nodeType === Node.ELEMENT_NODE) el.classList.add(className || 'slick-streamer-redact'); } - function clearMarks() { - document + function clearMarks(root) { + root .querySelectorAll( '.slick-streamer-redact,.slick-streamer-hide,.slick-streamer-avatar,.slick-streamer-private-label,.slick-streamer-private-row', ) @@ -191,6 +191,13 @@ }); } + function within(root, selector) { + var found = []; + if (root.nodeType === Node.ELEMENT_NODE && root.matches(selector)) found.push(root); + if (root.querySelectorAll) found.push.apply(found, root.querySelectorAll(selector)); + return found; + } + function bestTextParent(node) { var el = node && node.parentElement; var hops = 0; @@ -202,9 +209,9 @@ return node && node.parentElement; } - function markPrivateChannels() { + function markPrivateChannels(root) { if (settings().privateChannelNames === false) return; - document.querySelectorAll(PRIV_CONTAIN).forEach(function (container) { + within(root, PRIV_CONTAIN).forEach(function (container) { var names = container.querySelectorAll(PRIV_NAME); if (names.length) names.forEach(function (name) { @@ -212,7 +219,7 @@ }); else mark(container); }); - document.querySelectorAll(LOCK_ICON).forEach(function (icon) { + within(root, LOCK_ICON).forEach(function (icon) { if (!icon.closest(CHANNEL)) return; var row = icon.closest(LOCK_ROW); if (!row || row.closest('#slick-panel-overlay,#slick-config-backdrop')) return; @@ -233,9 +240,9 @@ }); } - function markDmPreviews() { + function markDmPreviews(root) { var mode = settings().dmPreviewBlur || 'all'; - document.querySelectorAll(DM_CONTAIN).forEach(function (container) { + within(root, DM_CONTAIN).forEach(function (container) { if (mode === 'all') { container .querySelectorAll('.c-avatar,img,[class*="avatar" i],[data-qa*="avatar" i],[style*="background-image" i]') @@ -254,12 +261,12 @@ }); } - function markVipStatus() { + function markVipStatus(root) { if (settings().vipStatus === false) return; - document.querySelectorAll(VIP).forEach(function (el) { + within(root, VIP).forEach(function (el) { mark(el, 'slick-streamer-hide'); }); - document.querySelectorAll(VIP_SCOPE).forEach(function (scope) { + within(root, VIP_SCOPE).forEach(function (scope) { var walker = document.createTreeWalker(scope, NodeFilter.SHOW_TEXT, { acceptNode: function (node) { if (!VIP_RE.test(node.nodeValue || '')) return NodeFilter.FILTER_REJECT; @@ -273,17 +280,30 @@ }); } - function scan() { + function scan(root) { + var scopes = new Set([root]); + if (root.nodeType === Node.ELEMENT_NODE) { + [PRIV_CONTAIN, LOCK_ROW, DM_CONTAIN, VIP_SCOPE].forEach(function (selector) { + var parent = root.closest(selector); + if (parent) scopes.add(parent); + }); + } + scopes.forEach(function (scope) { + markPrivateChannels(scope); + markDmPreviews(scope); + markVipStatus(scope); + }); + } + + function scanAll() { scanTimer = null; - clearMarks(); - markPrivateChannels(); - markDmPreviews(); - markVipStatus(); + clearMarks(document); + scan(document); } function ss() { if (scanTimer) return; - scanTimer = setTimeout(scan, 0); + scanTimer = setTimeout(scanAll, 50); } function trackStream(stream) { @@ -365,11 +385,32 @@ pn(); if (document.body) resetRootClasses(document.body); refr(); - scan(); - new MutationObserver(scan).observe(document.documentElement, { childList: true, subtree: true }); + scan(document); + var pendingRoots = new Set(); + var rootsTimer = null; + function queue(root) { + if (root.nodeType !== Node.ELEMENT_NODE) return; + for (var pending of pendingRoots) { + if (pending.contains(root)) return; + if (root.contains(pending)) pendingRoots.delete(pending); + } + pendingRoots.add(root); + } + new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + mutation.addedNodes.forEach(queue); + }); + if (!pendingRoots.size || rootsTimer) return; + rootsTimer = setTimeout(function () { + rootsTimer = null; + var roots = Array.from(pendingRoots); + pendingRoots.clear(); + roots.forEach(scan); + }, 100); + }).observe(document.documentElement, { childList: true, subtree: true }); window.addEventListener('slick:plugin-settings', function () { refr(); - scan(); + ss(); }); } diff --git a/plugins/UserPronouns/renderer.js b/plugins/UserPronouns/renderer.js index feecffb..ad4c2f5 100644 --- a/plugins/UserPronouns/renderer.js +++ b/plugins/UserPronouns/renderer.js @@ -242,7 +242,11 @@ if (el.textContent !== p) el.textContent = p; } let lastTsColor = ''; - function syncColor() { + let lastColorSync = 0; + function syncColor(force = false) { + const now = Date.now(); + if (!force && now - lastColorSync < 5000) return; + lastColorSync = now; const label = document.querySelector('.c-timestamp__label'); if (!label) return; const color = getComputedStyle(label).color; @@ -267,7 +271,7 @@ } function paintAll() { - syncColor(); + syncColor(true); paintWithin(document); } @@ -278,10 +282,17 @@ let t = null; const pendingRoots = new Set(); + function queue(root) { + for (const pending of pendingRoots) { + if (pending.contains(root)) return; + if (root.contains(pending)) pendingRoots.delete(pending); + } + pendingRoots.add(root); + } const obs = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) pendingRoots.add(node); + if (node.nodeType === Node.ELEMENT_NODE) queue(node); }); }); if (!pendingRoots.size) return; diff --git a/plugins/WhoReacted/renderer.js b/plugins/WhoReacted/renderer.js index c84c5af..d0e76eb 100644 --- a/plugins/WhoReacted/renderer.js +++ b/plugins/WhoReacted/renderer.js @@ -247,6 +247,17 @@ document.querySelectorAll('.c-reaction').forEach(paintReaction); } + function paintWithin(root) { + const reactions = new Set(); + if (root.nodeType === Node.ELEMENT_NODE) { + if (root.matches('.c-reaction')) reactions.add(root); + const parent = root.closest('.c-reaction'); + if (parent) reactions.add(parent); + } + if (root.querySelectorAll) root.querySelectorAll('.c-reaction').forEach((reaction) => reactions.add(reaction)); + reactions.forEach(paintReaction); + } + let paintTimer = null; function schedulePaint() { if (paintTimer) return; @@ -258,7 +269,29 @@ window.__slickWhoReacted = { pa, avatars }; - const obs = new MutationObserver(schedulePaint); + let rootsTimer = null; + const pendingRoots = new Set(); + function queue(root) { + for (const pending of pendingRoots) { + if (pending.contains(root)) return; + if (root.contains(pending)) pendingRoots.delete(pending); + } + pendingRoots.add(root); + } + const obs = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) queue(node); + }); + }); + if (!pendingRoots.size || rootsTimer) return; + rootsTimer = setTimeout(() => { + rootsTimer = null; + const roots = [...pendingRoots]; + pendingRoots.clear(); + roots.forEach(paintWithin); + }, 150); + }); function boot() { if (!document.body) return setTimeout(boot, 200); pa();