這幾天的功能實作中,順利完成了計時器的啟動、按鈕UI的切換,一切看起來都在完美運作。但我發現如果把網頁切換到別的分頁,計時器似乎變慢了。
所以這篇要來找出為什麼會變慢,以及如何透過 「時間戳記差值計算」 與 Page Visibility API 來改善目前的計時器。
瀏覽器為了省電與優化效能,有一套 頁面休眠與節能機制(Background Throttle):
分頁降頻:當使用者切換到其他頁面(分頁處於背景)時,瀏覽器會大幅降低背景分頁中 setInterval 或 setTimeout 的執行頻率(有時會從每 1000ms 觸發一次降至數秒甚至數分鐘才觸發一次)。
手機鎖屏休眠:在行動裝置上,螢幕關閉時作業系統會直接凍結 JavaScript 的執行。
如果按照原本的計時邏輯,依賴「每秒把 seconds 變數 +1」的方式,一旦 setInterval 停止或被降頻,seconds 就會停止增加,導致實際過了一小時,計時器卻只跑了 10 分鐘。
參考資料
1. Chrome Developers - Page Lifecycle API
2. MDN Web Docs - Page Visibility API
個人筆記:瀏覽器 | 頁面休眠與節能機制(Background Throttle)
所謂的時間戳記差值(Timestamp Delta)是指兩個時間點相差多少。
為了解決這個問題,要改變以 setInterval 的計數次數為主的做法,變成以系統時間為基準。這樣才可以避免受到頁面休眠的影響。
新舊計時邏輯對比
❌ 舊邏輯(累計法):總秒數 = 原有秒數 + 1 (受 setInterval 觸發次數影響)
✅ 新邏輯(時間差法):總秒數 = 過去已累積秒數 + Math.floor((當前時間戳 - 開始時間戳) / 1000)
調整 all.js 中的狀態變數,引入 startTime 與 elapsedTime:
// ===== 狀態變數重構 =====
let timerInterval = null;
let startTime = 0; // 紀錄當前這一段計時開始的時間戳記 (Date.now())
let elapsedTime = 0; // 紀錄之前已累積的毫秒數(例如暫停前累積的時間)
let isRunning = false;
// 取得目前總累積秒數
function getCalculatedSeconds() {
if (!isRunning) return Math.floor(elapsedTime / 1000);
// 當前累積時間 = 過去已累積時間 + (現在時間 - 本次開始時間)
const currentSessionMs = Date.now() - startTime;
return Math.floor((elapsedTime + currentSessionMs) / 1000);
}
備註Date.now() 與 elapsedTime 的時間單位都是「毫秒 (Milliseconds)」,要記得換算成「秒 (Seconds)」。
接著更新計時器的控制邏輯:
// 1. 開始計時
function startTimer() {
if (isRunning) return;
isRunning = true;
startTime = Date.now(); // 紀錄精確的開始時間點
// 更新 UI 顯示與按鈕組
btnStart.classList.add('d-none');
btnGroupActive.classList.remove('d-none');
// 每 200ms 觸發一次(提高頻率讓 UI 響應更流暢,但不影響精確度)
timerInterval = setInterval(() => {
const totalSeconds = getCalculatedSeconds();
timerDisplay.textContent = formatTime(totalSeconds);
}, 200);
}
// 2. 暫停 / 繼續切換
function togglePauseTimer() {
if (isRunning) {
// 進入暫停:把這次運行的時間加到 elapsedTime 存起來
elapsedTime += Date.now() - startTime;
clearInterval(timerInterval);
isRunning = false;
// UI 更新為「繼續」
btnPause.innerHTML = '<i class="bi bi-play-fill me-1"></i>繼續';
btnPause.classList.replace('btn-warning', 'btn-primary');
} else {
// 恢復計時:重新設定全新的 startTime
isRunning = true;
startTime = Date.now();
// UI 更新為「暫停」
btnPause.innerHTML = '<i class="bi bi-pause-fill me-1"></i>暫停';
btnPause.classList.replace('btn-primary', 'btn-warning');
timerInterval = setInterval(() => {
const totalSeconds = getCalculatedSeconds();
timerDisplay.textContent = formatTime(totalSeconds);
}, 200);
}
}
// 3. 結束/重置計時
function resetTimerUI() {
clearInterval(timerInterval);
isRunning = false;
startTime = 0;
elapsedTime = 0;
timerDisplay.textContent = formatTime(0);
btnStart.classList.remove('d-none');
btnGroupActive.classList.add('d-none');
// 按鈕樣式還原
btnPause.innerHTML = '<i class="bi bi-pause-fill me-1"></i>暫停';
btnPause.classList.replace('btn-primary', 'btn-warning');
}
備註
將 setInterval 的間隔從 1000ms 降到 200ms,可以避免因為畫面更新延遲而出現「秒數跳格」的感覺。
雖然改用 Date.now() 後,邏輯上的時間已經很精確,但當使用者切回瀏覽器分頁時,畫面上的數字可能會停在切出去的那一刻,直到下一次 setInterval 觸發才跳動。
我們可以監聽瀏覽器的 visibilitychange 事件,當使用者重新回到頁面時,立即強制刷一次 UI:
// 監聽頁面可見性改變(切換分頁 / 螢幕鎖屏喚醒)
document.addEventListener('visibilitychange', () => {
// 當頁面重新變為可見 (visible) 且計時器正在運作中時
if (document.visibilityState === 'visible' && isRunning) {
// 立即更新畫面上的時間顯示,免去等待 setInterval 的微小延遲
const currentSeconds = getCalculatedSeconds();
timerDisplay.textContent = formatTime(currentSeconds);
}
});
備註:document.visibilityState
visible:頁面目前處於前台分頁且螢幕開啟狀態。
hidden:頁面在背景分頁、最小化或手機處於鎖屏狀態。
經過了上面的優化,我們的計時器就可以不受瀏覽器休眠的影響,正確地紀錄任務時間了。
個人筆記:
瀏覽器 | 跟貓貓玩123木頭人:Page Visibility API(頁面可見性 API)
名詞介紹 | 什麼是毫秒時間戳(Timestamp)?電腦世界的「大碼錶」