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 +8 -0
- package/README.md +10 -1
- package/assets/icon-180.png +0 -0
- package/lib/client.js +187 -100
- package/lib/index.js +60 -26
- package/package.json +1 -1
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
|
-
| 长对话省渲染 | 开 | 无 |
|
|
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` |
|
package/assets/icon-180.png
CHANGED
|
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
|
|
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
|
-
|
|
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.
|
|
550
|
-
|
|
551
|
-
|
|
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
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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]
|
|
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
|
-
|
|
608
|
-
|
|
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]
|
|
613
|
+
if (!document.contains(tagged[i])) continue;
|
|
632
614
|
lists++;
|
|
633
|
-
total += tagged[i].
|
|
634
|
-
kids += tagged[i].
|
|
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.
|
|
679
|
-
window.
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
97
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|