iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

從 UI/UX 設計到前端實作:30 天打造專屬的任務學習計時器系列 第 16 篇

DAY 16|功能實作(四):精確計時(Date.now())與 background 防休眠

  • 分享至 

  • xImage
  •  

這幾天的功能實作中,順利完成了計時器的啟動、按鈕UI的切換,一切看起來都在完美運作。但我發現如果把網頁切換到別的分頁,計時器似乎變慢了。

所以這篇要來找出為什麼會變慢,以及如何透過 「時間戳記差值計算」 與 Page Visibility API 來改善目前的計時器。


為什麼 setInterval 會越計越慢?

瀏覽器為了省電與優化效能,有一套 頁面休眠與節能機制(Background Throttle):

  1. 分頁降頻:當使用者切換到其他頁面(分頁處於背景)時,瀏覽器會大幅降低背景分頁中 setInterval 或 setTimeout 的執行頻率(有時會從每 1000ms 觸發一次降至數秒甚至數分鐘才觸發一次)。

  2. 手機鎖屏休眠:在行動裝置上,螢幕關閉時作業系統會直接凍結 JavaScript 的執行。

如果按照原本的計時邏輯,依賴「每秒把 seconds 變數 +1」的方式,一旦 setInterval 停止或被降頻,seconds 就會停止增加,導致實際過了一小時,計時器卻只跑了 10 分鐘。

參考資料
1. Chrome Developers - Page Lifecycle API
2. MDN Web Docs - Page Visibility API

個人筆記:瀏覽器 | 頁面休眠與節能機制(Background Throttle)


解決方案:改用「時間戳記差值(Timestamp Delta)」

所謂的時間戳記差值(Timestamp Delta)是指兩個時間點相差多少。

為了解決這個問題,要改變以 setInterval 的計數次數為主的做法,變成以系統時間為基準。這樣才可以避免受到頁面休眠的影響。

新舊計時邏輯對比

  • ❌ 舊邏輯(累計法):總秒數 = 原有秒數 + 1 (受 setInterval 觸發次數影響)

  • ✅ 新邏輯(時間差法):總秒數 = 過去已累積秒數 + Math.floor((當前時間戳 - 開始時間戳) / 1000)


步驟 1:重構狀態變數與計時邏輯

調整 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)」。


步驟 2:更新啟動、暫停與重置函式

接著更新計時器的控制邏輯:

// 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,可以避免因為畫面更新延遲而出現「秒數跳格」的感覺。


引入 Page Visibility API 校正

雖然改用 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)?電腦世界的「大碼錶」


上一篇
DAY 15|功能實作(三):UI 按鈕狀態切換與暫停/結束控制
下一篇
DAY 17|功能實作(五):計時結束跳出Modal 彈窗
系列文
從 UI/UX 設計到前端實作:30 天打造專屬的任務學習計時器 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言