dsh-perf-boost 0.0.0-stage → 0.2.1
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 +65 -0
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/assets/icon-180.png +0 -0
- package/assets/icon.svg +19 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1230 -0
- package/lib/host/warmup.js +146 -0
- package/lib/index.js +278 -0
- package/package.json +65 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,1230 @@
|
|
|
1
|
+
// lib/client.js —— DSH 性能优化(客户端侧)
|
|
2
|
+
//
|
|
3
|
+
// 设计前提:只做「看不出差别」的事。默认开的四项都没有视觉行为;
|
|
4
|
+
// 会改观感的三项(关模糊 / 减动画 / 帧率角标)默认关,由用户自己点。
|
|
5
|
+
//
|
|
6
|
+
// ① 长对话省渲染:消息列表与其他长滚动列表的子项标 content-visibility:auto,
|
|
7
|
+
// contain-intrinsic-size 按该列表实测平均高度给,滚动条不跳。
|
|
8
|
+
// ② 窗口隐藏不排帧:最小化/切走时不再往合成器送帧。
|
|
9
|
+
// ③ 空闲降帧:20 秒无输入把 JS 驱动的 rAF 降到 30fps(CSS 动画不受影响)。
|
|
10
|
+
// ④ 媒体懒加载:图片 loading=lazy + decoding=async,视频 preload=metadata。
|
|
11
|
+
// ⑤ 后台暂停定时器:页面不可见时,所有 setInterval 回调直接跳过(不计时、不重绘),
|
|
12
|
+
// 回到前台再各自补跳一次,逻辑状态不会漏掉。
|
|
13
|
+
// ⑥ 视口外暂停视频:静音视频滚出视口就暂停,滚回来自动续播(有声音的一律不碰)。
|
|
14
|
+
// ⑦ 会话列表节流:流式输出时列表常因「只有 token 计数变了」整树重渲染,
|
|
15
|
+
// 这里把这类只改身份的发布攒到每秒一次;可见内容真变了照旧立刻发布。
|
|
16
|
+
//
|
|
17
|
+
// 兼容性约定:只「添加」不「替换」——给元素加 inline style / 属性,
|
|
18
|
+
// 关闭时按记录逐项还原;不删节点、不改别人的 class、不动别的插件结构。
|
|
19
|
+
window.__ModuleLoader__.load({
|
|
20
|
+
id: 'dsh-perf-boost',
|
|
21
|
+
factory: function (require) {
|
|
22
|
+
var module = { exports: {} };
|
|
23
|
+
var exports = module.exports;
|
|
24
|
+
var React = require('react');
|
|
25
|
+
|
|
26
|
+
var NAME = 'dsh-perf-boost';
|
|
27
|
+
var STORE = 'dsh-perf-boost:cfg';
|
|
28
|
+
var STYLE_ID = 'dpb-style';
|
|
29
|
+
var VERSION = '0.2.1';
|
|
30
|
+
// 更新公告:只用「新增了…的功能 / 修复了…的问题 / 优化了…的效果」三种句式。
|
|
31
|
+
var RELEASE_NOTES = [
|
|
32
|
+
{
|
|
33
|
+
version: '0.2.1',
|
|
34
|
+
items: [
|
|
35
|
+
'新增了大会话预热的开关,可以直接在设置里关掉的功能',
|
|
36
|
+
'新增了浅色模式的配色,开关和边框在浅色主题下也看得清的效果',
|
|
37
|
+
'优化了开关说明的文字,统一说成「可能会导致…」的效果',
|
|
38
|
+
'新增了百分比改成最近 1 分钟窗口,长时间使用不会越用越低的功能',
|
|
39
|
+
'优化了开关列表的顺序,默认开启的排在前面、默认关闭的放在后面的效果',
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
version: '0.2.0',
|
|
44
|
+
items: [
|
|
45
|
+
'新增了设置页「更新详情」,能直接看到每个版本改了什么的功能',
|
|
46
|
+
'新增了滚动时让路,滚动过程中不再扫列表、停手后再补的效果',
|
|
47
|
+
'新增了长任务让路,超过 50 毫秒的卡顿之后等主线程缓过来再扫的效果',
|
|
48
|
+
'新增了视口外图片低优先级解码,把带宽留给看得见的图的效果',
|
|
49
|
+
'新增了窗口不可见时暂停 CSS 动画,合成器不再空转的效果',
|
|
50
|
+
'修复了大会话预热会去读正在写入的会话文件的问题',
|
|
51
|
+
'优化了列表观察范围,从盯整页收窄到只盯消息列表的效果',
|
|
52
|
+
'优化了每个开关的说明,把副作用写清楚的效果',
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
var BADGE_ID = 'dpb-fps-badge';
|
|
57
|
+
var IDLE_FPS = 30;
|
|
58
|
+
var IDLE_AFTER_MS = 20000;
|
|
59
|
+
|
|
60
|
+
var DEFAULTS = {
|
|
61
|
+
// 默认开:不影响观感
|
|
62
|
+
lazyList: true,
|
|
63
|
+
hiddenPause: true,
|
|
64
|
+
idleThrottle: true,
|
|
65
|
+
mediaLazy: true,
|
|
66
|
+
hiddenTimers: true,
|
|
67
|
+
mediaPause: true,
|
|
68
|
+
listGate: true,
|
|
69
|
+
// 默认关:会改观感或动画
|
|
70
|
+
noBlur: false,
|
|
71
|
+
reduceMotion: false,
|
|
72
|
+
showFps: false,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
var cfg = readCfg();
|
|
76
|
+
var subs = [];
|
|
77
|
+
|
|
78
|
+
// ————————————————— 版本检查 —————————————————
|
|
79
|
+
var update = { state: 'idle', installed: '', latest: null, available: false, source: '', checkedAt: null, error: '' };
|
|
80
|
+
var UPDATE_ROUTE = '/api/dsh-perf-boost/update-check';
|
|
81
|
+
var WARM_ROUTE = '/api/dsh-perf-boost/warmup';
|
|
82
|
+
|
|
83
|
+
// 大会话预热跑在宿主里,开关状态存在宿主侧的文件,这里只是替它传话。
|
|
84
|
+
// 不带 enabled = 查询;带 enabled = 改并持久化。失败一律当「不知道」,不动界面。
|
|
85
|
+
function warmupCall(enabled) {
|
|
86
|
+
var body = typeof enabled === 'boolean' ? JSON.stringify({ enabled: enabled }) : '{}';
|
|
87
|
+
return fetch(WARM_ROUTE, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
credentials: 'same-origin',
|
|
90
|
+
headers: { 'content-type': 'application/json' },
|
|
91
|
+
body: body,
|
|
92
|
+
})
|
|
93
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)); })
|
|
94
|
+
.then(function (j) { return j && j.value ? !!j.value.enabled : null; })
|
|
95
|
+
.catch(function () { return null; });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function checkUpdate() {
|
|
99
|
+
if (window.__dpbUpdating) return;
|
|
100
|
+
window.__dpbUpdating = true;
|
|
101
|
+
update.state = 'checking';
|
|
102
|
+
notify();
|
|
103
|
+
var ctl = null;
|
|
104
|
+
try { ctl = typeof AbortController === 'function' ? new AbortController() : null; } catch (e) { ctl = null; }
|
|
105
|
+
var guard = ctl ? window.setTimeout(function () { try { ctl.abort(); } catch (e) { void e; } }, 9000) : null;
|
|
106
|
+
fetch(UPDATE_ROUTE, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
credentials: 'same-origin',
|
|
109
|
+
headers: { 'content-type': 'application/json' },
|
|
110
|
+
body: '{}',
|
|
111
|
+
signal: ctl ? ctl.signal : undefined,
|
|
112
|
+
})
|
|
113
|
+
.then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)); })
|
|
114
|
+
.then(function (j) {
|
|
115
|
+
var v = j && j.value ? j.value : null;
|
|
116
|
+
if (!v) throw new Error('空响应');
|
|
117
|
+
update.installed = v.installed || '';
|
|
118
|
+
update.latest = v.latest === undefined ? null : v.latest;
|
|
119
|
+
update.available = !!v.updateAvailable;
|
|
120
|
+
update.source = v.source || '';
|
|
121
|
+
update.checkedAt = v.checkedAt || null;
|
|
122
|
+
update.error = v.error || '';
|
|
123
|
+
// error 有值就是没问到答案(未发布 / 网络不通),别当成「已是最新」。
|
|
124
|
+
update.state = update.error ? 'unknown' : 'done';
|
|
125
|
+
})
|
|
126
|
+
.catch(function (e) {
|
|
127
|
+
update.state = 'failed';
|
|
128
|
+
update.error = String(e && e.message ? e.message : e);
|
|
129
|
+
})
|
|
130
|
+
.then(function () {
|
|
131
|
+
if (guard) window.clearTimeout(guard);
|
|
132
|
+
window.__dpbUpdating = false;
|
|
133
|
+
notify();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readCfg() {
|
|
138
|
+
var out = {};
|
|
139
|
+
for (var k in DEFAULTS) out[k] = DEFAULTS[k];
|
|
140
|
+
try {
|
|
141
|
+
var raw = window.localStorage.getItem(STORE);
|
|
142
|
+
if (raw) {
|
|
143
|
+
var j = JSON.parse(raw);
|
|
144
|
+
for (var k2 in DEFAULTS) {
|
|
145
|
+
if (j && typeof j[k2] === typeof DEFAULTS[k2]) out[k2] = j[k2];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (e) { void e; }
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function writeCfg() {
|
|
153
|
+
try { window.localStorage.setItem(STORE, JSON.stringify(cfg)); } catch (e) { void e; }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 上次跑出来的实测结果:刚启动时样本不够,就先拿这个顶上,别显示「—」。
|
|
157
|
+
var LAST_KEY = 'dsh-perf-boost:last';
|
|
158
|
+
function readLast() {
|
|
159
|
+
try {
|
|
160
|
+
var raw = window.localStorage.getItem(LAST_KEY);
|
|
161
|
+
if (!raw) return null;
|
|
162
|
+
var v = JSON.parse(raw);
|
|
163
|
+
if (!v || typeof v.pct !== 'number' || !isFinite(v.pct)) return null;
|
|
164
|
+
if (typeof v.at !== 'number' || Date.now() - v.at > 30 * 24 * 3600 * 1000) return null;
|
|
165
|
+
return v;
|
|
166
|
+
} catch (e) { return null; }
|
|
167
|
+
}
|
|
168
|
+
// 页面端口每次启动都变,localStorage 留不住,所以同时存一份到宿主文件里。
|
|
169
|
+
var remoteLast = null;
|
|
170
|
+
var lastRemoteAt = 0;
|
|
171
|
+
var LAST_ROUTE = '/api/dsh-perf-boost/last-run';
|
|
172
|
+
|
|
173
|
+
function loadLastRun() {
|
|
174
|
+
try {
|
|
175
|
+
fetch(LAST_ROUTE, {
|
|
176
|
+
method: 'POST', credentials: 'same-origin',
|
|
177
|
+
headers: { 'content-type': 'application/json' }, body: '{}',
|
|
178
|
+
}).then(function (r) { return r.ok ? r.json() : null; })
|
|
179
|
+
.then(function (j) {
|
|
180
|
+
if (j && j.value && typeof j.value.pct === 'number') {
|
|
181
|
+
remoteLast = j.value;
|
|
182
|
+
notify();
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
.catch(function () { /* 宿主没提供就只用本地数据 */ });
|
|
186
|
+
} catch (e) { void e; }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function saveLastRemote(record) {
|
|
190
|
+
try {
|
|
191
|
+
fetch(LAST_ROUTE, {
|
|
192
|
+
method: 'POST', credentials: 'same-origin',
|
|
193
|
+
headers: { 'content-type': 'application/json' }, body: JSON.stringify(record),
|
|
194
|
+
}).catch(function () { /* 路由还没注册(未重启)时就是 404,忽略 */ });
|
|
195
|
+
} catch (e) { void e; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function saveLast(pct, saved, busy) {
|
|
199
|
+
var at = Date.now();
|
|
200
|
+
var record = { pct: pct, saved: Math.round(saved), busy: Math.round(busy), at: at };
|
|
201
|
+
try {
|
|
202
|
+
var prev = readLast();
|
|
203
|
+
if (!prev || prev.pct !== pct || Math.abs((prev.saved || 0) - saved) >= 5000) {
|
|
204
|
+
window.localStorage.setItem(LAST_KEY, JSON.stringify(record));
|
|
205
|
+
}
|
|
206
|
+
} catch (e) { void e; }
|
|
207
|
+
// 宿主文件最多一分钟写一次,省得每 5 秒一次磁盘写。
|
|
208
|
+
if (at - lastRemoteAt < 60000) return;
|
|
209
|
+
lastRemoteAt = at;
|
|
210
|
+
saveLastRemote(record);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function notify() {
|
|
214
|
+
for (var i = 0; i < subs.length; i++) { try { subs[i](); } catch (e) { void e; } }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function setCfg(patch) {
|
|
218
|
+
var dirty = false;
|
|
219
|
+
var touchedGate = false;
|
|
220
|
+
for (var k in patch) {
|
|
221
|
+
if (cfg[k] !== patch[k]) {
|
|
222
|
+
cfg[k] = patch[k];
|
|
223
|
+
dirty = true;
|
|
224
|
+
if (k === 'listGate') touchedGate = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!dirty) return;
|
|
228
|
+
writeCfg();
|
|
229
|
+
applyAll();
|
|
230
|
+
if (touchedGate) {
|
|
231
|
+
if (cfg.listGate) { applyListGate(); }
|
|
232
|
+
else if (gate) { gate.dispose(); gate = null; }
|
|
233
|
+
}
|
|
234
|
+
notify();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function subscribe(fn) {
|
|
238
|
+
subs.push(fn);
|
|
239
|
+
return function () { var i = subs.indexOf(fn); if (i >= 0) subs.splice(i, 1); };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ————————————————— 真实开销计量 —————————————————
|
|
243
|
+
// 不摆预设权重:每次「我们经手的」回调都实测耗时,攒起「实际跑了多少」与
|
|
244
|
+
// 「拦下多少」,百分比就是这两堆真实毫秒算出来的比值。样本不够就返回 null,
|
|
245
|
+
// 让人看到「—」而不是一个编出来的数。
|
|
246
|
+
var MET = {
|
|
247
|
+
at: Date.now(),
|
|
248
|
+
rafMs: 0, rafRuns: 0, rafSkipped: 0, rafIdle: 0,
|
|
249
|
+
timerMs: 0, timerRuns: 0, timerSkipped: 0,
|
|
250
|
+
setMs: 0, setRuns: 0, setMerged: 0,
|
|
251
|
+
lazyItems: 0, mediaPaused: 0, mediaLazy: 0,
|
|
252
|
+
};
|
|
253
|
+
function metPer(x, n) { return n > 0 ? x / n : 0; }
|
|
254
|
+
// rAF 那部分不算「省下」:页面隐藏时压住回调只是推迟,回到前台会一次性补跑,
|
|
255
|
+
// 毫秒照样要花。真正不再发生的是被跳过的定时器回调和合并掉的发布。
|
|
256
|
+
function metSavedMs() {
|
|
257
|
+
return MET.timerSkipped * metPer(MET.timerMs, MET.timerRuns)
|
|
258
|
+
+ MET.setMerged * metPer(MET.setMs, MET.setRuns)
|
|
259
|
+
+ MET.rafIdle * metPer(MET.rafMs, MET.rafRuns);
|
|
260
|
+
}
|
|
261
|
+
// 百分比按「最近 60 秒」算:累计值在活跃一会儿之后一定会被摊薄,
|
|
262
|
+
// 而窗口值只反映当下——闲着时降帧省下的和实际跑的都在这 60 秒里比。
|
|
263
|
+
var SNAP = []; // 每秒一条 { t, s, b } 累计快照
|
|
264
|
+
var SNAP_MS = 60000;
|
|
265
|
+
function snapNow() {
|
|
266
|
+
var t = Date.now();
|
|
267
|
+
SNAP.push({ t: t, s: metSavedMs(), b: MET.rafMs + MET.timerMs + MET.setMs });
|
|
268
|
+
while (SNAP.length > 2 && t - SNAP[0].t > SNAP_MS) SNAP.shift();
|
|
269
|
+
}
|
|
270
|
+
function metPercent() {
|
|
271
|
+
var saved = metSavedMs();
|
|
272
|
+
var busy = MET.rafMs + MET.timerMs + MET.setMs;
|
|
273
|
+
if (saved + busy < 120) return null; // 样本太少就不下结论
|
|
274
|
+
if (SNAP.length >= 2) {
|
|
275
|
+
var first = SNAP[0];
|
|
276
|
+
var ds = saved - first.s;
|
|
277
|
+
var db = busy - first.b;
|
|
278
|
+
if (ds + db >= 120 && ds >= 0 && db >= 0) {
|
|
279
|
+
return Math.min(99, Math.round((ds / (ds + db)) * 100));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return Math.min(99, Math.round((saved / (saved + busy)) * 100));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ————————————————— 空闲检测 —————————————————
|
|
286
|
+
var lastInput = Date.now();
|
|
287
|
+
var lastBusyAt = 0; // 最近一次滚动 / 长任务的时间点,用来给扫描让路
|
|
288
|
+
function markInput() { lastInput = Date.now(); }
|
|
289
|
+
function idleNow() { return !!cfg.idleThrottle && Date.now() - lastInput > IDLE_AFTER_MS; }
|
|
290
|
+
function effectiveCap() { return idleNow() ? IDLE_FPS : 0; }
|
|
291
|
+
function markBusy() { lastBusyAt = Date.now(); }
|
|
292
|
+
function watchBusy() {
|
|
293
|
+
// 滚动时别扫:滚动本身就在吃主线程。
|
|
294
|
+
window.addEventListener('scroll', markBusy, { passive: true, capture: true });
|
|
295
|
+
// 长任务(>50ms)之后也让一让,等主线程缓过来再扫。
|
|
296
|
+
if (typeof PerformanceObserver !== 'function') return;
|
|
297
|
+
try {
|
|
298
|
+
var po = new PerformanceObserver(function (list) {
|
|
299
|
+
if (list && list.getEntries && list.getEntries().length) markBusy();
|
|
300
|
+
});
|
|
301
|
+
po.observe({ entryTypes: ['longtask'] });
|
|
302
|
+
window.__dpbLongTask = po;
|
|
303
|
+
} catch (e) { void e; }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ————————————————— rAF 接管 —————————————————
|
|
307
|
+
var nativeRAF = window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : null;
|
|
308
|
+
var nativeCAF = window.cancelAnimationFrame ? window.cancelAnimationFrame.bind(window) : function () {};
|
|
309
|
+
var queue = [];
|
|
310
|
+
var pending = false;
|
|
311
|
+
var nextId = 1e9; // 避开接管前发出去的原生 id(那些都很小),否则 cancel 会取消错对象
|
|
312
|
+
var live = new Map();
|
|
313
|
+
var lastFrameTs = 0;
|
|
314
|
+
|
|
315
|
+
function runFrame(ts) {
|
|
316
|
+
pending = false;
|
|
317
|
+
lastFrameTs = ts;
|
|
318
|
+
var batch = queue;
|
|
319
|
+
queue = [];
|
|
320
|
+
for (var i = 0; i < batch.length; i++) {
|
|
321
|
+
var entry = batch[i];
|
|
322
|
+
if (!entry || entry.cancelled) continue;
|
|
323
|
+
live.delete(entry.id);
|
|
324
|
+
var t0 = performance.now();
|
|
325
|
+
try { entry.cb(ts); } catch (e) { /* 单个回调出错不影响整帧 */ }
|
|
326
|
+
MET.rafMs += performance.now() - t0;
|
|
327
|
+
MET.rafRuns++;
|
|
328
|
+
}
|
|
329
|
+
if (queue.length) schedule();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function schedule() {
|
|
333
|
+
if (!nativeRAF || pending) return;
|
|
334
|
+
if (cfg.hiddenPause && document.hidden) {
|
|
335
|
+
// 浏览器本来就不在隐藏时送帧;跳过计数在注册那一侧按次累加,这里不重复算。
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
pending = true;
|
|
339
|
+
var cap = effectiveCap();
|
|
340
|
+
if (!cap) { nativeRAF(runFrame); return; }
|
|
341
|
+
var waitMs = (1000 / cap) - (performance.now() - lastFrameTs);
|
|
342
|
+
if (waitMs <= 2) { nativeRAF(runFrame); return; }
|
|
343
|
+
MET.rafIdle++; // 空闲降帧压住的一帧:这一轮的动画回调确实没跑
|
|
344
|
+
window.setTimeout(function () { if (nativeRAF) nativeRAF(runFrame); }, waitMs);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 从隐藏切回前台时必须重启队列:schedule() 在 document.hidden 时直接返回,
|
|
348
|
+
// 若此刻队列里还压着回调,浏览器恢复出帧后没人再调 schedule(),回调链就永久停住
|
|
349
|
+
//(表现就是「动画突然不动了」)。所以补一个恢复钩子 + 每秒看门狗兜底。
|
|
350
|
+
function resumeFrames() {
|
|
351
|
+
if (!nativeRAF || document.hidden) return;
|
|
352
|
+
if (pending || !queue.length) return;
|
|
353
|
+
schedule();
|
|
354
|
+
}
|
|
355
|
+
document.addEventListener('visibilitychange', resumeFrames);
|
|
356
|
+
|
|
357
|
+
function startWatchdog() {
|
|
358
|
+
if (window.__dpbWatchdog) return;
|
|
359
|
+
window.__dpbWatchdog = true;
|
|
360
|
+
window.__dpbWatchdogTimer = window.setInterval(function () {
|
|
361
|
+
if (!nativeRAF || document.hidden || pending) return;
|
|
362
|
+
if (queue.length) schedule();
|
|
363
|
+
}, 1000);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ————————————————— 后台暂停定时器 —————————————————
|
|
367
|
+
// 页面不可见时,setInterval 的回调照样一秒一次被调用:插件普遍拿它轮询状态、
|
|
368
|
+
// 走倒计时、刷角标,看不见的时候这些活纯属白烧 CPU。
|
|
369
|
+
// 这里只做一件事:不可见就跳过回调本身(定时器不销毁、周期不重置),
|
|
370
|
+
// 回到前台给每个「漏跳过」的定时器补跳一次,避免状态机原地卡住。
|
|
371
|
+
var timerRecs = [];
|
|
372
|
+
var nativeSetInterval = window.setInterval ? window.setInterval.bind(window) : null;
|
|
373
|
+
var nativeClearInterval = window.clearInterval ? window.clearInterval.bind(window) : null;
|
|
374
|
+
|
|
375
|
+
function installTimerPause() {
|
|
376
|
+
if (!nativeSetInterval || window.__dpbTimers) return;
|
|
377
|
+
if (!cfg.hiddenTimers) return;
|
|
378
|
+
window.__dpbTimers = true;
|
|
379
|
+
|
|
380
|
+
window.setInterval = function (fn, ms) {
|
|
381
|
+
if (typeof fn !== 'function') return nativeSetInterval(fn, ms);
|
|
382
|
+
var rec = { id: 0, fn: fn, skips: 0, self: this };
|
|
383
|
+
var wrapped = function () {
|
|
384
|
+
// 开关随时可关:每次回调都重新判断,关掉立刻恢复原始行为。
|
|
385
|
+
if (cfg.hiddenTimers && document.hidden) { rec.skips++; MET.timerSkipped++; return undefined; }
|
|
386
|
+
rec.skips = 0;
|
|
387
|
+
var t0 = performance.now();
|
|
388
|
+
try {
|
|
389
|
+
return fn.apply(rec.self || window, arguments);
|
|
390
|
+
} finally {
|
|
391
|
+
MET.timerMs += performance.now() - t0;
|
|
392
|
+
MET.timerRuns++;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
rec.id = nativeSetInterval(wrapped, ms);
|
|
396
|
+
timerRecs.push(rec);
|
|
397
|
+
if (timerRecs.length > 400) timerRecs.splice(0, timerRecs.length - 400);
|
|
398
|
+
return rec.id;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
window.clearInterval = function (id) {
|
|
402
|
+
for (var i = 0; i < timerRecs.length; i++) {
|
|
403
|
+
if (timerRecs[i].id === id) { timerRecs.splice(i, 1); break; }
|
|
404
|
+
}
|
|
405
|
+
return nativeClearInterval(id);
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function flushTimers() {
|
|
410
|
+
if (document.hidden) return;
|
|
411
|
+
for (var i = 0; i < timerRecs.length; i++) {
|
|
412
|
+
var rec = timerRecs[i];
|
|
413
|
+
if (!rec.skips) continue;
|
|
414
|
+
rec.skips = 0;
|
|
415
|
+
try { rec.fn.call(rec.self || window, performance.now()); } catch (e) { void e; }
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 两个开关都关时完全不接管,帧调度与原生一致。
|
|
420
|
+
function installRafThrottle() {
|
|
421
|
+
if (!nativeRAF || window.__dpbRaf) return;
|
|
422
|
+
if (!cfg.hiddenPause && !cfg.idleThrottle) return;
|
|
423
|
+
window.__dpbRaf = true;
|
|
424
|
+
startWatchdog();
|
|
425
|
+
window.requestAnimationFrame = function (cb) {
|
|
426
|
+
var id = nextId++;
|
|
427
|
+
var entry = { id: id, cb: cb, cancelled: false };
|
|
428
|
+
if (cfg.hiddenPause && document.hidden) {
|
|
429
|
+
// 隐藏期间最多留 200 个待跑回调;再多说明有东西在定时器里狂注册 rAF,
|
|
430
|
+
// 不设上限的话最小化一晚上能攒出几十万个闭包,回前台一次跑完直接卡死。
|
|
431
|
+
MET.rafSkipped++;
|
|
432
|
+
if (queue.length >= 200) {
|
|
433
|
+
var dropped = queue.shift();
|
|
434
|
+
if (dropped) live.delete(dropped.id);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
live.set(id, entry);
|
|
438
|
+
queue.push(entry);
|
|
439
|
+
schedule();
|
|
440
|
+
return id;
|
|
441
|
+
};
|
|
442
|
+
window.cancelAnimationFrame = function (id) {
|
|
443
|
+
var entry = live.get(id);
|
|
444
|
+
if (entry) { entry.cancelled = true; live.delete(id); return; }
|
|
445
|
+
try { nativeCAF(id); } catch (e) { void e; }
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function uninstallRafThrottle() {
|
|
450
|
+
if (!window.__dpbRaf) return;
|
|
451
|
+
window.__dpbRaf = false;
|
|
452
|
+
if (window.__dpbWatchdogTimer) {
|
|
453
|
+
window.clearInterval(window.__dpbWatchdogTimer);
|
|
454
|
+
window.__dpbWatchdogTimer = null;
|
|
455
|
+
}
|
|
456
|
+
window.__dpbWatchdog = false;
|
|
457
|
+
window.requestAnimationFrame = nativeRAF;
|
|
458
|
+
window.cancelAnimationFrame = nativeCAF;
|
|
459
|
+
queue = [];
|
|
460
|
+
live.clear();
|
|
461
|
+
pending = false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ————————————————— 长列表省渲染 —————————————————
|
|
465
|
+
// 目标容器:先用 DSH 自己的结构(消息列表 .wSkVaW_viewArea / conversation.session 槽位),
|
|
466
|
+
// 拿不到再退回启发式(可滚动、子项 >= 12)。每个容器按自己的实测平均高度写
|
|
467
|
+
// contain-intrinsic-size,避免预留高度和真实高度不一致导致滚动条跳动。
|
|
468
|
+
var PREFERRED = [
|
|
469
|
+
'.wSkVaW_scrollBody',
|
|
470
|
+
'.wSkVaW_viewArea',
|
|
471
|
+
'[data-slot="conversation.session"]',
|
|
472
|
+
];
|
|
473
|
+
var tagged = [];
|
|
474
|
+
|
|
475
|
+
function isScrollable(el) {
|
|
476
|
+
if (el.scrollHeight <= el.clientHeight + 40) return false;
|
|
477
|
+
var cs;
|
|
478
|
+
try { cs = window.getComputedStyle(el); } catch (e) { return false; }
|
|
479
|
+
return cs.overflowY === 'auto' || cs.overflowY === 'scroll' || cs.overflowY === 'overlay';
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 找出「消息列表」这个容器本身:从滚动区往下钻,取子项最多的一层。
|
|
483
|
+
// 不同 DSH 版本层级不一样(scrollBody > viewArea > row > …),
|
|
484
|
+
// 所以不写死某一路径,而是按结构找「谁直接挂着 24 个以上的兄弟」。
|
|
485
|
+
function pickListIn(root) {
|
|
486
|
+
if (!root) return null;
|
|
487
|
+
var best = null, bestKids = 0;
|
|
488
|
+
var nodes = [root].concat(Array.prototype.slice.call(root.querySelectorAll('div,ul,ol,section,main')));
|
|
489
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
490
|
+
var n = nodes[i];
|
|
491
|
+
if (!n.children) continue;
|
|
492
|
+
var k = n.children.length;
|
|
493
|
+
if (k < 24 || k <= bestKids) continue;
|
|
494
|
+
best = n;
|
|
495
|
+
bestKids = k;
|
|
496
|
+
}
|
|
497
|
+
return best;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function scrollRoots() {
|
|
501
|
+
var out = [];
|
|
502
|
+
var i, el;
|
|
503
|
+
for (i = 0; i < PREFERRED.length; i++) {
|
|
504
|
+
var found;
|
|
505
|
+
try { found = document.querySelectorAll(PREFERRED[i]); } catch (e) { continue; }
|
|
506
|
+
for (var j = 0; j < found.length; j++) {
|
|
507
|
+
el = found[j];
|
|
508
|
+
if (el && out.indexOf(el) < 0) out.push(el);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (!out.length) {
|
|
512
|
+
// 兜底:可滚动且子项够多
|
|
513
|
+
var all = document.querySelectorAll('div,ul,ol,section,main');
|
|
514
|
+
for (i = 0; i < all.length; i++) {
|
|
515
|
+
el = all[i];
|
|
516
|
+
if (!el.children || el.children.length < 12) continue;
|
|
517
|
+
if (!isScrollable(el)) continue;
|
|
518
|
+
out.push(el);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function collectTargets() {
|
|
526
|
+
var roots = scrollRoots();
|
|
527
|
+
var out = [];
|
|
528
|
+
for (var i = 0; i < roots.length; i++) {
|
|
529
|
+
var list = pickListIn(roots[i]);
|
|
530
|
+
if (list && out.indexOf(list) < 0) out.push(list);
|
|
531
|
+
}
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function avgChildHeight(el) {
|
|
536
|
+
var n = Math.min(el.children.length, 30);
|
|
537
|
+
var sum = 0, cnt = 0;
|
|
538
|
+
for (var i = 0; i < n; i++) {
|
|
539
|
+
var h = 0;
|
|
540
|
+
try { h = el.children[i].getBoundingClientRect().height; } catch (e) { h = 0; }
|
|
541
|
+
if (h > 0) { sum += h; cnt++; }
|
|
542
|
+
}
|
|
543
|
+
if (!cnt) return 220;
|
|
544
|
+
return Math.max(48, Math.round(sum / cnt));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function isLive(el) {
|
|
548
|
+
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; }
|
|
552
|
+
}
|
|
553
|
+
|
|
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
|
+
}
|
|
578
|
+
|
|
579
|
+
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
|
+
}
|
|
590
|
+
tagged = [];
|
|
591
|
+
}
|
|
592
|
+
|
|
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
|
+
function applyLazyList() {
|
|
599
|
+
if (!cfg.lazyList) { clearAll(); return; }
|
|
600
|
+
var targets = collectTargets();
|
|
601
|
+
if (!targets.length) return;
|
|
602
|
+
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);
|
|
604
|
+
}
|
|
605
|
+
for (var t = 0; t < targets.length; t++) {
|
|
606
|
+
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; }
|
|
613
|
+
}
|
|
614
|
+
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
|
+
}
|
|
623
|
+
MET.lazyItems = n;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 给设置页用的一句话状态:开了没有、找到几个列表、跳过多少项。
|
|
627
|
+
function lazyStatus() {
|
|
628
|
+
if (!cfg.lazyList) return '已关闭。';
|
|
629
|
+
var total = 0, lists = 0, kids = 0;
|
|
630
|
+
for (var i = 0; i < tagged.length; i++) {
|
|
631
|
+
if (!document.contains(tagged[i].el)) continue;
|
|
632
|
+
lists++;
|
|
633
|
+
total += tagged[i].items.length;
|
|
634
|
+
kids += tagged[i].el.children.length;
|
|
635
|
+
}
|
|
636
|
+
if (!lists) {
|
|
637
|
+
var targets = [];
|
|
638
|
+
try { targets = collectTargets(); } catch (e) { targets = []; }
|
|
639
|
+
if (!targets.length) return '开着,但当前页面没有够长的消息列表(聊天列表 ≥ 24 条才动手)。';
|
|
640
|
+
kids = targets[0].children.length;
|
|
641
|
+
return '开着,找到列表(' + kids + ' 项),不足 24 项或刚打开还没应用,滚一下即生效。';
|
|
642
|
+
}
|
|
643
|
+
if (!total) return '开着,列表 ' + kids + ' 项,暂时没有需要跳过的(末尾 4 项和正在生成的会保留)。';
|
|
644
|
+
return '已生效:' + lists + ' 个列表,跳过 ' + total + ' 项渲染(共 ' + kids + ' 项)。';
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ————————————————— 扫描调度 —————————————————
|
|
648
|
+
// 扫描要遍历 DOM,赶在首屏渲染或流式输出时跑就是跟用户抢帧。
|
|
649
|
+
// 规则:只在空闲时跑;正在流式输出就往后推,最多推 6 次(约 9 秒)再硬跑。
|
|
650
|
+
function isStreaming() {
|
|
651
|
+
try { return !!document.querySelector('[aria-busy="true"],[data-streaming="true"]'); }
|
|
652
|
+
catch (e) { return false; }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function idleRun(fn, delay, attempt) {
|
|
656
|
+
var tries = attempt || 0;
|
|
657
|
+
var run = function () {
|
|
658
|
+
if (document.hidden) return;
|
|
659
|
+
// 正在滚动 / 刚跑过长任务时别插队:这时候主线程本来就紧,再扫一遍只是雪上加霜。
|
|
660
|
+
if (Date.now() - lastBusyAt < 250) {
|
|
661
|
+
if (tries < 8) window.setTimeout(function () { idleRun(fn, delay, tries + 1); }, 400);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (isStreaming() && tries < 6) {
|
|
665
|
+
window.setTimeout(function () { idleRun(fn, delay, tries + 1); }, 1500);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
try { fn(); } catch (e) { void e; }
|
|
669
|
+
};
|
|
670
|
+
if (typeof window.requestIdleCallback === 'function') {
|
|
671
|
+
window.requestIdleCallback(run, { timeout: (delay || 800) + 1200 });
|
|
672
|
+
} else {
|
|
673
|
+
window.setTimeout(run, delay || 800);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function watchLazyList() {
|
|
678
|
+
if (window.__dpbObserver || typeof MutationObserver !== 'function') return;
|
|
679
|
+
window.__dpbObserver = true;
|
|
680
|
+
var timer = null;
|
|
681
|
+
var rescan = function () {
|
|
682
|
+
idleRun(function () {
|
|
683
|
+
try { applyLazyList(); } catch (e) { void e; }
|
|
684
|
+
try { applyMediaLazy(); } catch (e) { void e; }
|
|
685
|
+
try { applyViewportMedia(); } catch (e) { void e; }
|
|
686
|
+
}, 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
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ————————————————— 媒体懒加载 —————————————————
|
|
718
|
+
// 只补属性、不动结构:图片快进视口才解码,视频只取元数据。
|
|
719
|
+
function clearMedia() {
|
|
720
|
+
var list = document.querySelectorAll('[data-dpb-lazy]');
|
|
721
|
+
for (var i = 0; i < list.length; i++) {
|
|
722
|
+
var el = list[i];
|
|
723
|
+
try {
|
|
724
|
+
if (el.getAttribute('loading') === 'lazy') el.removeAttribute('loading');
|
|
725
|
+
if (el.getAttribute('decoding') === 'async') el.removeAttribute('decoding');
|
|
726
|
+
if (el.tagName === 'VIDEO' && el.getAttribute('preload') === 'metadata') el.removeAttribute('preload');
|
|
727
|
+
el.removeAttribute('data-dpb-lazy');
|
|
728
|
+
} catch (e) { void e; }
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function inViewport(el) {
|
|
733
|
+
var r = null;
|
|
734
|
+
try { r = el.getBoundingClientRect(); } catch (e) { return true; }
|
|
735
|
+
if (!r) return true;
|
|
736
|
+
var h = window.innerHeight || 0;
|
|
737
|
+
var w = window.innerWidth || 0;
|
|
738
|
+
return r.bottom > -400 && r.top < h + 400 && r.right > -400 && r.left < w + 400;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function applyMediaLazy() {
|
|
742
|
+
if (!cfg.mediaLazy) { clearMedia(); return; }
|
|
743
|
+
var list = document.querySelectorAll('img:not([data-dpb-lazy]),video:not([data-dpb-lazy]),iframe:not([data-dpb-lazy])');
|
|
744
|
+
for (var i = 0; i < list.length; i++) {
|
|
745
|
+
var el = list[i];
|
|
746
|
+
try {
|
|
747
|
+
if (el.tagName === 'IMG') {
|
|
748
|
+
if (!el.getAttribute('loading')) el.setAttribute('loading', 'lazy');
|
|
749
|
+
if (!el.getAttribute('decoding')) el.setAttribute('decoding', 'async');
|
|
750
|
+
if (!el.getAttribute('fetchpriority') && !inViewport(el)) el.setAttribute('fetchpriority', 'low');
|
|
751
|
+
} else if (el.tagName === 'VIDEO') {
|
|
752
|
+
if (!el.getAttribute('preload')) el.setAttribute('preload', 'metadata');
|
|
753
|
+
} else if (!el.getAttribute('loading')) {
|
|
754
|
+
el.setAttribute('loading', 'lazy');
|
|
755
|
+
}
|
|
756
|
+
el.setAttribute('data-dpb-lazy', '1');
|
|
757
|
+
MET.mediaLazy++;
|
|
758
|
+
} catch (e) { void e; }
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ————————————————— 视口外暂停视频 —————————————————
|
|
763
|
+
// 只在「静音」视频上动手:滚出视口暂停解码,滚回来自动续播。
|
|
764
|
+
// 有声音的一律不碰(突然没声比掉帧更让人火)。
|
|
765
|
+
var mediaObserver = null;
|
|
766
|
+
var pausedByUs = null;
|
|
767
|
+
|
|
768
|
+
function clearViewportMedia() {
|
|
769
|
+
if (mediaObserver) {
|
|
770
|
+
try { mediaObserver.disconnect(); } catch (e) { void e; }
|
|
771
|
+
mediaObserver = null;
|
|
772
|
+
}
|
|
773
|
+
var vids = document.querySelectorAll('[data-dpb-vp]');
|
|
774
|
+
for (var i = 0; i < vids.length; i++) {
|
|
775
|
+
var v = vids[i];
|
|
776
|
+
try {
|
|
777
|
+
v.removeAttribute('data-dpb-vp');
|
|
778
|
+
if (pausedByUs && pausedByUs.has(v)) {
|
|
779
|
+
pausedByUs.delete(v);
|
|
780
|
+
var p = v.play();
|
|
781
|
+
if (p && p.catch) p.catch(function () {});
|
|
782
|
+
}
|
|
783
|
+
} catch (e) { void e; }
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function applyViewportMedia() {
|
|
788
|
+
if (!cfg.mediaPause) { clearViewportMedia(); return; }
|
|
789
|
+
if (typeof IntersectionObserver !== 'function' || typeof WeakSet !== 'function') return;
|
|
790
|
+
if (!pausedByUs) pausedByUs = new WeakSet();
|
|
791
|
+
var vids = document.querySelectorAll('video:not([data-dpb-vp])');
|
|
792
|
+
if (!vids.length) return;
|
|
793
|
+
if (!mediaObserver) {
|
|
794
|
+
mediaObserver = new IntersectionObserver(function (entries) {
|
|
795
|
+
for (var i = 0; i < entries.length; i++) {
|
|
796
|
+
var en = entries[i];
|
|
797
|
+
var v = en.target;
|
|
798
|
+
try {
|
|
799
|
+
if (en.isIntersecting) {
|
|
800
|
+
if (pausedByUs.has(v)) {
|
|
801
|
+
pausedByUs.delete(v);
|
|
802
|
+
var p = v.play();
|
|
803
|
+
if (p && p.catch) p.catch(function () {});
|
|
804
|
+
}
|
|
805
|
+
} else if (!v.paused && (v.muted || v.volume === 0)) {
|
|
806
|
+
pausedByUs.add(v);
|
|
807
|
+
v.pause();
|
|
808
|
+
MET.mediaPaused++;
|
|
809
|
+
}
|
|
810
|
+
} catch (e) { void e; }
|
|
811
|
+
}
|
|
812
|
+
}, { rootMargin: '240px' });
|
|
813
|
+
}
|
|
814
|
+
for (var k = 0; k < vids.length; k++) {
|
|
815
|
+
vids[k].setAttribute('data-dpb-vp', '1');
|
|
816
|
+
try { mediaObserver.observe(vids[k]); } catch (e) { void e; }
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ————————————————— 会话列表节流 —————————————————
|
|
821
|
+
// 流式输出时,会话列表经常「内容没变、只有 token 计数变了」也要整棵树重渲染。
|
|
822
|
+
// 这里包一层 store.set:可见字段真变了立刻放行;只有投影身份变了就先攒着,
|
|
823
|
+
// 最多每秒发一次。用户看不出差别,但重渲染次数从每秒几十次降到 1 次。
|
|
824
|
+
var GATE_MS = 1000;
|
|
825
|
+
var gate = null;
|
|
826
|
+
|
|
827
|
+
function sameEntry(a, b) {
|
|
828
|
+
if (a === b) return true;
|
|
829
|
+
if (a === undefined || b === undefined) return false;
|
|
830
|
+
var keys = {};
|
|
831
|
+
var k;
|
|
832
|
+
for (k in a) keys[k] = 1;
|
|
833
|
+
for (k in b) keys[k] = 1;
|
|
834
|
+
delete keys.projectionValues; // 投影身份:侧栏并不显示,变了不算可见变化
|
|
835
|
+
for (k in keys) { if (!Object.is(a[k], b[k])) return false; }
|
|
836
|
+
return true;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function sameRecord(a, b) {
|
|
840
|
+
if (a === b) return true;
|
|
841
|
+
if (a === undefined || b === undefined) return false;
|
|
842
|
+
var ka = Object.keys(a);
|
|
843
|
+
var kb = Object.keys(b);
|
|
844
|
+
if (ka.length !== kb.length) return false;
|
|
845
|
+
for (var i = 0; i < ka.length; i++) {
|
|
846
|
+
var key = ka[i];
|
|
847
|
+
if (!(key in b)) return false;
|
|
848
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
849
|
+
}
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function sameVisible(a, b) {
|
|
854
|
+
if (a === b) return true;
|
|
855
|
+
if (!a || !b) return false;
|
|
856
|
+
if (!Object.is(a.current, b.current)) return false;
|
|
857
|
+
if (!Object.is(a.phase, b.phase)) return false;
|
|
858
|
+
if (!Object.is(a.currentAddress, b.currentAddress)) return false;
|
|
859
|
+
var ai = a.ids || [];
|
|
860
|
+
var bi = b.ids || [];
|
|
861
|
+
if (ai.length !== bi.length) return false;
|
|
862
|
+
for (var i = 0; i < ai.length; i++) { if (ai[i] !== bi[i]) return false; }
|
|
863
|
+
var ab = a.byId || {};
|
|
864
|
+
var bb = b.byId || {};
|
|
865
|
+
var ak = Object.keys(ab);
|
|
866
|
+
if (ak.length !== Object.keys(bb).length) return false;
|
|
867
|
+
for (var j = 0; j < ak.length; j++) { if (!sameEntry(ab[ak[j]], bb[ak[j]])) return false; }
|
|
868
|
+
return sameRecord(a.subagentsByParent, b.subagentsByParent) && sameRecord(a.jobsBySession, b.jobsBySession);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function installListGate(store) {
|
|
872
|
+
if (!store || typeof store.set !== 'function' || typeof store.getSnapshot !== 'function') return null;
|
|
873
|
+
if (store.__dpbGate) return store.__dpbGate;
|
|
874
|
+
var originalSet = store.set.bind(store);
|
|
875
|
+
var counts = { published: 0, coalesced: 0 };
|
|
876
|
+
// 每次真正落地的发布都计时,合并掉的那次按平均发布耗时折算成「省下的毫秒」。
|
|
877
|
+
function publish(next) {
|
|
878
|
+
var t0 = performance.now();
|
|
879
|
+
try { return originalSet(next); } finally {
|
|
880
|
+
MET.setMs += performance.now() - t0;
|
|
881
|
+
MET.setRuns++;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
var pending = null;
|
|
885
|
+
var timer = null;
|
|
886
|
+
|
|
887
|
+
function clearTimer() {
|
|
888
|
+
if (timer) { window.clearTimeout(timer); timer = null; }
|
|
889
|
+
}
|
|
890
|
+
function flush() {
|
|
891
|
+
clearTimer();
|
|
892
|
+
if (pending === null) return;
|
|
893
|
+
var next = pending;
|
|
894
|
+
pending = null;
|
|
895
|
+
counts.published++;
|
|
896
|
+
publish(next);
|
|
897
|
+
}
|
|
898
|
+
var g = {
|
|
899
|
+
counts: counts,
|
|
900
|
+
set: function (next) {
|
|
901
|
+
if (!cfg.listGate) { counts.published++; return publish(next); }
|
|
902
|
+
var current = null;
|
|
903
|
+
try { current = store.getSnapshot(); } catch (e) { current = null; }
|
|
904
|
+
if (!sameVisible(current, next)) {
|
|
905
|
+
pending = null;
|
|
906
|
+
clearTimer();
|
|
907
|
+
counts.published++;
|
|
908
|
+
return publish(next);
|
|
909
|
+
}
|
|
910
|
+
pending = next;
|
|
911
|
+
counts.coalesced++;
|
|
912
|
+
MET.setMerged++;
|
|
913
|
+
if (!timer) timer = window.setTimeout(flush, GATE_MS);
|
|
914
|
+
},
|
|
915
|
+
flush: flush,
|
|
916
|
+
dispose: function () {
|
|
917
|
+
flush();
|
|
918
|
+
if (store.__dpbGate === g) {
|
|
919
|
+
delete store.__dpbGate;
|
|
920
|
+
try { store.set = originalSet; } catch (e) { void e; }
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
};
|
|
924
|
+
store.set = g.set;
|
|
925
|
+
store.__dpbGate = g;
|
|
926
|
+
try { window.__dpbGate = g; } catch (e) { void e; } // 自检用:只放句柄,不放数据
|
|
927
|
+
return g;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function safeService(name) {
|
|
931
|
+
// 没注入就 get 会抛 "cannot get property without inject",这里吞掉当没有。
|
|
932
|
+
var ctx = window.__dpbCtx;
|
|
933
|
+
if (!ctx || typeof ctx.get !== 'function') return null;
|
|
934
|
+
try { return ctx.get(name); } catch (e) { return null; }
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function applyListGate() {
|
|
938
|
+
if (gate || !cfg.listGate) return;
|
|
939
|
+
var sessions = safeService('sessions');
|
|
940
|
+
var store = sessions && sessions.list ? sessions.list : null;
|
|
941
|
+
if (!store) {
|
|
942
|
+
// sessions 服务可能比插件晚注册:每秒再试一次,最多 10 次。
|
|
943
|
+
var tries = (window.__dpbGateTries || 0) + 1;
|
|
944
|
+
window.__dpbGateTries = tries;
|
|
945
|
+
if (tries <= 10) {
|
|
946
|
+
window.setTimeout(function () { try { applyListGate(); } catch (e) { void e; } }, 1000);
|
|
947
|
+
}
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
window.__dpbGateTries = 0;
|
|
951
|
+
gate = installListGate(store);
|
|
952
|
+
if (gate) { try { console.log('[perf-boost] 会话列表节流已挂载'); } catch (e) { void e; } }
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// ————————————————— 样式 —————————————————
|
|
956
|
+
function cssText() {
|
|
957
|
+
var out = [
|
|
958
|
+
'.dpb-wrap{display:flex;flex-direction:column;gap:12px;padding:2px 0 28px}',
|
|
959
|
+
'.dpb-card{border:1px solid color-mix(in oklab,currentColor 14%,transparent);border-radius:10px;padding:14px 16px}',
|
|
960
|
+
'.dpb-card h3{margin:0 0 10px;font-size:13px;font-weight:600}',
|
|
961
|
+
'.dpb-row{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:9px 0;border-top:1px solid color-mix(in oklab,currentColor 8%,transparent)}',
|
|
962
|
+
'.dpb-row:first-of-type{border-top:0;padding-top:0}',
|
|
963
|
+
'.dpb-l{font-size:13px}',
|
|
964
|
+
'.dpb-d{font-size:11.5px;opacity:.5;margin-top:3px;line-height:1.5;max-width:420px}',
|
|
965
|
+
'.dpb-sw{position:relative;width:38px;height:21px;flex:0 0 38px;border-radius:999px;border:1px solid color-mix(in oklab,currentColor 32%,transparent);background:color-mix(in oklab,currentColor 12%,transparent);cursor:pointer;transition:background .15s}',
|
|
966
|
+
'.dpb-sw[data-on="1"]{background:var(--dsw-accent,#4d6bfe);border-color:transparent}',
|
|
967
|
+
'.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
|
+
'.dpb-sw[data-on="1"] .dpb-knob{transform:translateX(17px);background:#fff}',
|
|
969
|
+
'.dpb-quiet *, .dpb-quiet *::before, .dpb-quiet *::after{animation-play-state:paused!important}',
|
|
970
|
+
'.dpb-notes{margin:12px 2px 0;font-size:12px;line-height:1.8;opacity:.78}',
|
|
971
|
+
'.dpb-notes b{font-weight:600;opacity:1}',
|
|
972
|
+
'.dpb-note{margin:10px 2px 0;font-size:12px;line-height:1.7;opacity:.72}',
|
|
973
|
+
'.dpb-stat{display:flex;gap:16px;flex-wrap:wrap;font-size:12px;opacity:.75}',
|
|
974
|
+
'.dpb-stat b{font-weight:600;opacity:1}',
|
|
975
|
+
'.dpb-score{display:flex;align-items:center;gap:14px;flex-wrap:wrap}',
|
|
976
|
+
'.dpb-score-num{font:700 30px/1 ui-monospace,Consolas,monospace;color:var(--dsw-accent,#4d6bfe)}',
|
|
977
|
+
'.dpb-score-title{font-size:13px;font-weight:600}',
|
|
978
|
+
'.dpb-btn{font:inherit;font-size:12px;line-height:1;padding:7px 11px;border-radius:7px;border:1px solid color-mix(in oklab,currentColor 20%,transparent);background:transparent;color:inherit;cursor:pointer}',
|
|
979
|
+
'.dpb-btn:hover{border-color:color-mix(in oklab,currentColor 45%,transparent)}',
|
|
980
|
+
'#' + BADGE_ID + '{position:fixed;right:10px;bottom:10px;z-index:2147483000;font:600 12px/1 ui-monospace,Consolas,monospace;padding:4px 8px;border-radius:6px;background:rgba(0,0,0,.6);color:#6fe3c4;pointer-events:none}',
|
|
981
|
+
];
|
|
982
|
+
if (cfg.reduceMotion) {
|
|
983
|
+
out.push('*,*::before,*::after{animation-duration:.001ms !important;animation-iteration-count:1 !important;transition-duration:.001ms !important;scroll-behavior:auto !important}');
|
|
984
|
+
}
|
|
985
|
+
if (cfg.noBlur) {
|
|
986
|
+
out.push('*,*::before,*::after{backdrop-filter:none !important;-webkit-backdrop-filter:none !important}');
|
|
987
|
+
}
|
|
988
|
+
return out.join('\n');
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function applyStyle() {
|
|
992
|
+
var el = document.getElementById(STYLE_ID);
|
|
993
|
+
if (!el) {
|
|
994
|
+
el = document.createElement('style');
|
|
995
|
+
el.id = STYLE_ID;
|
|
996
|
+
document.head.appendChild(el);
|
|
997
|
+
}
|
|
998
|
+
el.textContent = cssText();
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ————————————————— 读数 —————————————————
|
|
1002
|
+
var lastFps = 0;
|
|
1003
|
+
|
|
1004
|
+
function startBadge() {
|
|
1005
|
+
var el = document.getElementById(BADGE_ID);
|
|
1006
|
+
if (!el) {
|
|
1007
|
+
el = document.createElement('div');
|
|
1008
|
+
el.id = BADGE_ID;
|
|
1009
|
+
document.body.appendChild(el);
|
|
1010
|
+
}
|
|
1011
|
+
el.style.display = cfg.showFps ? 'block' : 'none';
|
|
1012
|
+
if (window.__dpbBadge) return;
|
|
1013
|
+
window.__dpbBadge = true;
|
|
1014
|
+
var frames = 0, t0 = 0;
|
|
1015
|
+
var loop = function (ts) {
|
|
1016
|
+
frames++;
|
|
1017
|
+
if (!t0) t0 = ts;
|
|
1018
|
+
if (ts - t0 >= 1000) {
|
|
1019
|
+
lastFps = Math.round((frames * 1000) / (ts - t0));
|
|
1020
|
+
frames = 0;
|
|
1021
|
+
t0 = ts;
|
|
1022
|
+
var node = document.getElementById(BADGE_ID);
|
|
1023
|
+
if (node && cfg.showFps) node.textContent = lastFps + ' fps';
|
|
1024
|
+
notify();
|
|
1025
|
+
}
|
|
1026
|
+
window.requestAnimationFrame(loop);
|
|
1027
|
+
};
|
|
1028
|
+
window.requestAnimationFrame(loop);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// ————————————————— 设置页 —————————————————
|
|
1032
|
+
function Sw(props) {
|
|
1033
|
+
return React.createElement('button', {
|
|
1034
|
+
type: 'button', role: 'switch', 'aria-label': props.label,
|
|
1035
|
+
className: 'dpb-sw', 'data-on': props.value ? '1' : '0',
|
|
1036
|
+
onClick: function () { props.onChange(!props.value); },
|
|
1037
|
+
}, React.createElement('span', { className: 'dpb-knob' }));
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function Row(props) {
|
|
1041
|
+
return React.createElement('div', { className: 'dpb-row' },
|
|
1042
|
+
React.createElement('div', null,
|
|
1043
|
+
React.createElement('div', { className: 'dpb-l' }, props.label),
|
|
1044
|
+
props.hint ? React.createElement('div', { className: 'dpb-d' }, props.hint) : null),
|
|
1045
|
+
props.children);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function Panel() {
|
|
1049
|
+
// 自检用:数一下这个面板到底渲染了几次(正常应该是「打开 1 次 + 数字变化时才加」)。
|
|
1050
|
+
try { window.__dpbPanelRenders = (window.__dpbPanelRenders || 0) + 1; } catch (e) { void e; }
|
|
1051
|
+
var t = React.useState(0);
|
|
1052
|
+
var bump = t[1];
|
|
1053
|
+
React.useEffect(function () {
|
|
1054
|
+
// 打开设置页时才问一次版本,别在启动时和后台定时器里白花网络请求。
|
|
1055
|
+
if (update.state === 'idle') { try { checkUpdate(); } catch (e) { void e; } }
|
|
1056
|
+
}, []);
|
|
1057
|
+
React.useEffect(function () {
|
|
1058
|
+
// 只在「数字真的变了」时才重渲染:页面在后台不动,前台最长 5 秒看一眼。
|
|
1059
|
+
// 之前是每秒无条件 bump,设置页开着就等于每秒一次 React 重渲染,纯属自找开销。
|
|
1060
|
+
var last = '';
|
|
1061
|
+
var sig = function () {
|
|
1062
|
+
var g = window.__dpbGate ? window.__dpbGate.counts : null;
|
|
1063
|
+
return [
|
|
1064
|
+
metPercent(), Math.round(metSavedMs() / 100),
|
|
1065
|
+
MET.rafSkipped, MET.timerSkipped,
|
|
1066
|
+
g ? g.published : 0, g ? g.coalesced : 0,
|
|
1067
|
+
MET.lazyItems, MET.mediaLazy, MET.mediaPaused,
|
|
1068
|
+
].join('|');
|
|
1069
|
+
};
|
|
1070
|
+
var tick = function () {
|
|
1071
|
+
if (document.hidden) return;
|
|
1072
|
+
snapNow();
|
|
1073
|
+
var now = metPercent();
|
|
1074
|
+
if (now !== null) saveLast(now, metSavedMs(), MET.rafMs + MET.timerMs + MET.setMs);
|
|
1075
|
+
var s = sig();
|
|
1076
|
+
if (s === last) return;
|
|
1077
|
+
last = s;
|
|
1078
|
+
bump(function (n) { return n + 1; });
|
|
1079
|
+
};
|
|
1080
|
+
last = sig();
|
|
1081
|
+
var off = subscribe(tick);
|
|
1082
|
+
var timer = window.setInterval(tick, 5000);
|
|
1083
|
+
return function () { off(); window.clearInterval(timer); };
|
|
1084
|
+
}, []);
|
|
1085
|
+
|
|
1086
|
+
var warmup = React.useState(null);
|
|
1087
|
+
var warmupOn = warmup[0];
|
|
1088
|
+
var setWarmupOn = warmup[1];
|
|
1089
|
+
React.useEffect(function () {
|
|
1090
|
+
if (warmupOn !== null) return;
|
|
1091
|
+
warmupCall().then(function (v) { if (v !== null) setWarmupOn(v); });
|
|
1092
|
+
}, [warmupOn]);
|
|
1093
|
+
|
|
1094
|
+
var live = metPercent();
|
|
1095
|
+
var saved = metSavedMs();
|
|
1096
|
+
var busy = MET.rafMs + MET.timerMs + MET.setMs;
|
|
1097
|
+
var cached = live === null ? (readLast() || remoteLast) : null;
|
|
1098
|
+
var pct = live !== null ? live : (cached ? cached.pct : null);
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
return React.createElement('div', { className: 'dpb-wrap' },
|
|
1102
|
+
|
|
1103
|
+
React.createElement('div', { className: 'dpb-card' },
|
|
1104
|
+
React.createElement('div', { className: 'dpb-score' },
|
|
1105
|
+
React.createElement('div', { className: 'dpb-score-num' }, pct === null ? '—' : (pct === 0 && saved < 1 ? '—' : '≈' + pct + '%')),
|
|
1106
|
+
React.createElement('div', null,
|
|
1107
|
+
React.createElement('div', { className: 'dpb-score-title' },
|
|
1108
|
+
'最近 1 分钟省下的主线程时间')))),
|
|
1109
|
+
|
|
1110
|
+
React.createElement('div', { className: 'dpb-card' },
|
|
1111
|
+
React.createElement('h3', null, '开关'),
|
|
1112
|
+
React.createElement(Row, { label: '长对话省渲染', hint: lazyStatus() + ' 可能会导致被跳过的行滚进屏幕时空白一瞬,很快就补上。' },
|
|
1113
|
+
React.createElement(Sw, { label: '长对话省渲染', value: !!cfg.lazyList, onChange: function (v) { setCfg({ lazyList: v }); } })),
|
|
1114
|
+
React.createElement(Row, { label: '窗口隐藏不排帧', hint: '最小化或切走后不再送帧。可能会导致后台标签页里的动画停住,切回前台补一帧。默认开。' },
|
|
1115
|
+
React.createElement(Sw, { label: '窗口隐藏不排帧', value: !!cfg.hiddenPause, onChange: function (v) { setCfg({ hiddenPause: v }); } })),
|
|
1116
|
+
React.createElement(Row, { label: '空闲降帧', hint: '20 秒没操作把 JS 动画降到 30fps,CSS 动画不受影响。可能会导致看着觉得动画变慢,动一下鼠标立刻恢复。默认开。' },
|
|
1117
|
+
React.createElement(Sw, { label: '空闲降帧', value: !!cfg.idleThrottle, onChange: function (v) { setCfg({ idleThrottle: v }); } })),
|
|
1118
|
+
React.createElement(Row, { label: '媒体懒加载', hint: '图片快进视口才解码,视频只取元数据。可能会导致快速拖到很远处的图片先白一下再出来。默认开。' },
|
|
1119
|
+
React.createElement(Sw, { label: '媒体懒加载', value: !!cfg.mediaLazy, onChange: function (v) { setCfg({ mediaLazy: v }); } })),
|
|
1120
|
+
React.createElement(Row, { label: '后台暂停定时器', hint: '窗口不可见时,插件的每秒轮询全部跳过;回到前台补跳一次。可能会导致隐藏期间攒下的轮询只补一次,个别插件会看到一次跳变。默认开。' },
|
|
1121
|
+
React.createElement(Sw, { label: '后台暂停定时器', value: !!cfg.hiddenTimers, onChange: function (v) { setCfg({ hiddenTimers: v }); } })),
|
|
1122
|
+
React.createElement(Row, { label: '视口外暂停视频', hint: '静音视频滚出屏幕就暂停解码,滚回来继续播。可能会导致滚回来时黑一下或重新缓冲。有声音的不动。默认开。' },
|
|
1123
|
+
React.createElement(Sw, { label: '视口外暂停视频', value: !!cfg.mediaPause, onChange: function (v) { setCfg({ mediaPause: v }); } })),
|
|
1124
|
+
React.createElement(Row, { label: '会话列表节流', hint: '流式输出时列表只因 token 计数变化就整树重渲染;这里把它压到每秒一次,可见内容真变了立刻放行。可能会导致侧栏 token 数字最多晚 1 秒才跳。默认开。' },
|
|
1125
|
+
React.createElement(Sw, { label: '会话列表节流', value: !!cfg.listGate, onChange: function (v) { setCfg({ listGate: v }); } })),
|
|
1126
|
+
React.createElement(Row, { label: '大会话预热', hint: '启动 15 秒后在后台把最近 10 个 8MB 以上会话读进系统文件缓存。可能会导致读盘那几十秒里打开会话稍慢、磁盘占用高一点。' + (warmupOn === false ? '当前已关闭。' : '') },
|
|
1127
|
+
React.createElement(Sw, {
|
|
1128
|
+
label: '大会话预热',
|
|
1129
|
+
value: warmupOn !== false,
|
|
1130
|
+
onChange: function (v) {
|
|
1131
|
+
setWarmupOn(v);
|
|
1132
|
+
warmupCall(v).then(function (real) { if (real !== null) setWarmupOn(real); });
|
|
1133
|
+
},
|
|
1134
|
+
})),
|
|
1135
|
+
React.createElement(Row, { label: '关闭背景模糊', hint: '浮层从磨砂变纯色底,能看出来。默认关。' },
|
|
1136
|
+
React.createElement(Sw, { label: '关闭背景模糊', value: !!cfg.noBlur, onChange: function (v) { setCfg({ noBlur: v }); } })),
|
|
1137
|
+
React.createElement(Row, { label: '减少动画', hint: '关掉 CSS 动画和过渡,弹窗不再逐帧动。默认关。' },
|
|
1138
|
+
React.createElement(Sw, { label: '减少动画', value: !!cfg.reduceMotion, onChange: function (v) { setCfg({ reduceMotion: v }); } })),
|
|
1139
|
+
React.createElement(Row, { label: '帧率角标', hint: '右下角常驻一个 fps 小方块,看数据用。默认关。' },
|
|
1140
|
+
React.createElement(Sw, { label: '帧率角标', value: !!cfg.showFps, onChange: function (v) { setCfg({ showFps: v }); } })),
|
|
1141
|
+
),
|
|
1142
|
+
|
|
1143
|
+
React.createElement('div', { className: 'dpb-card' },
|
|
1144
|
+
React.createElement('div', { className: 'dpb-stat' },
|
|
1145
|
+
React.createElement('span', null, '版本 ', React.createElement('b', null, update.installed ? 'v' + update.installed : 'v' + VERSION)),
|
|
1146
|
+
React.createElement('span', null, '最新 ', React.createElement('b', null,
|
|
1147
|
+
update.state === 'checking' ? '检查中…'
|
|
1148
|
+
: update.available ? 'v' + update.latest
|
|
1149
|
+
: update.latest ? '已是最新'
|
|
1150
|
+
: update.state === 'unknown' ? (update.error === 'not-published' ? '未发布' : '未查到')
|
|
1151
|
+
: update.state === 'failed' ? '未查到'
|
|
1152
|
+
: '—')),
|
|
1153
|
+
React.createElement('button', {
|
|
1154
|
+
type: 'button', className: 'dpb-btn',
|
|
1155
|
+
onClick: function () { checkUpdate(); },
|
|
1156
|
+
}, update.state === 'checking' ? '检查中' : '检查更新')),
|
|
1157
|
+
React.createElement('div', { className: 'dpb-notes' },
|
|
1158
|
+
React.createElement('b', null, '更新详情'),
|
|
1159
|
+
RELEASE_NOTES[0].items.map(function (line, i) {
|
|
1160
|
+
return React.createElement('div', { key: 'n' + i }, line);
|
|
1161
|
+
}))));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// ————————————————— 装配 —————————————————
|
|
1165
|
+
function applyAll() {
|
|
1166
|
+
if (!cfg.hiddenPause && !cfg.idleThrottle) uninstallRafThrottle();
|
|
1167
|
+
else installRafThrottle();
|
|
1168
|
+
installTimerPause();
|
|
1169
|
+
applyStyle();
|
|
1170
|
+
// 首屏别抢:等空闲再扫,扫描期间若在流式输出会往后推。
|
|
1171
|
+
idleRun(function () {
|
|
1172
|
+
try { applyLazyList(); } catch (e) { void e; }
|
|
1173
|
+
try { applyMediaLazy(); } catch (e) { void e; }
|
|
1174
|
+
try { applyViewportMedia(); } catch (e) { void e; }
|
|
1175
|
+
}, 600);
|
|
1176
|
+
if (cfg.lazyList) watchLazyList();
|
|
1177
|
+
if (cfg.showFps) startBadge();
|
|
1178
|
+
else {
|
|
1179
|
+
var el = document.getElementById(BADGE_ID);
|
|
1180
|
+
if (el) el.style.display = 'none';
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function applyHiddenClass() {
|
|
1185
|
+
var root = document.documentElement;
|
|
1186
|
+
if (!root) return;
|
|
1187
|
+
if (cfg.hiddenPause && document.hidden) root.classList.add('dpb-quiet');
|
|
1188
|
+
else root.classList.remove('dpb-quiet');
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function mount() {
|
|
1192
|
+
applyAll();
|
|
1193
|
+
// 采样快照自己走:不依赖设置页开着,否则百分比只在打开设置后才更新。
|
|
1194
|
+
// 2 秒一条,只往 ring 里放三个数字,隐藏时被上面的定时器接管一起跳过。
|
|
1195
|
+
if (!window.__dpbSnapTimer) {
|
|
1196
|
+
snapNow();
|
|
1197
|
+
window.__dpbSnapTimer = window.setInterval(snapNow, 2000);
|
|
1198
|
+
}
|
|
1199
|
+
document.addEventListener('visibilitychange', flushTimers);
|
|
1200
|
+
document.addEventListener('visibilitychange', applyHiddenClass);
|
|
1201
|
+
applyHiddenClass();
|
|
1202
|
+
watchBusy();
|
|
1203
|
+
|
|
1204
|
+
['pointerdown', 'pointermove', 'keydown', 'wheel', 'scroll', 'touchstart'].forEach(function (evt) {
|
|
1205
|
+
window.addEventListener(evt, markInput, { passive: true, capture: true });
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
var ctx = window.__dpbCtx;
|
|
1209
|
+
var slots = ctx && typeof ctx.get === 'function' ? ctx.get('slots') : null;
|
|
1210
|
+
try { applyListGate(); } catch (e) { void e; }
|
|
1211
|
+
if (!slots || typeof slots.inject !== 'function') return;
|
|
1212
|
+
slots.inject('settings.section', function () {
|
|
1213
|
+
return slots.register(
|
|
1214
|
+
{ name: 'settings.section', id: NAME, order: 910, label: function () { return '性能优化'; } },
|
|
1215
|
+
function () { return React.createElement(Panel, null); },
|
|
1216
|
+
);
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
exports.inject = ['slots', 'sessions'];
|
|
1221
|
+
exports.apply = function (ctx) {
|
|
1222
|
+
window.__dpbCtx = ctx;
|
|
1223
|
+
try { loadLastRun(); } catch (e) { void e; }
|
|
1224
|
+
try { window.__dpbMet = MET; window.__dpbMetSnapshot = function () { return { saved: metSavedMs(), busy: MET.rafMs + MET.timerMs + MET.setMs, pct: metPercent() }; }; } catch (e) { void e; }
|
|
1225
|
+
try { mount(); } catch (e) { try { console.warn('[dsh-perf-boost]', e); } catch (err) { void err; } }
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
return module.exports;
|
|
1229
|
+
},
|
|
1230
|
+
});
|