dsh-perf-boost 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@
4
4
 
5
5
  ## 更新详情
6
6
 
7
+ ### 0.2.2
8
+
9
+ - 优化了长对话省渲染的实现,改成一处分样式接管子项,新消息进来自动生效的效果
10
+ - 修复了流式输出时每来一条消息就扫一遍页面的问题,改成一分钟几十次的空闲巡检的问题
11
+
7
12
  ### 0.2.1
8
13
 
9
14
  - 新增了「大会话预热」的开关,设置页里点一下就能关的功能
package/README.md CHANGED
@@ -9,6 +9,11 @@ Client-side rendering optimizations for DSH Desktop.
9
9
 
10
10
  ## 更新详情
11
11
 
12
+ ### 0.2.2
13
+
14
+ - 优化了长对话省渲染的实现,改成一处分样式接管子项,新消息进来自动生效的效果
15
+ - 修复了流式输出时每来一条消息就扫一遍页面的问题,改成一分钟几十次的空闲巡检的问题
16
+
12
17
  ### 0.2.1
13
18
 
14
19
  - 新增了「大会话预热」的开关,设置页里点一下就能关的功能
@@ -35,7 +40,7 @@ Client-side rendering optimizations for DSH Desktop.
35
40
 
36
41
  | 开关 | 默认 | 影响视觉 | 作用 |
37
42
  | --- | --- | --- | --- |
38
- | 长对话省渲染 | 开 | 无 | 列表子项加 `content-visibility: auto`,占位高度取该列表实测平均子项高度;末尾 4 项与正在流式输出的消息不处理,超过 300px 的代码块同样跳过 |
43
+ | 长对话省渲染 | 开 | 无 | 给滚动列表容器挂一个 class,子项的 `content-visibility: auto` 由一条 CSS 规则接管(新消息自动生效,不用逐个打 inline style),占位高度取该列表实测平均子项高度;末尾 4 项与标了 `aria-busy` 的消息保持正常渲染,代码块单独按 300px 跳过 |
39
44
  | 窗口隐藏不排帧 | 开 | 无 | 页面不可见时 `schedule()` 直接返回;切回时有 `visibilitychange` 钩子恢复,每秒看门狗兜底 |
40
45
  | 空闲降帧 | 开 | 无 | 20 秒无输入将 rAF 驱动的动画降到 30fps,有输入立即恢复 |
41
46
  | 媒体懒加载 | 开 | 无 | 图片加 `loading=lazy`、`decoding=async`,视频加 `preload=metadata` |
Binary file
package/lib/client.js CHANGED
@@ -26,16 +26,25 @@ window.__ModuleLoader__.load({
26
26
  var NAME = 'dsh-perf-boost';
27
27
  var STORE = 'dsh-perf-boost:cfg';
28
28
  var STYLE_ID = 'dpb-style';
29
- var VERSION = '0.2.1';
29
+ var CV_CLASS = 'dpb-cv'; // 挂在滚动列表容器上,子项的 content-visibility 交给 CSS
30
+ var CV_VAR = '--dpb-cis';
31
+ var VERSION = '0.2.2';
30
32
  // 更新公告:只用「新增了…的功能 / 修复了…的问题 / 优化了…的效果」三种句式。
31
33
  var RELEASE_NOTES = [
34
+ {
35
+ version: '0.2.2',
36
+ items: [
37
+ '优化了长对话省渲染的实现,改成一条 CSS 规则接管,新消息进来自动生效的效果',
38
+ '修复了流式输出时每来一条消息就扫一遍页面的问题,改成一分钟几十次的空闲巡检',
39
+ ],
40
+ },
32
41
  {
33
42
  version: '0.2.1',
34
43
  items: [
35
44
  '新增了大会话预热的开关,可以直接在设置里关掉的功能',
36
45
  '新增了浅色模式的配色,开关和边框在浅色主题下也看得清的效果',
37
- '优化了开关说明的文字,统一说成「可能会导致…」的效果',
38
- '新增了百分比改成最近 1 分钟窗口,长时间使用不会越用越低的功能',
46
+ '优化了开关说明的文字,统一说成「可能会导致…」的效果',
47
+ '新增了百分比改成最近 1 分钟窗口,长时间使用不会越用越低的功能',
39
48
  '优化了开关列表的顺序,默认开启的排在前面、默认关闭的放在后面的效果',
40
49
  ],
41
50
  },
@@ -95,7 +104,7 @@ window.__ModuleLoader__.load({
95
104
  .catch(function () { return null; });
96
105
  }
97
106
 
98
- function checkUpdate() {
107
+ function checkUpdate(force) {
99
108
  if (window.__dpbUpdating) return;
100
109
  window.__dpbUpdating = true;
101
110
  update.state = 'checking';
@@ -107,7 +116,7 @@ window.__ModuleLoader__.load({
107
116
  method: 'POST',
108
117
  credentials: 'same-origin',
109
118
  headers: { 'content-type': 'application/json' },
110
- body: '{}',
119
+ body: force === true ? '{"force":true}' : '{}',
111
120
  signal: ctl ? ctl.signal : undefined,
112
121
  })
113
122
  .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)); })
@@ -544,82 +553,46 @@ window.__ModuleLoader__.load({
544
553
  return Math.max(48, Math.round(sum / cnt));
545
554
  }
546
555
 
547
- function isLive(el) {
556
+ // 容器上挂 class,子项交给 CSS。
557
+ // 逐项打 inline style 等于「每来一条消息就把整个列表摸一遍」,长会话里这笔开销
558
+ // 比省下来的渲染还贵;改成一处分样式后,新消息天然被覆盖,JS 只负责找容器。
559
+ function markContainer(el) {
560
+ if (el.children.length < 24) return;
548
561
  try {
549
- if (el.getAttribute && (el.getAttribute('aria-busy') === 'true' || el.getAttribute('data-streaming') === 'true')) return true;
550
- return !!(el.querySelector && el.querySelector('[aria-busy="true"]'));
551
- } catch (e) { return false; }
562
+ if (!el.classList.contains(CV_CLASS)) el.classList.add(CV_CLASS);
563
+ var h = avgChildHeight(el);
564
+ if (el.style.getPropertyValue(CV_VAR) !== h + 'px') el.style.setProperty(CV_VAR, h + 'px');
565
+ } catch (e) { void e; }
552
566
  }
553
567
 
554
- function tagItems(record) {
555
- var el = record.el;
556
- var size = 'auto ' + avgChildHeight(el) + 'px';
557
- // 列表不长就没有收益,只增加出错面:少于 24 项直接不动。
558
- if (el.children.length < 24) return;
559
- // 末尾 4 项是正在生成/刚贴上的内容,免掉跳过渲染,避免「正在动的画面被冻住」。
560
- var lastSafe = Math.max(0, el.children.length - 4);
561
- for (var i = 0; i < lastSafe; i++) {
562
- var it = el.children[i];
563
- if (it.style.contentVisibility === 'auto') continue;
564
- if (isLive(it)) continue;
565
- record.items.push({ el: it, cv: it.style.contentVisibility, cis: it.style.containIntrinsicSize });
566
- it.style.contentVisibility = 'auto';
567
- it.style.containIntrinsicSize = size;
568
- }
569
- var pres = el.querySelectorAll('pre');
570
- for (var k = 0; k < pres.length; k++) {
571
- var pre = pres[k];
572
- if (pre.scrollHeight <= 300 || pre.style.contentVisibility === 'auto') continue;
573
- record.items.push({ el: pre, cv: pre.style.contentVisibility, cis: pre.style.containIntrinsicSize });
574
- pre.style.contentVisibility = 'auto';
575
- pre.style.containIntrinsicSize = 'auto 300px';
576
- }
568
+ function unmark(el) {
569
+ try {
570
+ el.classList.remove(CV_CLASS);
571
+ el.style.removeProperty(CV_VAR);
572
+ } catch (e) { void e; }
577
573
  }
578
574
 
579
575
  function clearAll() {
580
- for (var i = 0; i < tagged.length; i++) {
581
- var rec = tagged[i];
582
- for (var j = 0; j < rec.items.length; j++) {
583
- var it = rec.items[j];
584
- try {
585
- it.el.style.contentVisibility = it.cv || '';
586
- it.el.style.containIntrinsicSize = it.cis || '';
587
- } catch (e) { void e; }
588
- }
589
- }
576
+ for (var i = 0; i < tagged.length; i++) unmark(tagged[i]);
590
577
  tagged = [];
591
578
  }
592
579
 
593
- function recordFor(el) {
594
- for (var i = 0; i < tagged.length; i++) if (tagged[i].el === el) return tagged[i];
595
- return null;
596
- }
597
-
598
580
  function applyLazyList() {
599
581
  if (!cfg.lazyList) { clearAll(); return; }
600
582
  var targets = collectTargets();
601
- if (!targets.length) return;
602
583
  for (var i = tagged.length - 1; i >= 0; i--) {
603
- if (targets.indexOf(tagged[i].el) < 0 && !document.contains(tagged[i].el)) tagged.splice(i, 1);
584
+ if (targets.indexOf(tagged[i]) < 0 || !document.contains(tagged[i])) {
585
+ unmark(tagged[i]);
586
+ tagged.splice(i, 1);
587
+ }
604
588
  }
605
589
  for (var t = 0; t < targets.length; t++) {
606
590
  var el = targets[t];
607
- var rec = recordFor(el);
608
- if (!rec) {
609
- rec = { el: el, items: [] };
610
- tagged.push(rec);
611
- }
612
- try { tagItems(rec); } catch (e) { void e; }
591
+ if (tagged.indexOf(el) < 0) tagged.push(el);
592
+ try { markContainer(el); } catch (e) { void e; }
613
593
  }
614
594
  var n = 0;
615
- for (var m = 0; m < tagged.length; m++) {
616
- var items = tagged[m].items;
617
- // 被替换掉、被压缩掉的子项还留着引用的话,已脱离文档的 DOM 永远回收不了。
618
- for (var q = items.length - 1; q >= 0; q--) {
619
- if (!document.contains(items[q].el)) items.splice(q, 1);
620
- }
621
- n += items.length;
622
- }
595
+ for (var m = 0; m < tagged.length; m++) n += Math.max(0, tagged[m].children.length - 4);
623
596
  MET.lazyItems = n;
624
597
  }
625
598
 
@@ -628,10 +601,10 @@ window.__ModuleLoader__.load({
628
601
  if (!cfg.lazyList) return '已关闭。';
629
602
  var total = 0, lists = 0, kids = 0;
630
603
  for (var i = 0; i < tagged.length; i++) {
631
- if (!document.contains(tagged[i].el)) continue;
604
+ if (!document.contains(tagged[i])) continue;
632
605
  lists++;
633
- total += tagged[i].items.length;
634
- kids += tagged[i].el.children.length;
606
+ total += Math.max(0, tagged[i].children.length - 4);
607
+ kids += tagged[i].children.length;
635
608
  }
636
609
  if (!lists) {
637
610
  var targets = [];
@@ -674,44 +647,20 @@ window.__ModuleLoader__.load({
674
647
  }
675
648
  }
676
649
 
650
+ // 只做「容器换没换」的巡检。子项由 CSS 自己跟上,所以不必盯整棵 DOM——
651
+ // 盯子树会在流式输出时被每一条新消息唤醒,那笔开销反而比省下来的大。
677
652
  function watchLazyList() {
678
- if (window.__dpbObserver || typeof MutationObserver !== 'function') return;
679
- window.__dpbObserver = true;
680
- var timer = null;
681
- var rescan = function () {
653
+ if (window.__dpbWatcher) return;
654
+ window.__dpbWatcher = true;
655
+ window.setInterval(function () {
656
+ if (document.hidden) return;
657
+ if (!cfg.lazyList && !cfg.mediaLazy && !cfg.mediaPause) return;
682
658
  idleRun(function () {
683
659
  try { applyLazyList(); } catch (e) { void e; }
684
660
  try { applyMediaLazy(); } catch (e) { void e; }
685
661
  try { applyViewportMedia(); } catch (e) { void e; }
686
662
  }, 400);
687
- };
688
- var obs = new MutationObserver(function () {
689
- if (timer) return;
690
- // 滚动中、或者刚跑完长任务:这会儿别排队,等安静下来后的下一次变动再说。
691
- if (Date.now() - lastBusyAt < 200) return;
692
- timer = window.setTimeout(function () {
693
- timer = null;
694
- if (document.hidden) return;
695
- rescan();
696
- }, 1500);
697
- });
698
- obs.observe(document.body, { childList: true, subtree: true });
699
- // 盯整页太吵:列表出现后把观察点挪到列表本身;列表被整体替换时再挪回来。
700
- var watchTarget = null;
701
- var follow = function () {
702
- if (document.hidden) return;
703
- var list = null;
704
- try { list = collectTargets()[0] || null; } catch (e) { list = null; }
705
- var next = list && list.isConnected ? list : document.body;
706
- if (next === watchTarget) return;
707
- try { obs.observe(next, { childList: true, subtree: true }); } catch (e) { void e; }
708
- watchTarget = next;
709
- };
710
- follow();
711
- window.setInterval(follow, 10000);
712
- document.addEventListener('visibilitychange', function () {
713
- if (document.hidden && timer) { window.clearTimeout(timer); timer = null; }
714
- });
663
+ }, 2500);
715
664
  }
716
665
 
717
666
  // ————————————————— 媒体懒加载 —————————————————
@@ -967,6 +916,12 @@ window.__ModuleLoader__.load({
967
916
  '.dpb-knob{position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;background:color-mix(in oklab,currentColor 92%,transparent);transition:transform .15s;box-shadow:0 0 0 1px color-mix(in oklab,currentColor 10%,transparent)}',
968
917
  '.dpb-sw[data-on="1"] .dpb-knob{transform:translateX(17px);background:#fff}',
969
918
  '.dpb-quiet *, .dpb-quiet *::before, .dpb-quiet *::after{animation-play-state:paused!important}',
919
+ // 子项:滚出视口就不渲染;末尾 4 项和正在生成的那条保持正常渲染。
920
+ '.' + CV_CLASS + ' > *{content-visibility:auto;contain-intrinsic-size:auto var(' + CV_VAR + ',220px)}',
921
+ '.' + CV_CLASS + ' > :nth-last-child(-n+4){content-visibility:visible}',
922
+ '.' + CV_CLASS + ' > [aria-busy="true"]{content-visibility:visible}',
923
+ // 大代码块单独跳过:一条消息里有几千行 pre 时,救的是这一条。
924
+ '.' + CV_CLASS + ' pre{content-visibility:auto;contain-intrinsic-size:auto 300px;overflow-anchor:none}',
970
925
  '.dpb-notes{margin:12px 2px 0;font-size:12px;line-height:1.8;opacity:.78}',
971
926
  '.dpb-notes b{font-weight:600;opacity:1}',
972
927
  '.dpb-note{margin:10px 2px 0;font-size:12px;line-height:1.7;opacity:.72}',
@@ -1152,7 +1107,7 @@ window.__ModuleLoader__.load({
1152
1107
  : '—')),
1153
1108
  React.createElement('button', {
1154
1109
  type: 'button', className: 'dpb-btn',
1155
- onClick: function () { checkUpdate(); },
1110
+ onClick: function () { checkUpdate(true); },
1156
1111
  }, update.state === 'checking' ? '检查中' : '检查更新')),
1157
1112
  React.createElement('div', { className: 'dpb-notes' },
1158
1113
  React.createElement('b', null, '更新详情'),
@@ -1173,7 +1128,7 @@ window.__ModuleLoader__.load({
1173
1128
  try { applyMediaLazy(); } catch (e) { void e; }
1174
1129
  try { applyViewportMedia(); } catch (e) { void e; }
1175
1130
  }, 600);
1176
- if (cfg.lazyList) watchLazyList();
1131
+ watchLazyList();
1177
1132
  if (cfg.showFps) startBadge();
1178
1133
  else {
1179
1134
  var el = document.getElementById(BADGE_ID);
package/lib/index.js CHANGED
@@ -11,6 +11,7 @@
11
11
  // 路由只读、无副作用:只取 `latest` 标签的版本号,不下载、不安装、不落盘。
12
12
  // 网络不可用时返回 `latest: null`,页面显示「未查到」,其余功能不受影响。
13
13
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
14
+ import https from 'node:https';
14
15
  import { homedir } from 'node:os';
15
16
  import { join } from 'node:path';
16
17
 
@@ -32,7 +33,10 @@ const PACKAGE_NAME = 'dsh-perf-boost';
32
33
  // npmmirror 优先(国内快),失败再问官方源。
33
34
  const REGISTRIES = ['https://registry.npmmirror.com', 'https://registry.npmjs.org'];
34
35
  const CACHE_MS = 10 * 60 * 1000;
35
- const TIMEOUT_MS = 8000;
36
+ const TIMEOUT_MS = 5000;
37
+ // 复用连接:第一次握手 200ms 上下,之后 40ms 上下。宿主里的全局 fetch 首次常常
38
+ // 要 3-4 秒(undici 独立连接池 + 冷 DNS),本机实测差 20 倍。
39
+ const AGENT = new https.Agent({ keepAlive: true, maxSockets: 4 });
36
40
 
37
41
  let cache = { at: 0, latest: null, source: '', error: '' };
38
42
 
@@ -63,45 +67,57 @@ export function isNewer(candidate, current) {
63
67
  return false;
64
68
  }
65
69
 
66
- async function fetchLatest(registry) {
67
- const url = `${registry}/${PACKAGE_NAME}/latest`;
68
- const response = await fetch(url, {
69
- headers: { accept: 'application/json' },
70
- signal: AbortSignal.timeout(TIMEOUT_MS),
70
+ function httpsJson(url) {
71
+ return new Promise((resolve, reject) => {
72
+ const request = https.get(url, { agent: AGENT, family: 4, headers: { accept: 'application/json' } }, (response) => {
73
+ const status = response.statusCode ?? 0;
74
+ if (status !== 200) {
75
+ response.resume();
76
+ reject(new Error(`HTTP ${status}`));
77
+ return;
78
+ }
79
+ const chunks = [];
80
+ response.on('data', (chunk) => chunks.push(chunk));
81
+ response.on('end', () => {
82
+ try {
83
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
84
+ } catch {
85
+ reject(new Error('响应不是 JSON'));
86
+ }
87
+ });
88
+ response.on('error', reject);
89
+ });
90
+ request.setTimeout(TIMEOUT_MS, () => request.destroy(new Error('timeout')));
91
+ request.on('error', reject);
71
92
  });
72
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
73
- const body = await response.json();
93
+ }
94
+
95
+ async function fetchLatest(registry) {
96
+ const body = await httpsJson(`${registry}/${PACKAGE_NAME}/latest`);
74
97
  const version = body?.version;
75
98
  if (typeof version !== 'string' || version === '') throw new Error('响应里没有 version');
76
99
  return { version, source: registry };
77
100
  }
78
101
 
79
- async function resolveLatest() {
102
+ async function resolveLatest(force = false) {
80
103
  const now = Date.now();
81
- if (cache.at !== 0 && cache.at !== undefined && now - cache.at < CACHE_MS) return cache;
104
+ if (!force && cache.at !== 0 && cache.at !== undefined && now - cache.at < CACHE_MS) return cache;
82
105
  let lastError = '未连接';
83
106
  let notPublished = false;
84
- let best = null;
85
- let bestSource = '';
107
+ // 先问第一个源,拿到就回;失败才轮到下一个。
108
+ // 之前是「两边都问、取更高的版本」,多花一次跨洋往返(本机实测 2.0-2.5 秒),
109
+ // 为的只是防止镜像比官方慢半拍;版本号差半分钟不影响点「检查更新」这件事。
86
110
  for (const registry of REGISTRIES) {
87
111
  try {
88
112
  const { version, source } = await fetchLatest(registry);
89
- // 镜像可能比官方慢半拍,两边都问,取版本更高的那个,别被慢的镜像带偏。
90
- if (best === null || isNewer(version, best)) {
91
- best = version;
92
- bestSource = source;
93
- }
113
+ cache = { at: now, latest: version, source, error: '' };
114
+ return cache;
94
115
  } catch (error) {
95
116
  lastError = error?.message ?? String(error);
96
- // 所有镜像都 404 = 这个包还没发到 npm,不算「网络失败」。
97
- if (lastError === 'HTTP 404') notPublished = true;
98
- else notPublished = false;
117
+ // 这个源说 404 = 它上面没有这个包,换下一个源继续试。
118
+ notPublished = lastError === 'HTTP 404';
99
119
  }
100
120
  }
101
- if (best !== null) {
102
- cache = { at: now, latest: best, source: bestSource, error: '' };
103
- return cache;
104
- }
105
121
  cache = { at: now, latest: null, source: '', error: notPublished ? 'not-published' : lastError };
106
122
  return cache;
107
123
  }
@@ -162,6 +178,14 @@ export function apply(ctx, config) {
162
178
  .catch(() => { void 0; });
163
179
  }
164
180
 
181
+ // 启动后先在后台问一次,设置页打开时直接命中缓存(失败无所谓,路由里还会再试)。
182
+ try {
183
+ const warmTimer = setTimeout(() => { resolveLatest().catch(() => { void 0; }); }, 3000);
184
+ if (typeof warmTimer?.unref === 'function') warmTimer.unref();
185
+ } catch {
186
+ // 拿不到定时器就算了,只是第一次打开设置页慢一点。
187
+ }
188
+
165
189
  ctx.inject(['connection'], (c) => {
166
190
  const connection = c.get('connection');
167
191
  const register = typeof connection?.fetch?.register === 'function'
@@ -245,8 +269,17 @@ export function apply(ctx, config) {
245
269
  // 注册失败只是设置页那个开关不出现,预热仍按配置文件走。
246
270
  }
247
271
 
248
- const handler = async () => {
249
- const [installed, resolved] = await Promise.all([readInstalledVersion(), resolveLatest()]);
272
+ const handler = async (request) => {
273
+ let force = false;
274
+ try {
275
+ const text = typeof request?.text === 'function' ? await request.text() : '';
276
+ // 手动点「检查更新」要拿最新答案,不能被 10 分钟缓存挡住;自动检查仍走缓存。
277
+ if (text) force = JSON.parse(text)?.force === true;
278
+ } catch {
279
+ force = false;
280
+ }
281
+ const startedAt = Date.now();
282
+ const [installed, resolved] = await Promise.all([readInstalledVersion(), resolveLatest(force)]);
250
283
  const latest = resolved.latest;
251
284
  return Response.json(
252
285
  {
@@ -258,6 +291,7 @@ export function apply(ctx, config) {
258
291
  source: resolved.source,
259
292
  checkedAt: new Date(resolved.at === 0 ? Date.now() : resolved.at).toISOString(),
260
293
  error: latest === null ? resolved.error : '',
294
+ elapsedMs: Date.now() - startedAt,
261
295
  },
262
296
  },
263
297
  { headers: { 'cache-control': 'no-store' } },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-perf-boost",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "DSH 桌面端性能优化(纯客户端):长对话省渲染、窗口隐藏不排帧默认开,关背景模糊/减少动画/空闲降帧按需打开,设置页显示实测省下的主线程时间。Client-side rendering optimizations for DeepSeek Harness Desktop.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",