No React. No virtual DOM. No framework lock-in. Drop it into any page and your elements stay in sync — tab visibility, network reconnects, scroll intersection, all handled.
let intervalId = null
let isVisible = true
let isOnline = navigator.onLine
let lastFetched = null
function fetchPrice() {
if (!isVisible || !isOnline) return
fetch('/api/price')
.then(r => r.json())
.then(data => {
document.querySelector('#price')
.textContent = data.price
lastFetched = Date.now()
})
.catch(console.error)
}
// Poll every 5 seconds
intervalId = setInterval(fetchPrice, 5000)
fetchPrice()
// Pause when tab is hidden
document.addEventListener('visibilitychange', () => {
isVisible = !document.hidden
if (!document.hidden) {
// Was it stale while hidden?
if (Date.now() - lastFetched > 5000) {
fetchPrice()
}
}
})
// Refetch on network restore
window.addEventListener('online', () => {
isOnline = true
fetchPrice()
})
window.addEventListener('offline', () => {
isOnline = false
})
// Pause when scrolled out
const io = new IntersectionObserver(([e]) => {
isVisible = e.isIntersecting
if (e.isIntersecting) fetchPrice()
})
io.observe(document.querySelector('#price'))
// Remember to clean up (you'll forget)
function destroy() {
clearInterval(intervalId)
io.disconnect()
// ...remove event listeners
}
import { stale } from 'stale'
const unsub = stale('#price', {
ttl: '5s',
refetch: () =>
fetch('/api/price').then(r => r.json()),
update: (el, data) => {
el.textContent = data.price
},
})
// That's it. All of this is automatic:
// ✓ TTL expires → refetch
// ✓ Tab hidden → pause TTL clock
// ✓ Tab regains focus → refetch if stale
// ✓ Network offline → pause
// ✓ Back online → refetch immediately
// ✓ Scroll out → pause TTL clock
// ✓ Scroll back in → resume + refetch
// ✓ DOM removed → full cleanup
unsub() // cleanup everything
Three cards running at different TTLs (4s / 6s / 8s) so you can watch them update independently. Each has a sparkline, freshness indicator, and draining TTL bar.
Switch to another tab and come back. The timeline records exactly when polling was active vs. paused — no wasted requests while you're away.
Toggle the simulated network. When you go back online, stale() fires a refetch immediately — no waiting for the next TTL cycle.
These cards only poll when visible. Scroll them out of view and the counter drops — zero background work for off-screen elements.