Tab Visibility Interval Test V2
More advanced version of Tab-Safe interval testing
by ryunp
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<div style="max-width: 666px; margin: 0 auto">
<div id="intro-text" style="text-align: center;">
<p style="font-weight: bold;margin: 0px; padding: 0px">Observing Timers During Tab Visibility Changes</p>
<p style="margin: 0px; padding: 0px">[<a href="https://jsfiddle.net/ryunp/0kbm36eq/" target="_blank">JSFiddle</a>]</p>
<p>The goal is to pause the timer/interval when the tab focus is lost, and resume when activated.</p>
<p>Timers and interval callbacks continue triggering when a tab is backgrounded. Some behavior, like jQuery's '.fadeTo()' animation, gets buffered until the tab is reactivated which can lead to repeated 'machine gun' executions. When initially adding the animation callback for testing, the issue appeared. However, the issue no longer occurs. Haven't figured out why.</p>
<p>See what happens when toggling the PauseOnVisibilityChange flag and changing tabs. Check Dev Console for event notifications.</p>
</div>
<hr>
<div id="controls" style="text-align: center; display: flex; flex-wrap: wrap; justify-content: center;">
<button id="start">Start</button>
<button id="pause">Pause</button>
<button id="resume">Resume</button>
<button id="stop">Stop</button>
<label><input id="pause-toggle" type="checkbox" checked>PauseOnVisibilityChange</label>
<label><input id="jquery-toggle" type="checkbox" checked>jQuery Animation</label>
</div>
<hr>
<div style="height: 375px; display: flex; flex-wrap: wrap; flex-direction: row;">
<div style="flex-grow: 1; border: 1px dashed #aaa; ">
<div id="time">Interval: </div>
<div>Active Elapsed: <span id="elapsed">0ms</span></div>
<br>
<div id="properties" style="min-width: 320px;"></div>
</div>
<div id="event-log" style="flex-grow: 1; max-height: 375px; overflow-y: auto; border: 1px dashed #aaa; "></div>
</div>
</div>
CSS
th {
text-align: left;
}
#intro-text {
opacity: 1;
}
.transition {
transition: opacity 200ms;
}
#intro-text.faded {
opacity: 0.5;
}
JavaScript
class TabSafeHeartbeat {
constructor (intervalMs) {
this.enabled = false
this.executeCount = 0
this.intervalHandle = null
this.intervalMs = intervalMs
this.isPaused = false
this.lastExecutionTime = null
this.lastPauseTime = null
this.onTabVisibilityChangeFn = this.onTabVisibilityChange.bind(this)
this.pauseOnVisibilityChange = true
this.resumeOnVisibility = false
this.sleepTimeSinceLastExecute = 0
this.thresholdReachedHandlers = new Set()
this.timeoutHandle = null
if (this.pauseOnVisibilityChange)
this.enableTabVisibilityAware()
}
onTabVisibilityChange () {
if (document.visibilityState === 'visible') {
console.log("Tab activated")
if (this.resumeOnVisibility) {
this.resume()
}
} else {
console.log("Tab hidden")
if (this.enabled) {
if (this.isPaused) {
this.resumeOnVisibility = false
} else {
this.resumeOnVisibility = true
this.pause()
}
}
}
}
enableTabVisibilityAware () {
console.log("Toggling tab visibility awareness on")
this.pauseOnVisibilityChange = true
document.addEventListener("visibilitychange", this.onTabVisibilityChangeFn);
}
disableTabVisibilityAware () {
console.log("Toggling tab visibility awareness off")
this.pauseOnVisibilityChange = false
document.removeEventListener("visibilitychange", this.onTabVisibilityChangeFn);
}
start (intervalMs = null) {
if (intervalMs !== null) {
this.intervalMs = intervalMs
}
console.log(`Starting interval with ${this.intervalMs}ms`)
// Abort previous interval if already running
if (this.enabled) {
this.stop()
}
this.enabled = true
this.lastPauseTime = null
this.lastExecutionTime = Date.now()
this.sleepTimeSinceLastExecute = 0
this.intervalHandle = setInterval(this.onThresholdReached.bind(this), this.intervalMs)
}
resume () {
if...