Skip to content

Commit a03ab7f

Browse files
committed
perf: 把 content-observer 的优化覆盖扩展到所有重型视频站、缩略图墙、聊天直播站
之前只针对 B 站做了三轴优化(Set 去重 / SKIP_TAGS 跳媒体元素 / mutation+resource 上限自停),但场景类似的站点很多——抖音 / 快手 / TikTok 短视频流、YouTube / Twitch / niconico / AcFun 弹幕聊天、Pornhub / XHamster / XVideos / MissAV / Jable / JavDB 等大量缩略图墙加无限滚动。这一轮把这套保护机制做厚做全。 PerformanceObserver 加双轴过滤: - SKIP_INITIATOR_TYPES 拒绝 img / video / audio / beacon / track / object / embed / css 这八类 entry。这些资源对识别技术栈零价值(图片是产品 / 缩略图,video / audio 是流媒体本体,css 是样式不是 stylesheet 文件本身),却占掉绝大多数 PerformanceObserver entry。Pornhub / 抖音的资源数能从几千降到几百 - SKIP_RESOURCE_EXT 兜底正则:.ts / .m4s / .mp4 / .webm / .mov / .m3u8 / .mpd / .jpg / .jpeg / .png / .gif / .webp / .avif / .ico / .woff / .woff2 / .ttf / .otf / .eot 这些扩展名直接 skip。HLS 视频流 .ts 分片每分钟产生几百条,不过滤一定打满。entry 携带 query string 也匹配。两轴并联是因为 initiatorType 不一定可靠(某些 SDK 用 fetch 加载 m3u8,initiatorType=fetch 不会被 SKIP_INITIATOR_TYPES 拦下,靠扩展名兜底) mutation 加 burst 冷却: - 1 秒滑动窗口,新增节点数累计超过 MUTATION_BURST_THRESHOLD = 300 直接进 5 秒冷却(mutationCooldownUntil = now + 5000) - 冷却期内整个 MutationObserver 回调直接 return,不入队、不 flush、清掉 pendingMutationNodes 与未调度的 RAF - 抖音 / 快手 / TikTok feed 滑动一次插入 200-1000 个 DOM 节点,在原本「累计 5000 条自停」之前就已经卡 1-2 秒。Burst 冷却让前 300 条收完就立即停采集 5 秒,给主线程让位 - 5 秒后窗口重新开始计数,如果用户已经从短视频流滑走或停手,新窗口里 mutation 数量会回归正常水平继续采集;如果还在持续滑动,下一个窗口同样很快触发冷却,循环保护 弹幕 / 聊天 / 直播 / feed 容器整个子树拉黑: - SKIP_CONTAINER_NAMES 是一组正则,匹配元素的 id 或 class token:danmaku / barrage / bullet-comment / bullet-screen / chat / chat-(panel|area|list|box|room|stream|window) / live-chat / comment-(stream|live|list) / feed / webcast - 命中:B 站 / AcFun / niconico 弹幕容器、Twitch / YouTube live chat、抖音 / TikTok 直播间 webcast 容器、各 SPA 推荐 feed - 检查时机:collectFromElement 入口和 mutation push 入队前各拦截一次。命中即整个子树跳过,不进 querySelectorAll、不入队 - 实现细节:matchesSkipContainer 把 element.id + className.split(/\s+/) 拼成 token 列表,对 9 条正则做 token 级匹配(不跑 element.matches,避免选择器编译开销)。class token 切分一次,正则数组遍历一次 state.resourceCount 计入跳过前的总数(用作自停阈值),但 addUrl 只对真正入列的资源算"添加成功",scheduleSend 只在真正添加时触发。这样过滤后的 entry 不会引发不必要的 sendMessage 噪声,但累计阈值仍按"实际收到了多少 entry"工作。 插件版本升级到 1.1.3。
1 parent 0f147b5 commit a03ab7f

2 files changed

Lines changed: 59 additions & 3 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stackprism",
33
"private": true,
4-
"version": "1.1.2",
4+
"version": "1.1.3",
55
"type": "module",
66
"description": "StackPrism 用于检测网页前端、后端、CDN、SaaS、广告营销、统计、登录、支付、网站程序和主题模板线索。",
77
"scripts": {

src/content/content-observer.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,25 @@
55
const MAX_MUTATION_COUNT = 5000
66
const MAX_RESOURCE_COUNT = 1500
77
const SEND_DELAY = 900
8+
const MUTATION_BURST_WINDOW_MS = 1000
9+
const MUTATION_BURST_THRESHOLD = 300
10+
const MUTATION_COOLDOWN_MS = 5000
811
const CONTEXT_INVALIDATED_PATTERN = /extension context invalidated|context invalidated/i
912
const OBSERVER_INSTANCE_KEY = '__stackPrismContentObserver__'
1013
const SKIP_TAGS = new Set(['VIDEO', 'AUDIO', 'CANVAS', 'PICTURE', 'SOURCE', 'TRACK', 'SVG', 'IMG'])
14+
const SKIP_INITIATOR_TYPES = new Set(['img', 'video', 'audio', 'beacon', 'track', 'object', 'embed', 'css'])
15+
const SKIP_RESOURCE_EXT = /\.(ts|m4s|mp4|webm|mov|m3u8|mpd|jpg|jpeg|png|gif|webp|avif|ico|woff2?|ttf|otf|eot)(\?.*)?$/i
16+
const SKIP_CONTAINER_NAMES = [
17+
/danmaku/i,
18+
/bullet[\s_-]*(comment|screen|chat)/i,
19+
/barrage/i,
20+
/(^|[\s._#-])chat([\s._#-]|$)/i,
21+
/chat-?(panel|area|list|box|room|stream|window)/i,
22+
/live-?chat/i,
23+
/comment-?(stream|live|list)/i,
24+
/(^|[\s._-])feed([\s._-]|$)/i,
25+
/webcast/i
26+
]
1127
const state = {
1228
startedAt: Date.now(),
1329
updatedAt: Date.now(),
@@ -39,6 +55,9 @@
3955
let wrappedReplaceState = null
4056
let pendingMutationNodes = []
4157
let pendingMutationFrame = 0
58+
let mutationBurstWindowStart = Date.now()
59+
let mutationBurstCount = 0
60+
let mutationCooldownUntil = 0
4261

4362
// ----- 底层 helper -----
4463

@@ -197,9 +216,25 @@
197216
const SUBTREE_SCAN_LIMIT = 200
198217
const SUBTREE_SELECTOR = 'script[src], link[href], iframe[src], [id], [class], [data-v-app], [ng-version], astro-island, astro-slot'
199218

219+
const matchesSkipContainer = element => {
220+
const tokens = []
221+
const id = element.id
222+
if (id) tokens.push(id)
223+
const className = typeof element.className === 'string' ? element.className : element.getAttribute?.('class') || ''
224+
if (className) {
225+
for (const piece of className.split(/\s+/)) if (piece) tokens.push(piece)
226+
}
227+
if (!tokens.length) return false
228+
for (const re of SKIP_CONTAINER_NAMES) {
229+
for (const token of tokens) if (re.test(token)) return true
230+
}
231+
return false
232+
}
233+
200234
const collectFromElement = element => {
201235
let changed = false
202236
if (SKIP_TAGS.has(element.tagName)) return changed
237+
if (matchesSkipContainer(element)) return changed
203238
changed = collectElementIfRelevant(element) || changed
204239
if (!element.querySelectorAll || !element.childElementCount) return changed
205240
const matches = element.querySelectorAll(SUBTREE_SELECTOR)
@@ -369,15 +404,18 @@
369404
try {
370405
const observer = new PerformanceObserver(list => {
371406
if (stopped) return
407+
let added = 0
372408
for (const entry of list.getEntries()) {
373-
addUrl('resources', entry.name)
409+
if (SKIP_INITIATOR_TYPES.has(entry.initiatorType)) continue
410+
if (SKIP_RESOURCE_EXT.test(entry.name)) continue
411+
if (addUrl('resources', entry.name)) added += 1
374412
state.resourceCount += 1
375413
}
376414
if (state.resourceCount >= MAX_RESOURCE_COUNT) {
377415
observer.disconnect()
378416
performanceObserver = null
379417
}
380-
scheduleSend()
418+
if (added) scheduleSend()
381419
})
382420
performanceObserver = observer
383421
observer.observe({ type: 'resource', buffered: true })
@@ -426,14 +464,32 @@
426464
const root = document.documentElement || document
427465
const observer = new MutationObserver(mutations => {
428466
if (stopped) return
467+
const now = Date.now()
468+
if (now < mutationCooldownUntil) return
469+
if (now - mutationBurstWindowStart > MUTATION_BURST_WINDOW_MS) {
470+
mutationBurstWindowStart = now
471+
mutationBurstCount = 0
472+
}
429473
for (const mutation of mutations) {
430474
for (const node of mutation.addedNodes) {
431475
if (node.nodeType !== Node.ELEMENT_NODE) continue
432476
if (SKIP_TAGS.has(node.tagName)) continue
477+
if (matchesSkipContainer(node)) continue
433478
state.mutationCount += 1
479+
mutationBurstCount += 1
434480
pendingMutationNodes.push(node)
435481
}
436482
}
483+
if (mutationBurstCount >= MUTATION_BURST_THRESHOLD) {
484+
mutationCooldownUntil = now + MUTATION_COOLDOWN_MS
485+
pendingMutationNodes = []
486+
if (pendingMutationFrame) {
487+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(pendingMutationFrame)
488+
else clearTimeout(pendingMutationFrame)
489+
pendingMutationFrame = 0
490+
}
491+
return
492+
}
437493
if (state.mutationCount >= MAX_MUTATION_COUNT) {
438494
observer.disconnect()
439495
mutationObserver = null

0 commit comments

Comments
 (0)