本帖最后由 steven026 于 2022-9-2 20:08 编辑
本帖最后由 steven026 于 2022-9-2 19:32 编辑
前言
本文仅针对JS定时器setInterval和setTimeout在浏览器非活动标签页中的最小定时间隔进行探究并给出解决方案,非JS定时器入门指南,不再赘述其基础用法,如有需要可自行阅读MDN文档。
https://developer.mozilla.org/zh-CN/docs/Web/API/setInterval
https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout
介绍
原生JavaScript中并未setInterval和setTimeout的最小定时间隔,但根据setInterval和setTimeout定时原理,过小的定时间隔可能会出现各种性能错误,因此主流浏览器出于稳定性考虑人为规定了一个最小间隔4ms来减少错误,若定时器被创建时的定时间隔缺省或小于4ms,定时间隔会被浏览器提升至4ms(实际间隔受到原理影响或代码影响会略有浮动,并非严格>=4ms)。
且出于节能考虑,当浏览器标签处于不活动状态时(大多数情况下可以通过!document.hidden
或是否正在播放音频来判断标签是否活动),这个最小定时间隔setInterval会被提升到1000ms而setTimeout会增加1000ms(经测试会不断在原始间隔和浏览器提升间隔中来回跳动,间隔越低越不稳定)。
根据这一浏览器特性,未经特殊处理的JS定时器往往会出现各种不符合预期的错误。
定时器实际间隔测试
一般可以通过记录定时器运行前的时间和运行后的时间,并将两次时间相减来获取实际定时器间隔。
时间记录一般可以使用new Date()
或performance.now()
。
具体代码表现为
//setInterval
let start = past = performance.now()
let delay = 0
let i = 1
function timer(text) {
let now = performance.now()
let cost = now - past
let costAll = now - start
past = now
console.log(`${text}+${document.visibilityState} 第${i}次运行;总间隔${costAll};平均间隔${costAll / i};两次间隔${cost};设置间隔${delay};`)
i++
}
let timerInterval = setInterval(() => {
timer("setInterval")
if (i > 1000) clearInterval(timerInterval)
}, delay)
//setTimeout
let start = past = performance.now()
let delay = 0
let i = 1
!(function timer(text) {
let now = performance.now()
let cost = now - past
let costAll = now - start
past = now
console.log(`${text}+${document.visibilityState} 第${i}次运行;总间隔${costAll};平均间隔${costAll / i};两次间隔${cost};设置间隔${delay};`)
i++
if (i <= 1000) setTimeout(timer, delay, "setTimeout")
})()
测试结果仅供参考(EDGE v104)
setInterval 活动 设置0ms 实际4ms
setInterval 不活动 设置0ms 实际0~1000ms 平均500ms
setInterval 不活动 设置200ms 实际1000ms(未浮动 但一段时间后会被提升至60000ms)
setTimeout 活动 设置0ms 实际5ms
setInterval 不活动 设置2000ms 实际2000~3000ms 平均2500ms
测试数据基本和上述介绍中数据吻合
(和MDN部分数据冲突,可能和浏览器新行为特性有关)
应对方法
查阅相关资料可以发现一般有3种办法,使浏览器非活动标签中页的实际间隔降低到活动标签的最小间隔。
注:方法1个人在v104+Chrome\EDGE中测试失效,不推荐使用;方法2、方法3受CSP跨域限制,应对方法见后文。
1.修改浏览器设置(个人测试没什么效果且设置较麻烦)
仅供参考
https://wiki.melvoridle.com/w/Mitigating_Browser_Throttling
2.循环播放一段-58db以上的空音频
当一个标签页在播放音频时会被浏览器认为该标签页属于活动页,即使document.hidden == true
下方代码改进自原文章 https://stackoverflow.com/a/51191818/914546
原文章写于2018年,代码中的@resource音频链接以后可能会失效,建议下载到本地或保存为Base64
大致思路是通过@resource+GM_getResourceURL使用油猴下载一段-58db以上的空音频,然后通过new Audio+loop方式挂载到DOM标签中,并为document添加"visibilitychange"事件,当document隐藏时播放音频,显示时暂停播放。
(由于原音频是http协议,无法直接在https协议中使用,偷懒直接用GM_getResourceURL+unsafeWindow了,可以自行上传到https地址或手动将其转换成base64在none环境下直接用)
// ==UserScript==
// @name 保持浏览器标签活动
// @version 1.0
// @description 循环播放一段-58db空音频
// @author DreamNya
// @match https://bbs.tampermonkey.net.cn/
// @grant GM_getResourceURL
// @grant unsafeWindow
// @resource ogg http://adventure.land/sounds/loops/empty_loop_for_js_performance.ogg
// @resource wav http://adventure.land/sounds/loops/empty_loop_for_js_performance.wav
// ==/UserScript==
let ogg = new Audio(GM_getResourceURL("ogg").replace("data:application", "data:audio/ogg"));
ogg.loop = true;
unsafeWindow.ogg = ogg;
let wav = new Audio(GM_getResourceURL("wav").replace("data:application", "data:audio/wav"));
wav.loop = true;
unsafeWindow.wav = wav;
document.addEventListener("visibilitychange", function () { //document不可见时循环播放空音频 可见时暂停
document.hidden ? ogg.play() : ogg.pause() //ogg和wav二选一就行
})
3.Web Worker
没有参考文章,自己根据MDN文档研究改进的方法。
大致思路是由于Web Woker只接受同域.js或者Blob,因此可以通过new Blob创建一个Worker函数,再通过URL.createObjectURL创建Blob链接供new Worker创建一个不受浏览器限制的独立线程,由Worker不断给油猴作用域postMessage,然后通过油猴作用域的onmessage回调函数。
由于Web Woker线程有自己独立的作用域,且postMessage无法传递也无法使Worker访问window,因此Web Woker除了定时setInterval或setTimeout不需要做任何事,只要当个工具人就可以了,回调函数全部写在油猴内就行。postMessage也不需要传递任何内容,传递本身这个动作足矣。
另外Web Worker的最小定时间隔也是4ms,Web Worker中的setTimeout也从5ms降低到了4ms。具体测试结果运行下方代码就可以看到。
// ==UserScript==
// @name 工具人Web Worker setInterval
// @version 1.0
// @description 安安静静定时定时postMessage就够了
// @author DreamNya
// @match https://bbs.tampermonkey.net.cn/
// @grant none
// ==/UserScript==
let worker_fuc = "setInterval(()=>{postMessage('')},0)"; //Worker函数
let worker_Blob = new Blob([worker_fuc]); //生成一个Blob
let worker_URL = URL.createObjectURL(worker_Blob); //生成一个Blob链接
let worker = new Worker(worker_URL); //创建一个Web Worker
worker.onmessage = function (e) {
callback()
if (i > 1000) worker.terminate()
}
let start = performance.now();
let past = start;
let delay = 0;
let i = 1;
function callback(text = "setInterval+Worker") {
let now = performance.now()
let cost = now - past
let costAll = now - start
past = now
console.log(`${text}+${document.visibilityState} 第${i}次运行;总间隔${costAll};平均间隔${costAll / i};两次间隔${cost};设置间隔${delay};`)
i++
}
// ==UserScript==
// @name 工具人Web Worker setTimout
// @version 1.0
// @description 安安静静定时postMessage就够了
// @author DreamNya
// @match https://bbs.tampermonkey.net.cn/
// @grant none
// ==/UserScript==
let worker_fuc = "onmessage=function(e){setTimeout(()=>{postMessage('')},0)}"; //Worker函数 onmessage后setTimeout
let worker_Blob = new Blob([worker_fuc]); //生成一个Blob
let worker_URL = URL.createObjectURL(worker_Blob); //生成一个Blob链接
let worker = new Worker(worker_URL); //创建一个Web Worker
worker.onmessage = function (e) {
callback()
if (i > 1000) worker.terminate()
}
let start = performance.now();
let past = start;
let delay = 0;
let i = 1;
function callback(text = "setTimeout+Worker") {
let now = performance.now()
let cost = now - past
let costAll = now - start
past = now
console.log(`${text}+${document.visibilityState} 第${i}次运行;总间隔${costAll};平均间隔${costAll / i};两次间隔${cost};设置间隔${delay};`)
i++
worker.postMessage("") //完成上述函数后postMessage相当于setTimeout
}
关于CSP跨域限制的应对方法
注:似乎只能应对写在responseHeader中的CSP,无法解决写在DOM meta标签中的CSP(待研究)
以下内容出处https://github.com/lisonge/vite-plugin-monkey/blob/main/README_zh.md#csp
你可以使用 Tampermonkey 然后打开插件配置 extension://iikmkjmpaadaobahmlepeloendndfphd/options.html#nav=settings
在 安全
, 设置 如果站点有内容安全策略(CSP)则向其策略:
为 全部移除(可能不安全)
具体情况请看 issues/1
如果你使用 Violentmonkey
/Greasemonkey
, 你能通过以下方式解决
测试结果(测试网站MDN)
当我们打开MDN时,默认情况MDN(或其他部分网站)出于安全防护目的会在responseHeader中设置CSP:'self'限制跨域防止被脚本攻击,同时这也可能会限制油猴/脚本猫脚本运行(见下图)
对于这种网站,若想使脚本中自定义的Audio/Worker正常运行,我们可以通过浏览器插件来绕开CSP限制。(以下为油猴设置方法,不需要额外插件;脚本猫或其他插件可以用上面介绍的方法绕开)
刷新页面,重新测试,此时不再受CSP限制了。
完结撒花
欢迎各位对此进行交流讨论,第一次写这类文章,如有不足之处尽请谅解。
更新日志:
2022-09-02:v2 更新CSP跨域限制应对方法
2022-08-30:v1 初次发布