iT邦幫忙

2025 iThome 鐵人賽

DAY 29
0
Modern Web

Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具系列 第 29

Day 29:效能與最佳化:打造流暢的日誌與高頻率通知體驗

  • 分享至 

  • xImage
  •  

前言

戰友們,我們已經走到了這趟旅程的終點線前。我們的「通用 BLE 瑞士刀」已經打造完成,它鋒利、功能完備,足以應對絕大多數的 BLE 探索與互動任務。

但是,一個好的工具和一個偉大的工具之間,往往存在著一道名為「效能」的鴻溝。

想像一下,當我們訂閱一個高頻率的感測器(例如陀螺儀或加速計)時,通知數據可能會以每秒數十甚至上百次的頻率瘋狂湧入。我們目前的 log 函式和 UI 更新邏輯,每一次收到數據,都會去操作一次 DOM(appendChild 或更新 textContent)。這種頻繁的 DOM 操作,是網頁效能的頭號殺手,它會很快地佔滿瀏覽器的運算資源,導致介面卡頓、甚至崩潰。

一個專業級的偵錯工具,必須能夠從容地應對這種極端情況。因此,在我們為這趟旅程畫上句點之前,今天的最後一課,就是要為我們的工具進行一次效能最佳化手術。我們將學習前端開發中至關重要的最佳化技巧,確保我們的工具即使在數據的驚濤駭浪中,也能保持流暢與穩定。

內文

1. 效能的敵人:DOM 操作為何如此昂貴?

每次我們用 JavaScript 修改 DOM(例如 appendChild),瀏覽器都需要做很多背後工作:

  1. 重新計算佈局 (Reflow):計算元素的新尺寸和位置。

  2. 重新繪製 (Repaint):將元素的新外觀重新畫出來。

如果每秒做幾百次這樣的工作,瀏覽器的主執行緒就會被完全佔用,沒有時間去回應使用者的點擊或滾動,從而導致頁面「凍結」。

2. 最佳化 (一):為日誌系統引入緩衝區 (Buffering)

我們的策略是:不要每來一條日誌就去敲一次門(操作 DOM),而是先把日誌存起來(放在一個陣列緩衝區裡),等存到一定數量,或者過了一定時間後,再把這一批日誌一次性地敲門送進去。

修改 app.js 中的日誌系統:

// app.js

// --- [2] 日誌記錄函式 (重構版) ---
let logBuffer = [];     // 新增一個日誌緩衝區陣列
let logTimer = null;    // 新增一個計時器變數

function log(message) {
  // 1. 在開發者控制台總是即時印出
  console.log(message);
  
  // 2. 將訊息加入緩衝區
  const now = new Date();
  const timeString = now.toLocaleTimeString();
  logBuffer.push(`<strong>[${timeString}]</strong> ${message}`);
  
  // 3. 如果計時器不在運行中,就啟動一個
  if (!logTimer) {
    logTimer = setTimeout(() => {
      flushLogBuffer(); // 500 毫秒後,將緩衝區的內容一次性寫入 DOM
    }, 500); // 延遲時間可以根據需求調整
  }
}

function flushLogBuffer() {
  if (logBuffer.length > 0) {
    const logFragment = document.createDocumentFragment(); // 使用 DocumentFragment 提升效能
    
    logBuffer.forEach(message => {
      const logEntry = document.createElement('p');
      logEntry.innerHTML = message;
      logFragment.appendChild(logEntry);
    });
    
    logContainer.appendChild(logFragment);
    logContainer.scrollTop = logContainer.scrollHeight; // 滾動到底部
  }
  
  // 清空緩衝區並重置計時器
  logBuffer = [];
  logTimer = null;
}

程式碼解析:

  • 緩衝區log 函式現在只負責將訊息推入 logBuffer 陣列。

  • 計時器setTimeout 確保了 flushLogBuffer 函式最多每 500 毫秒才會被執行一次。

  • DocumentFragment:這是一個輕量級的「DOM 容器」,我們可以先把所有新的 p 元素都 appendChild 到這個 fragment 上(這是在記憶體中操作,非常快),然後再把這個 fragment 一次性地 appendChild 到真實的 logContainer 中。這樣,無論我們有多少條日誌,都只會引發一次昂貴的 Reflow 和 Repaint。

3. 最佳化 (二):節流 (Throttling) 特徵值更新

對於 Subscribe 收到的高頻數據,我們不需要顯示每一個中間值。人眼也無法分辨每秒更新 100 次和每秒更新 10 次的區別。我們可以使用「節流」技術來限制 UI 更新的頻率。

app.js 中新增一個 throttle 輔助函式:

// app.js

/**
 * 節流函式:確保一個函式在一定時間內最多只被執行一次
 * @param {Function} func - 要被節流的函式
 * @param {number} limit - 時間限制 (毫秒)
 * @returns {Function} - 包裝後的節流函式
 */
function throttle(func, limit) {
  let inThrottle;
  return function() {
    const args = arguments;
    const context = this;
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  }
}

接著,修改 renderCharacteristichandleNotifications 的部分:

// 在 renderCharacteristic 函式內部

// ...
let isSubscribed = false;

// 將 UI 更新的部分單獨抽離
const updateValueUI = (value) => {
  valueSpan.textContent = value;
  // (可選) 更新中央資料庫
  gattProfile.services[serviceUuid].characteristics[charInfo.uuid].value = value;
};

// 使用節流函式包裝我們的 UI 更新函式,限制為每 200 毫秒最多更新一次
const throttledUpdateUI = throttle(updateValueUI, 200);

const handleNotifications = (event) => {
  const valueDataView = event.target.value;
  // 我們的 parseValue 函式在這裡依然很有用!
  const parsedValue = parseValue(valueDataView); 
  const message = `[Notification] ${charInfo.uuid}: ${parsedValue}`;
  
  log(message); // 日誌依然可以即時記錄 (因為它已經被最佳化了)
  throttledUpdateUI(parsedValue); // 但 UI 的更新是經過節流的
};

// ...

程式碼解析:

  • 我們定義了一個經典的 throttle 函式。

  • 我們將真正更新 UI 的邏輯 updateValueUI 包裝成了一個節流版本 throttledUpdateUI

  • handleNotifications 中,我們總是呼叫 throttledUpdateUI。它會確保即使通知每秒來 100 次,valueSpan.textContent 也最多只會被修改 5 次(1000ms / 200ms)。


後續

今天,我們為這趟精彩的旅程,進行了最後、也是最重要的一次打磨。
明天,我們將迎來這趟 30 天鐵人賽的最終章。我們將不再撰寫新的程式碼,而是會進行一次全面的專案總結與回顧。我們將審視我們所建立的一切,鞏固所學的知識,並一起探討這把瑞士刀未來還能有哪些更酷的升級方向,為這段精彩的旅程,畫上一個完美的句號。


上一篇
Day 28:深入探索與強健性:讀取描述符與手動斷線
系列文
Web Bluetooth API 實戰:30 天打造通用 BLE 偵錯工具29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言