dsh-perf-boost 0.2.1 → 0.2.3

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,14 @@
4
4
 
5
5
  ## 更新详情
6
6
 
7
+ ### 0.2.3
8
+
9
+ - 新增了「后台回收内存」的开关:窗口隐藏超过 45 秒后摘掉视频源释放缓冲、清空资源计时、停掉观察器;设置页有「立即回收」按钮,也显示当前 JS 堆占用的功能
10
+ ### 0.2.2
11
+
12
+ - 优化了长对话省渲染的实现,改成一处分样式接管子项,新消息进来自动生效的效果
13
+ - 修复了流式输出时每来一条消息就扫一遍页面的问题,改成一分钟几十次的空闲巡检的问题
14
+
7
15
  ### 0.2.1
8
16
 
9
17
  - 新增了「大会话预热」的开关,设置页里点一下就能关的功能
package/README.md CHANGED
@@ -9,6 +9,15 @@ Client-side rendering optimizations for DSH Desktop.
9
9
 
10
10
  ## 更新详情
11
11
 
12
+ ### 0.2.3
13
+
14
+ - 新增了「后台回收内存」:窗口隐藏超过 45 秒后摘掉视频源释放缓冲、清空资源计时、停掉观察器;设置页可「立即回收」,并显示当前 JS 堆占用。
15
+
16
+ ### 0.2.2
17
+
18
+ - 优化了长对话省渲染的实现,改成一处分样式接管子项,新消息进来自动生效的效果
19
+ - 修复了流式输出时每来一条消息就扫一遍页面的问题,改成一分钟几十次的空闲巡检的问题
20
+
12
21
  ### 0.2.1
13
22
 
14
23
  - 新增了「大会话预热」的开关,设置页里点一下就能关的功能
@@ -35,7 +44,7 @@ Client-side rendering optimizations for DSH Desktop.
35
44
 
36
45
  | 开关 | 默认 | 影响视觉 | 作用 |
37
46
  | --- | --- | --- | --- |
38
- | 长对话省渲染 | 开 | 无 | 列表子项加 `content-visibility: auto`,占位高度取该列表实测平均子项高度;末尾 4 项与正在流式输出的消息不处理,超过 300px 的代码块同样跳过 |
47
+ | 长对话省渲染 | 开 | 无 | 给滚动列表容器挂一个 class,子项的 `content-visibility: auto` 由一条 CSS 规则接管(新消息自动生效,不用逐个打 inline style),占位高度取该列表实测平均子项高度;末尾 4 项与标了 `aria-busy` 的消息保持正常渲染,代码块单独按 300px 跳过 |
39
48
  | 窗口隐藏不排帧 | 开 | 无 | 页面不可见时 `schedule()` 直接返回;切回时有 `visibilitychange` 钩子恢复,每秒看门狗兜底 |
40
49
  | 空闲降帧 | 开 | 无 | 20 秒无输入将 rAF 驱动的动画降到 30fps,有输入立即恢复 |
41
50
  | 媒体懒加载 | 开 | 无 | 图片加 `loading=lazy`、`decoding=async`,视频加 `preload=metadata` |
Binary file
package/lib/client.js CHANGED
@@ -26,16 +26,32 @@ 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.3';
30
32
  // 更新公告:只用「新增了…的功能 / 修复了…的问题 / 优化了…的效果」三种句式。
31
33
  var RELEASE_NOTES = [
34
+ {
35
+ version: '0.2.3',
36
+ items: [
37
+ '新增了后台回收内存的开关,窗口隐藏一会儿就把视频缓冲和资源记录放掉的功能',
38
+ '新增了设置页里显示当前 JS 堆占用的效果',
39
+ ],
40
+ },
41
+ {
42
+ version: '0.2.2',
43
+ items: [
44
+ '优化了长对话省渲染的实现,改成一条 CSS 规则接管,新消息进来自动生效的效果',
45
+ '修复了流式输出时每来一条消息就扫一遍页面的问题,改成一分钟几十次的空闲巡检',
46
+ ],
47
+ },
32
48
  {
33
49
  version: '0.2.1',
34
50
  items: [
35
51
  '新增了大会话预热的开关,可以直接在设置里关掉的功能',
36
52
  '新增了浅色模式的配色,开关和边框在浅色主题下也看得清的效果',
37
- '优化了开关说明的文字,统一说成「可能会导致…」的效果',
38
- '新增了百分比改成最近 1 分钟窗口,长时间使用不会越用越低的功能',
53
+ '优化了开关说明的文字,统一说成「可能会导致…」的效果',
54
+ '新增了百分比改成最近 1 分钟窗口,长时间使用不会越用越低的功能',
39
55
  '优化了开关列表的顺序,默认开启的排在前面、默认关闭的放在后面的效果',
40
56
  ],
41
57
  },
@@ -66,6 +82,7 @@ window.__ModuleLoader__.load({
66
82
  hiddenTimers: true,
67
83
  mediaPause: true,
68
84
  listGate: true,
85
+ bgTrim: true,
69
86
  // 默认关:会改观感或动画
70
87
  noBlur: false,
71
88
  reduceMotion: false,
@@ -95,7 +112,7 @@ window.__ModuleLoader__.load({
95
112
  .catch(function () { return null; });
96
113
  }
97
114
 
98
- function checkUpdate() {
115
+ function checkUpdate(force) {
99
116
  if (window.__dpbUpdating) return;
100
117
  window.__dpbUpdating = true;
101
118
  update.state = 'checking';
@@ -107,7 +124,7 @@ window.__ModuleLoader__.load({
107
124
  method: 'POST',
108
125
  credentials: 'same-origin',
109
126
  headers: { 'content-type': 'application/json' },
110
- body: '{}',
127
+ body: force === true ? '{"force":true}' : '{}',
111
128
  signal: ctl ? ctl.signal : undefined,
112
129
  })
113
130
  .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)); })
@@ -249,6 +266,7 @@ window.__ModuleLoader__.load({
249
266
  timerMs: 0, timerRuns: 0, timerSkipped: 0,
250
267
  setMs: 0, setRuns: 0, setMerged: 0,
251
268
  lazyItems: 0, mediaPaused: 0, mediaLazy: 0,
269
+ trimRuns: 0, trimMedia: 0,
252
270
  };
253
271
  function metPer(x, n) { return n > 0 ? x / n : 0; }
254
272
  // rAF 那部分不算「省下」:页面隐藏时压住回调只是推迟,回到前台会一次性补跑,
@@ -544,82 +562,46 @@ window.__ModuleLoader__.load({
544
562
  return Math.max(48, Math.round(sum / cnt));
545
563
  }
546
564
 
547
- function isLive(el) {
565
+ // 容器上挂 class,子项交给 CSS。
566
+ // 逐项打 inline style 等于「每来一条消息就把整个列表摸一遍」,长会话里这笔开销
567
+ // 比省下来的渲染还贵;改成一处分样式后,新消息天然被覆盖,JS 只负责找容器。
568
+ function markContainer(el) {
569
+ if (el.children.length < 24) return;
548
570
  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; }
571
+ if (!el.classList.contains(CV_CLASS)) el.classList.add(CV_CLASS);
572
+ var h = avgChildHeight(el);
573
+ if (el.style.getPropertyValue(CV_VAR) !== h + 'px') el.style.setProperty(CV_VAR, h + 'px');
574
+ } catch (e) { void e; }
552
575
  }
553
576
 
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
- }
577
+ function unmark(el) {
578
+ try {
579
+ el.classList.remove(CV_CLASS);
580
+ el.style.removeProperty(CV_VAR);
581
+ } catch (e) { void e; }
577
582
  }
578
583
 
579
584
  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
- }
585
+ for (var i = 0; i < tagged.length; i++) unmark(tagged[i]);
590
586
  tagged = [];
591
587
  }
592
588
 
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
589
  function applyLazyList() {
599
590
  if (!cfg.lazyList) { clearAll(); return; }
600
591
  var targets = collectTargets();
601
- if (!targets.length) return;
602
592
  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);
593
+ if (targets.indexOf(tagged[i]) < 0 || !document.contains(tagged[i])) {
594
+ unmark(tagged[i]);
595
+ tagged.splice(i, 1);
596
+ }
604
597
  }
605
598
  for (var t = 0; t < targets.length; t++) {
606
599
  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; }
600
+ if (tagged.indexOf(el) < 0) tagged.push(el);
601
+ try { markContainer(el); } catch (e) { void e; }
613
602
  }
614
603
  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
- }
604
+ for (var m = 0; m < tagged.length; m++) n += Math.max(0, tagged[m].children.length - 4);
623
605
  MET.lazyItems = n;
624
606
  }
625
607
 
@@ -628,10 +610,10 @@ window.__ModuleLoader__.load({
628
610
  if (!cfg.lazyList) return '已关闭。';
629
611
  var total = 0, lists = 0, kids = 0;
630
612
  for (var i = 0; i < tagged.length; i++) {
631
- if (!document.contains(tagged[i].el)) continue;
613
+ if (!document.contains(tagged[i])) continue;
632
614
  lists++;
633
- total += tagged[i].items.length;
634
- kids += tagged[i].el.children.length;
615
+ total += Math.max(0, tagged[i].children.length - 4);
616
+ kids += tagged[i].children.length;
635
617
  }
636
618
  if (!lists) {
637
619
  var targets = [];
@@ -674,44 +656,20 @@ window.__ModuleLoader__.load({
674
656
  }
675
657
  }
676
658
 
659
+ // 只做「容器换没换」的巡检。子项由 CSS 自己跟上,所以不必盯整棵 DOM——
660
+ // 盯子树会在流式输出时被每一条新消息唤醒,那笔开销反而比省下来的大。
677
661
  function watchLazyList() {
678
- if (window.__dpbObserver || typeof MutationObserver !== 'function') return;
679
- window.__dpbObserver = true;
680
- var timer = null;
681
- var rescan = function () {
662
+ if (window.__dpbWatcher) return;
663
+ window.__dpbWatcher = true;
664
+ window.setInterval(function () {
665
+ if (document.hidden) return;
666
+ if (!cfg.lazyList && !cfg.mediaLazy && !cfg.mediaPause) return;
682
667
  idleRun(function () {
683
668
  try { applyLazyList(); } catch (e) { void e; }
684
669
  try { applyMediaLazy(); } catch (e) { void e; }
685
670
  try { applyViewportMedia(); } catch (e) { void e; }
686
671
  }, 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
- });
672
+ }, 2500);
715
673
  }
716
674
 
717
675
  // ————————————————— 媒体懒加载 —————————————————
@@ -967,6 +925,12 @@ window.__ModuleLoader__.load({
967
925
  '.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
926
  '.dpb-sw[data-on="1"] .dpb-knob{transform:translateX(17px);background:#fff}',
969
927
  '.dpb-quiet *, .dpb-quiet *::before, .dpb-quiet *::after{animation-play-state:paused!important}',
928
+ // 子项:滚出视口就不渲染;末尾 4 项和正在生成的那条保持正常渲染。
929
+ '.' + CV_CLASS + ' > *{content-visibility:auto;contain-intrinsic-size:auto var(' + CV_VAR + ',220px)}',
930
+ '.' + CV_CLASS + ' > :nth-last-child(-n+4){content-visibility:visible}',
931
+ '.' + CV_CLASS + ' > [aria-busy="true"]{content-visibility:visible}',
932
+ // 大代码块单独跳过:一条消息里有几千行 pre 时,救的是这一条。
933
+ '.' + CV_CLASS + ' pre{content-visibility:auto;contain-intrinsic-size:auto 300px;overflow-anchor:none}',
970
934
  '.dpb-notes{margin:12px 2px 0;font-size:12px;line-height:1.8;opacity:.78}',
971
935
  '.dpb-notes b{font-weight:600;opacity:1}',
972
936
  '.dpb-note{margin:10px 2px 0;font-size:12px;line-height:1.7;opacity:.72}',
@@ -1065,6 +1029,7 @@ window.__ModuleLoader__.load({
1065
1029
  MET.rafSkipped, MET.timerSkipped,
1066
1030
  g ? g.published : 0, g ? g.coalesced : 0,
1067
1031
  MET.lazyItems, MET.mediaLazy, MET.mediaPaused,
1032
+ MET.trimRuns, heapMb(),
1068
1033
  ].join('|');
1069
1034
  };
1070
1035
  var tick = function () {
@@ -1132,6 +1097,13 @@ window.__ModuleLoader__.load({
1132
1097
  warmupCall(v).then(function (real) { if (real !== null) setWarmupOn(real); });
1133
1098
  },
1134
1099
  })),
1100
+ React.createElement(Row, { label: '后台回收内存', hint: '窗口隐藏超过 45 秒后放掉视频缓冲、清空资源计时,观察器也停掉。可能会导致回到前台时视频要重新缓冲一下。默认开。' + lastTrimText() },
1101
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flex: '0 0 auto' } },
1102
+ React.createElement('button', {
1103
+ type: 'button', className: 'dpb-btn',
1104
+ onClick: function () { try { if (typeof window.__dpbTrim === 'function') window.__dpbTrim(); } catch (e) { void e; } bump(function (n) { return n + 1; }); },
1105
+ }, '立即回收'),
1106
+ React.createElement(Sw, { label: '后台回收内存', value: !!cfg.bgTrim, onChange: function (v) { setCfg({ bgTrim: v }); } }))),
1135
1107
  React.createElement(Row, { label: '关闭背景模糊', hint: '浮层从磨砂变纯色底,能看出来。默认关。' },
1136
1108
  React.createElement(Sw, { label: '关闭背景模糊', value: !!cfg.noBlur, onChange: function (v) { setCfg({ noBlur: v }); } })),
1137
1109
  React.createElement(Row, { label: '减少动画', hint: '关掉 CSS 动画和过渡,弹窗不再逐帧动。默认关。' },
@@ -1143,6 +1115,7 @@ window.__ModuleLoader__.load({
1143
1115
  React.createElement('div', { className: 'dpb-card' },
1144
1116
  React.createElement('div', { className: 'dpb-stat' },
1145
1117
  React.createElement('span', null, '版本 ', React.createElement('b', null, update.installed ? 'v' + update.installed : 'v' + VERSION)),
1118
+ React.createElement('span', null, '堆 ', React.createElement('b', null, heapMb() === null ? '—' : heapMb() + ' MB')),
1146
1119
  React.createElement('span', null, '最新 ', React.createElement('b', null,
1147
1120
  update.state === 'checking' ? '检查中…'
1148
1121
  : update.available ? 'v' + update.latest
@@ -1152,7 +1125,7 @@ window.__ModuleLoader__.load({
1152
1125
  : '—')),
1153
1126
  React.createElement('button', {
1154
1127
  type: 'button', className: 'dpb-btn',
1155
- onClick: function () { checkUpdate(); },
1128
+ onClick: function () { checkUpdate(true); },
1156
1129
  }, update.state === 'checking' ? '检查中' : '检查更新')),
1157
1130
  React.createElement('div', { className: 'dpb-notes' },
1158
1131
  React.createElement('b', null, '更新详情'),
@@ -1167,13 +1140,14 @@ window.__ModuleLoader__.load({
1167
1140
  else installRafThrottle();
1168
1141
  installTimerPause();
1169
1142
  applyStyle();
1143
+ onVisibilityForTrim();
1170
1144
  // 首屏别抢:等空闲再扫,扫描期间若在流式输出会往后推。
1171
1145
  idleRun(function () {
1172
1146
  try { applyLazyList(); } catch (e) { void e; }
1173
1147
  try { applyMediaLazy(); } catch (e) { void e; }
1174
1148
  try { applyViewportMedia(); } catch (e) { void e; }
1175
1149
  }, 600);
1176
- if (cfg.lazyList) watchLazyList();
1150
+ watchLazyList();
1177
1151
  if (cfg.showFps) startBadge();
1178
1152
  else {
1179
1153
  var el = document.getElementById(BADGE_ID);
@@ -1188,6 +1162,118 @@ window.__ModuleLoader__.load({
1188
1162
  else root.classList.remove('dpb-quiet');
1189
1163
  }
1190
1164
 
1165
+ // ————————————————— 后台回收内存 —————————————————
1166
+ // 隐藏超过 45 秒才动手:切走一下去看文件不动它,免得回来视频要重新缓冲。
1167
+ // 长期最小化时,把能放的全放掉:视频缓冲、资源计时、观察器、记录表。
1168
+ var BG_TRIM_AFTER_MS = 45000;
1169
+ var bgTrimTimer = 0;
1170
+
1171
+ function heapMb() {
1172
+ var m = null;
1173
+ try { m = performance.memory; } catch (e) { m = null; }
1174
+ if (!m || !m.usedJSHeapSize) return null;
1175
+ return Math.round(m.usedJSHeapSize / 1048576);
1176
+ }
1177
+
1178
+ function trimMedia() {
1179
+ var n = 0;
1180
+ var vids = document.querySelectorAll('video');
1181
+ for (var k = 0; k < vids.length; k++) {
1182
+ var v = vids[k];
1183
+ try {
1184
+ // 后台正在出声的不碰:用户可能在听。
1185
+ if (!v.paused && !v.muted && v.volume > 0) continue;
1186
+ if (v.getAttribute('data-dpb-src') !== null) continue;
1187
+ var src = v.getAttribute('src') || '';
1188
+ var kids = v.querySelectorAll('source');
1189
+ var sources = [];
1190
+ for (var j = 0; j < kids.length; j += 1) {
1191
+ sources.push({ src: kids[j].getAttribute('src') || '', type: kids[j].getAttribute('type') || '' });
1192
+ }
1193
+ v.setAttribute('data-dpb-src', src);
1194
+ v.setAttribute('data-dpb-pre', v.getAttribute('preload') || '');
1195
+ if (sources.length) v.setAttribute('data-dpb-sources', JSON.stringify(sources));
1196
+ v.setAttribute('preload', 'none');
1197
+ // 只把 src 摘掉再 load(),浏览器才真正丢弃已缓冲的媒体数据;
1198
+ // 留着 src 只是标记「别预载」,缓冲还在内存里(实测 buffered 不归零)。
1199
+ if (src || sources.length) {
1200
+ v.removeAttribute('src');
1201
+ for (var m = kids.length - 1; m >= 0; m -= 1) kids[m].remove();
1202
+ v.load();
1203
+ }
1204
+ n += 1;
1205
+ } catch (e) { void e; }
1206
+ }
1207
+ return n;
1208
+ }
1209
+
1210
+ function lastTrimText() {
1211
+ var t = window.__dpbLastTrim;
1212
+ if (!t) return '';
1213
+ var mins = Math.max(1, Math.round((Date.now() - t.at) / 60000));
1214
+ return ' 上次回收:' + mins + ' 分钟前,放掉 ' + t.freed + ' 个视频缓冲。';
1215
+ }
1216
+
1217
+ function trimMemory(why) {
1218
+ if (!cfg.bgTrim) return;
1219
+ var freed = 0;
1220
+ try { if (performance.clearResourceTimings) performance.clearResourceTimings(); } catch (e) { void e; }
1221
+ try { freed = trimMedia(); } catch (e) { void e; }
1222
+ try { if (mediaObserver) mediaObserver.disconnect(); } catch (e) { void e; }
1223
+ try { if (timerRecs.length > 200) timerRecs.splice(0, timerRecs.length - 200); } catch (e) { void e; }
1224
+ try { if (typeof window.gc === 'function') window.gc(); } catch (e) { void e; }
1225
+ MET.trimRuns += 1;
1226
+ MET.trimMedia += freed;
1227
+ try { window.__dpbLastTrim = { at: Date.now(), why: why, freed: freed, heap: heapMb() }; } catch (e) { void e; }
1228
+ }
1229
+
1230
+ // 设置页的「立即回收」与自检脚本都走这里。
1231
+ try { window.__dpbTrim = function () { trimMemory('manual'); return window.__dpbLastTrim || null; }; } catch (e) { void e; }
1232
+
1233
+ function restoreMediaPreload() {
1234
+ var list = document.querySelectorAll('[data-dpb-src]');
1235
+ for (var i = 0; i < list.length; i++) {
1236
+ try {
1237
+ var v = list[i];
1238
+ var src = v.getAttribute('data-dpb-src') || '';
1239
+ var pre = v.getAttribute('data-dpb-pre') || '';
1240
+ var srcs = null;
1241
+ try { srcs = JSON.parse(v.getAttribute('data-dpb-sources') || 'null'); } catch (e) { srcs = null; }
1242
+ if (src) v.setAttribute('src', src);
1243
+ if (srcs && srcs.length) {
1244
+ for (var j = 0; j < srcs.length; j += 1) {
1245
+ var el = document.createElement('source');
1246
+ if (srcs[j].src) el.setAttribute('src', srcs[j].src);
1247
+ if (srcs[j].type) el.setAttribute('type', srcs[j].type);
1248
+ v.appendChild(el);
1249
+ }
1250
+ }
1251
+ if (pre) v.setAttribute('preload', pre); else v.removeAttribute('preload');
1252
+ v.removeAttribute('data-dpb-src');
1253
+ v.removeAttribute('data-dpb-pre');
1254
+ v.removeAttribute('data-dpb-sources');
1255
+ // 把源接回去后重新 load 一次,浏览器才会重新按需取流。
1256
+ if (src || (srcs && srcs.length)) v.load();
1257
+ } catch (e) { void e; }
1258
+ }
1259
+ }
1260
+
1261
+ function onVisibilityForTrim() {
1262
+ if (bgTrimTimer) { window.clearTimeout(bgTrimTimer); bgTrimTimer = 0; }
1263
+ if (document.hidden) {
1264
+ if (!cfg.bgTrim) return;
1265
+ bgTrimTimer = window.setTimeout(function () {
1266
+ bgTrimTimer = 0;
1267
+ if (document.hidden) trimMemory('hidden');
1268
+ }, BG_TRIM_AFTER_MS);
1269
+ } else {
1270
+ try { restoreMediaPreload(); } catch (e) { void e; }
1271
+ idleRun(function () {
1272
+ try { applyViewportMedia(); } catch (e) { void e; }
1273
+ }, 300);
1274
+ }
1275
+ }
1276
+
1191
1277
  function mount() {
1192
1278
  applyAll();
1193
1279
  // 采样快照自己走:不依赖设置页开着,否则百分比只在打开设置后才更新。
@@ -1198,6 +1284,7 @@ window.__ModuleLoader__.load({
1198
1284
  }
1199
1285
  document.addEventListener('visibilitychange', flushTimers);
1200
1286
  document.addEventListener('visibilitychange', applyHiddenClass);
1287
+ document.addEventListener('visibilitychange', onVisibilityForTrim);
1201
1288
  applyHiddenClass();
1202
1289
  watchBusy();
1203
1290
 
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.3",
4
4
  "description": "DSH 桌面端性能优化(纯客户端):长对话省渲染、窗口隐藏不排帧默认开,关背景模糊/减少动画/空闲降帧按需打开,设置页显示实测省下的主线程时间。Client-side rendering optimizations for DeepSeek Harness Desktop.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",