博客 / 詳情

返回

神級JS API,誰用誰好用

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

1. ResizeObserver

ResizeObserver 是一個瀏覽器原生的 JavaScript API,用於監聽 DOM 元素尺寸的變化。它類似於 MutationObserver,但專門用於觀察元素的大小(寬高)變化,而無需依賴 window.resize 事件(後者只對視口變化有效)。

🧩 基本用法

const resizeObserver = new ResizeObserver(entries => {
  for (let entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`元素尺寸:${width} x ${height}`);
    
    // entry.target 是被觀察的 DOM 元素
    console.log('目標元素:', entry.target);
  }
});

// 開始觀察某個元素
resizeObserver.observe(document.querySelector('#my-element'));

// 可選:觀察多個元素
// resizeObserver.observe(element1);
// resizeObserver.observe(element2);

📦 entry.contentRect vs getBoundingClientRect()

  • entry.contentRect:表示內容區域(不包括 padding、border、margin),類似於 getComputedStyle().width/height 的計算結果。
  • 如果你需要包括 border 和 padding 的尺寸,可以結合 entry.target.getBoundingClientRect() 使用。

🛑 停止觀察

// 停止觀察某個元素
resizeObserver.unobserve(element);

// 停止觀察所有元素並釋放資源
resizeObserver.disconnect();

建議:在組件銷燬(如 React 的 useEffect 清理函數、Vue 的 onBeforeUnmount)時調用 disconnect(),避免內存泄漏。

✅ 使用場景

  1. 響應式組件:當容器尺寸變化時動態調整子元素(如圖表、Canvas、視頻)。
  2. 自定義滾動條或佈局:監聽內容區域變化以更新 UI。
  3. 替代 window.onresize:更精確地響應特定元素的尺寸變化,而非整個窗口。
  4. Web Components / 封裝組件:內部自動適配父容器大小。

🌐 瀏覽器兼容性

  • ✅ Chrome 64+
  • ✅ Firefox 69+
  • ✅ Safari 13.1+
  • ✅ Edge 79+
  • ❌ IE 不支持(需 polyfill)

兼容性已非常廣泛,現代項目可放心使用。

🛠️ Polyfill(如需支持舊瀏覽器)

可通過 GitHub - juggle/resize-observer 提供的 polyfill:

npm install @juggle/resize-observer
import ResizeObserver from '@juggle/resize-observer';

// 如果原生不支持,則使用 polyfill
if (!window.ResizeObserver) {
  window.ResizeObserver = ResizeObserver;
}

示例:React 中使用

import { useEffect, useRef } from 'react';

function MyComponent() {
  const containerRef = useRef(null);

  useEffect(() => {
    const observer = new ResizeObserver(entries => {
      for (let entry of entries) {
        console.log('新寬度:', entry.contentRect.width);
      }
    });

    if (containerRef.current) {
      observer.observe(containerRef.current);
    }

    return () => {
      observer.disconnect(); // 清理
    };
  }, []);

  return <div ref={containerRef}>可變尺寸容器</div>;
}

2.IntersectionObserver

IntersectionObserver 是一個強大的瀏覽器原生 API,用於異步監聽目標元素與祖先元素(或視口)的交叉(相交)狀態變化。它常用於實現懶加載、無限滾動、曝光統計、動畫觸發等場景,性能遠優於傳統的 scroll 事件監聽。

🧩 基本用法

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    // entry.target:被觀察的 DOM 元素
    // entry.isIntersecting:是否與根(viewport 或 root)相交
    // entry.intersectionRatio:相交區域佔目標元素的比例(0 ~ 1)
    // entry.intersectionRect:相交區域的矩形信息
    // entry.boundingClientRect:目標元素相對於視口的位置
    // entry.rootBounds:根元素的邊界(通常是視口)

    if (entry.isIntersecting) {
      console.log('元素進入視口:', entry.target);
      // 例如:加載圖片、觸發動畫
    } else {
      console.log('元素離開視口');
    }
  });
});

// 開始觀察某個元素
observer.observe(document.querySelector('#my-element'));

⚙️ 配置選項(可選)

const options = {
  root: null, // 默認為視口(viewport);可設為某個祖先元素
  rootMargin: '0px', // 類似 CSS margin,擴展或收縮根的邊界(支持負值)
  threshold: 0.5 // 觸發回調的相交比例閾值(0 ~ 1),可為數字或數組
};

const observer = new IntersectionObserver(callback, options);

threshold 示例:

  • threshold: 0:只要有一點進入就觸發(默認)。
  • threshold: 1:完全進入才觸發。
  • threshold: [0, 0.25, 0.5, 0.75, 1]:在 0%、25%、50%... 時都觸發。

🛑 停止觀察

observer.unobserve(element); // 停止單個元素
observer.disconnect();      // 停止所有並釋放資源

建議:在組件銷燬時調用 disconnect(),防止內存泄漏。

✅ 典型應用場景

1. 圖片懶加載

const imgObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src; // 從 data-src 加載真實圖片
      imgObserver.unobserve(img); // 加載後停止觀察
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  imgObserver.observe(img);
});

2. 滾動到底部自動加載(無限滾動)

觀察一個“哨兵”元素(如分頁加載提示),當它進入視口時觸發加載。

3. 曝光埋點 / 廣告可見性統計

當廣告或內容區域進入視口一定比例時,上報“曝光”事件。

4. 滾動動畫(如 AOS 效果)

元素進入視口時添加 CSS 動畫類。

🌐 瀏覽器兼容性

  • ✅ Chrome 51+
  • ✅ Firefox 55+
  • ✅ Safari 12.1+
  • ✅ Edge 15+
  • ❌ IE 不支持(需 polyfill)

現代瀏覽器支持良好,移動端也廣泛可用。

🛠️ Polyfill(兼容舊瀏覽器)

官方推薦 polyfill(由 W3C 團隊維護):

npm install intersection-observer
// 在應用入口引入(自動填充 window.IntersectionObserver)
import 'intersection-observer';

注意:polyfill 會回退到 scroll + getBoundingClientRect(),性能較差,僅用於兼容。


💡 與 ResizeObserver / MutationObserver 對比

ScreenShot_2026-01-05_104413_685

 

三者互補,常結合使用。

📌 小技巧

  • 使用 rootMargin: '100px' 可以提前觸發(在元素距離視口還有 100px 時就加載)。
  • 在 <img loading="lazy"> 普及的今天,簡單圖片懶加載可直接用 HTML 屬性,但複雜邏輯仍需 IntersectionObserver

3.Page Visibility

Page Visibility API 是一個瀏覽器原生 API,用於檢測當前網頁是否對用户可見(即是否處於前台標籤頁或被最小化/切換到後台)。它可以幫助開發者優化性能、節省資源,或實現特定業務邏輯(如暫停視頻、停止輪詢、統計停留時長等)。


🧩 核心屬性與事件

1. document.visibilityState

返回當前頁面的可見性狀態,可能值包括:

ScreenShot_2026-01-05_104754_463

實際開發中主要關注 'visible''hidden'

2. document.hidden(已廢棄,建議用 visibilityState

  • true:頁面不可見
  • false:頁面可見

⚠️ 雖仍可用,但 MDN 建議使用 visibilityState

3. visibilitychange 事件

當頁面可見性狀態改變時觸發。

✅ 基本用法示例

function handleVisibilityChange() {
  if (document.visibilityState === 'visible') {
    console.log('頁面回到前台');
    // 恢復視頻播放、重啓定時器、刷新數據等
  } else if (document.visibilityState === 'hidden') {
    console.log('頁面進入後台');
    // 暫停視頻、停止輪詢、保存狀態等
  }
}

// 監聽可見性變化
document.addEventListener('visibilitychange', handleVisibilityChange);

🌟 典型應用場景

1. 暫停/恢復媒體播放

const video = document.querySelector('video');

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    video.pause();
  } else {
    video.play();
  }
});

2. 停止不必要的輪詢或定時任務

let intervalId;

function startPolling() {
  intervalId = setInterval(fetchData, 5000);
}

function stopPolling() {
  clearInterval(intervalId);
}

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    stopPolling();
  } else {
    startPolling();
  }
});

startPolling(); // 初始啓動

3. 用户停留時長統計

let startTime = Date.now();
let totalVisibleTime = 0;

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    totalVisibleTime += Date.now() - startTime;
  } else {
    startTime = Date.now();
  }
});

// 頁面卸載時上報總可見時長
window.addEventListener('beforeunload', () => {
  totalVisibleTime += Date.now() - startTime;
  sendToAnalytics({ visibleTime: totalVisibleTime });
});

4. 節省資源(如 Canvas 動畫、WebGL)

在頁面不可見時暫停渲染循環,減少 CPU/GPU 消耗。

🌐 瀏覽器兼容性

  • ✅ Chrome 13+
  • ✅ Firefox 10+
  • ✅ Safari 7+
  • ✅ Edge 12+
  • ✅ iOS Safari / Android Browser(現代版本)

兼容性極佳,幾乎所有現代瀏覽器都支持。

⚠️ 注意事項

  • 不保證精確性:在某些系統(如 macOS 快速切換)中,狀態切換可能有微小延遲。

  • 不是用户活躍度檢測:頁面可見 ≠ 用户正在看(用户可能切到其他應用但瀏覽器窗口仍在前台)。

  • 與 blur/focus 事件的區別

    • window.onfocus / window.onblur:監聽窗口焦點(如切換到其他應用)。
    • visibilitychange:監聽標籤頁是否可見(即使窗口有焦點,但標籤頁在後台也算 hidden)。
    • 兩者可結合使用以獲得更全面的狀態判斷。

🔍 擴展:結合 focus/blur 更精準判斷

let isPageVisible = !document.hidden;
let isWindowFocused = !document.hasFocus();

window.addEventListener('focus', () => {
  isWindowFocused = true;
  if (isPageVisible) {
    console.log('用户很可能正在看頁面');
  }
});

window.addEventListener('blur', () => {
  isWindowFocused = false;
});

document.addEventListener('visibilitychange', () => {
  isPageVisible = !document.hidden;
});

4.Web Share API

Web Share API 是一個現代瀏覽器提供的原生 API,允許網頁調用操作系統級別的分享功能,讓用户將內容(如鏈接、文本、標題等)快速分享到設備上安裝的其他應用(如微信、郵件、短信、筆記等)。

✅ 基本用法

if (navigator.share) {
  navigator.share({
    title: '分享標題',
    text: '分享的描述文字',
    url: 'https://example.com'
  })
  .then(() => {
    console.log('分享成功');
  })
  .catch((error) => {
    if (error.name === 'AbortError') {
      console.log('用户取消了分享');
    } else {
      console.error('分享失敗:', error);
    }
  });
} else {
  // 回退方案:顯示自定義分享按鈕或提示
  alert('您的瀏覽器不支持 Web Share API,請手動複製鏈接');
}

⚠️ 必須在用户手勢觸發的上下文中調用(如點擊事件),否則會拋出安全錯誤。


🔐 安全與限制

  • 僅限安全上下文:必須在 HTTPS(或 localhost)下使用。
  • 用户手勢要求:只能在 clicktouchend 等用户操作回調中調用。
  • 字段非全部必需:但至少要提供 titletexturl 中的一個(推薦提供 url)。
  • 無法控制目標應用:分享目標由操作系統決定,開發者無法指定(如“只分享到微信”)。

📱 支持情況(截至 2025 年)

ScreenShot_2026-01-05_105000_853

🧩 高級用法:分享文件(Web Share API Level 2)

現代瀏覽器(Chrome 89+ 等)支持分享文件(如圖片、PDF):


if (navigator.canShare && navigator.canShare({ files: [file] })) {
  await navigator.share({
    title: '圖片分享',
    files: [file] // File 對象數組
  });
}

注意:文件必須來自用户選擇(如 <input type="file">)或由網頁生成,不能是任意網絡文件。

🔄 回退方案(Fallback)

當不支持 Web Share 時,可提供複製鏈接或自定義分享按鈕:

function fallbackShare(url) {
  const input = document.createElement('input');
  input.value = url;
  document.body.appendChild(input);
  input.select();
  document.execCommand('copy');
  document.body.removeChild(input);
  alert('鏈接已複製到剪貼板');
}

📦 在框架中使用(React 示例)

function ShareButton({ url, title, text }) {
  const handleShare = async () => {
    if (navigator.share) {
      try {
        await navigator.share({ url, title, text });
      } catch (err) {
        console.warn('分享被取消或失敗', err);
      }
    } else {
      fallbackShare(url);
    }
  };

  return (
    <button onClick={handleShare}>
      分享
    </button>
  );
}

🚀 優勢

  • 原生體驗:使用系統分享面板,用户熟悉且支持所有已安裝應用。
  • 無需第三方 SDK:避免集成微信、微博等 SDK 的複雜性。
  • 隱私友好:不收集用户分享行為數據(除非你自己上報)。

📌 小貼士

  • 測試時可在 Chrome DevTools 的 Device Mode(設備模擬)  中查看分享彈窗。
  • 在 PWA 中使用效果最佳,可實現“類原生”分享體驗。

5. Wake Lock

Wake Lock API 是一個現代 Web API,允許網頁防止設備進入休眠狀態(如屏幕變暗、鎖屏),常用於需要長時間保持活躍的場景,例如:

  • 視頻播放器(避免播放時屏幕關閉)
  • 導航應用(持續顯示路線)
  • 掃碼/AR 應用(保持攝像頭活躍)
  • 閲讀器/電子書(長時間閲讀不鎖屏)

🔒 兩種鎖類型(目前主要支持 screen


// 1. Screen Wake Lock(屏幕喚醒鎖) ← 當前唯一廣泛支持的類型
// 2. System Wake Lock(系統喚醒鎖) ← 尚未標準化,基本不可用

目前 只有 screen 類型 在主流瀏覽器中可用。


✅ 基本用法(Screen Wake Lock)

let wakeLock = null;

async function requestWakeLock() {
  try {
    // 請求屏幕喚醒鎖
    wakeLock = await navigator.wakeLock.request('screen');
    console.log('Wake Lock 已激活');

    // 監聽釋放事件(如頁面隱藏、用户鎖屏)
    wakeLock.addEventListener('release', () => {
      console.log('Wake Lock 已釋放');
    });

  } catch (err) {
    console.error('Wake Lock 請求失敗:', err);
  }
}

// 在用户交互後調用(如點擊按鈕)
document.getElementById('keepAwakeBtn').addEventListener('click', requestWakeLock);

⚠️ 必須由用户手勢觸發(如 click),不能在頁面加載時自動請求。


🛑 釋放鎖(可選,通常自動釋放)

if (wakeLock) {
  await wakeLock.release(); // 顯式釋放
  wakeLock = null;
}

鎖會在以下情況自動釋放:

  • 頁面進入後台(visibilitychange → hidden
  • 瀏覽器標籤頁關閉
  • 用户手動鎖屏
  • 頁面失去焦點(部分瀏覽器)

🌐 瀏覽器兼容性(截至 2025 年)

ScreenShot_2026-01-05_105153_319

 

移動端 Chrome(Android)支持最好,iOS Safari 完全不支持

可通過 caniuse.com/wake-lock 查看最新狀態。


🛡️ 安全與權限要求

  • 必須在 HTTPS 下使用(localhost 除外)
  • 必須由用户手勢觸發(如點擊、觸摸)
  • 僅在頁面可見時有效(頁面切到後台會自動釋放)
  • 不會繞過系統鎖屏密碼,僅防止屏幕變暗/休眠

💡 實際應用場景示例

場景:視頻播放時不鎖屏

const video = document.querySelector('video');

video.addEventListener('play', async () => {
  if ('wakeLock' in navigator) {
    try {
      wakeLock = await navigator.wakeLock.request('screen');
    } catch (err) {
      console.warn('無法保持屏幕常亮:', err);
    }
  }
});

video.addEventListener('pause', () => {
  if (wakeLock) wakeLock.release();
});

場景:結合 Page Visibility 自動管理

document.addEventListener('visibilitychange', () => {
  if (document.hidden && wakeLock) {
    wakeLock.release(); // 頁面隱藏時主動釋放
  }
});

🔄 降級方案(Fallback)

在不支持 Wake Lock 的環境(如 iOS):

  • 提示用户“請手動關閉自動鎖屏”
  • 使用全屏 API(requestFullscreen())有時可延長屏幕活躍時間(非可靠)
  • 對於視頻,可嘗試使用 <video playsinline webkit-playsinline> 等屬性優化體驗

📌 注意事項

  • 不要濫用:長時間保持喚醒會顯著增加耗電。
  • 始終提供關閉選項:讓用户能手動禁用“保持喚醒”。
  • 測試真實設備:模擬器行為可能與真機不同。

🔍 檢測是否支持

if ('wakeLock' in navigator) {
  // 支持 Wake Lock API
}

6. Broadcast Channel

BroadcastChannel 是一個現代 Web API,允許同源(same-origin)的不同瀏覽器上下文(如多個標籤頁、iframe、Web Worker)之間進行簡單、高效的跨文檔通信。 它類似於“發佈-訂閲”模式:一個上下文發送消息,所有監聽同一頻道的其他上下文都能收到。


🧩 基本用法

1. 創建頻道並監聽消息

// 所有頁面/worker 使用相同的頻道名
const channel = new BroadcastChannel('my-app-channel');

// 監聽來自其他上下文的消息
channel.addEventListener('message', (event) => {
  console.log('收到消息:', event.data);
});

// 或使用 onmessage
// channel.onmessage = (event) => { ... };

2. 發送消息

// 任意同源頁面或 worker 中
channel.postMessage({ type: 'USER_LOGIN', userId: 123 });

3. 關閉頻道(可選,推薦在頁面卸載時調用)

window.addEventListener('beforeunload', () => {
  channel.close(); // 釋放資源
});

自動廣播:消息會發送給所有監聽 'my-app-channel' 的同源上下文(包括髮送者自己,除非你過濾)。


🔐 安全限制

  • 同源策略:只有協議 + 域名 + 端口完全相同的頁面才能通信。

    • https://example.com/page1 和 https://example.com/page2 ✅
    • https://example.com 和 https://sub.example.com ❌
    • http://localhost:3000 和 http://localhost:8080 ❌
  • 不支持跨域:不能用於跨域 iframe 通信(此時應考慮 postMessage + origin 驗證)。


✅ 典型應用場景

1. 用户登錄/登出同步

當用户在一個標籤頁登錄,其他標籤頁自動更新狀態:

// 登錄頁
channel.postMessage({ type: 'AUTH_CHANGED', user: { id: 1, name: 'Alice' } });

// 其他頁面
channel.onmessage = (e) => {
  if (e.data.type === 'AUTH_CHANGED') {
    if (e.data.user) {
      updateUI(e.data.user); // 顯示用户信息
    } else {
      logoutAllTabs(); // 用户登出
    }
  }
};

2. 多標籤頁狀態同步

  • 購物車變更
  • 主題切換(深色/淺色模式)
  • 語言切換

3. 通知其他標籤頁刷新數據

例如後台管理頁更新後,通知前台頁面重新拉取配置。

4. 與 Web Worker 通信

主線程和多個 worker 可通過 BroadcastChannel 廣播消息。


🌐 瀏覽器兼容性(截至 2025 年)

ScreenShot_2026-01-05_105603_663

⚠️ Safari 在 15.4 之前完全不支持,如需兼容舊版 iOS,需使用 localStorage + storage 事件作為 fallback。


🔄 降級方案(Fallback for older browsers)

利用 localStoragestorage 事件實現類似廣播:


// 發送消息(fallback)
function broadcastFallback(message) {
  localStorage.setItem('broadcast-msg', JSON.stringify({
    ...message,
    timestamp: Date.now()
  }));
}

// 接收消息(其他標籤頁會觸發 storage 事件)
window.addEventListener('storage', (e) => {
  if (e.key === 'broadcast-msg') {
    const message = JSON.parse(e.newValue);
    console.log('Fallback 收到:', message);
  }
});

缺點:只能傳遞字符串,且 storage 事件不會在當前標籤頁觸發(正好避免自己收到自己發的消息)。


🆚 與其他通信方式對比

ScreenShot_2026-01-05_105637_931

💡 小技巧

  • 避免無限循環:如果多個頁面都響應消息並再次廣播,可能形成循環。建議使用 type 字段區分消息來源或添加防重機制。
  • 結構化克隆postMessage 支持傳輸 ArrayBufferBlobMap 等(遵循結構化克隆算法),不只是 JSON。

📦 在框架中使用(React 示例)

import { useEffect } from 'react';

function useBroadcastChannel(channelName, onMessage) {
  useEffect(() => {
    const channel = new BroadcastChannel(channelName);
    channel.onmessage = onMessage;

    return () => {
      channel.close();
    };
  }, [channelName, onMessage]);
}

// 使用
function App() {
  useBroadcastChannel('theme-channel', (e) => {
    if (e.data.type === 'THEME_CHANGE') {
      document.body.className = e.data.theme;
    }
  });

  const changeTheme = (theme) => {
    new BroadcastChannel('theme-channel').postMessage({
      type: 'THEME_CHANGE',
      theme
    });
  };

  return <button onClick={() => changeTheme('dark')}>切換深色</button>;
}

BroadcastChannel和 Vuex / Redux

🔍 核心區別

ScreenShot_2026-01-05_105709_802

 

🧩 舉個例子説明差異

場景:用户登錄後,所有打開的標籤頁都要顯示用户名
  • BroadcastChannel

    • 標籤頁 A 登錄 → 通過 channel.postMessage({ type: 'LOGIN', user }) 廣播。
    • 標籤頁 B、C(即使沒用 Vue/React)監聽到消息 → 各自更新自己的 UI
    • 每個頁面獨立維護自己的狀態,只是通過消息“同步”了登錄事件。
  • 用 Vuex

    • 只在當前標籤頁內,多個 Vue 組件共享 store.state.user
    • 標籤頁 A 的 Vuex 無法直接影響標籤頁 B 的 Vuex。
    • 如果你打開兩個標籤頁,它們有兩個完全獨立的 Vuex 實例

✅ 所以:Vuex 管“頁面內”,BroadcastChannel 管“頁面間”


🤝 它們可以結合使用!

實際項目中,兩者常配合使用


// 在 Vuex 的 action 中監聽 BroadcastChannel
const channel = new BroadcastChannel('auth-channel');

const store = new Vuex.Store({
  state: { user: null },
  mutations: {
    SET_USER(state, user) {
      state.user = user;
    }
  },
  actions: {
    login({ commit }, user) {
      commit('SET_USER', user);
      // 登錄後廣播給其他標籤頁
      channel.postMessage({ type: 'LOGIN', user });
    }
  }
});

// 監聽其他標籤頁的登錄/登出
channel.onmessage = (e) => {
  if (e.data.type === 'LOGIN') {
    store.commit('SET_USER', e.data.user); // 更新當前頁狀態
  } else if (e.data.type === 'LOGOUT') {
    store.commit('SET_USER', null);
  }
};

這樣:

  • 頁面內:Vuex 管理狀態,組件自動響應。
  • 頁面間:BroadcastChannel 同步關鍵事件。

❓那有沒有“跨標籤頁的 Vuex”?

有!社區有一些庫嘗試結合兩者,例如:

  • vuex-shared-mutations:通過 localStorage 或 BroadcastChannel 同步 Vuex 的 mutations。
  • 自定義方案:監聽 storage 事件或 BroadcastChannel,觸發本地 store 更新。

但核心思想不變:跨標籤頁通信靠 BroadcastChannel(或 storage),狀態管理靠 Vuex


✅ 總結

ScreenShot_2026-01-05_105747_818

 

7. PerformanceObserver

PerformanceObserver 是一個強大的 Web API,用於異步監聽性能相關的事件和指標,而無需輪詢 performance.getEntries()。它是現代 Web 性能監控(如 Core Web Vitals)的核心工具。


🎯 核心作用

監聽瀏覽器自動記錄的 Performance Timeline(性能時間線) 中的新條目,例如:

  • 資源加載(resource
  • 導航 timing(navigation
  • 長任務(longtask
  • 元素曝光(element,實驗性)
  • 最重要:  CLS、LCP、FCP、INP 等 Web Vitals 指標

🧩 基本用法

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.entryType, entry.startTime, entry.duration);
  }
});

// 開始監聽特定類型的性能條目
observer.observe({ entryTypes: ['resource', 'navigation', 'paint'] });

⚠️ 必須指定 entryTypes(或 type),否則不會觸發回調。


🔍 常見 entryTypes 及用途

ScreenShot_2026-01-05_105840_602

✅ LCP、CLS、INP 等現代指標必須通過 PerformanceObserver 獲取,無法通過 getEntries() 靜態讀取。


✅ 實戰示例

1. 監聽 LCP(最大內容繪製)

let lcpReported = false;

new PerformanceObserver((entryList) => {
  const lcpEntry = entryList.getEntries().at(-1); // 取最後一個(最準確)
  if (!lcpReported) {
    console.log('LCP:', lcpEntry.startTime); // 單位:毫秒
    // 上報到分析平台
    sendToAnalytics({ metric: 'LCP', value: lcpEntry.startTime });
    lcpReported = true;
  }
}).observe({ type: 'largest-contentful-paint', buffered: true });

buffered: true 表示獲取已發生但未被觀察到的歷史條目(對 LCP/CLS 必須加!)。


2. 監聽 CLS(累積佈局偏移)

let clsValue = 0;

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) { // 忽略用户交互後的偏移
      clsValue += entry.value;
    }
  }
  console.log('當前 CLS:', clsValue);
}).observe({ type: 'layout-shift', buffered: true });

3. 監控慢資源加載

new PerformanceObserver((list) => {
  for (const resource of list.getEntries()) {
    if (resource.duration > 2000) {
      console.warn('慢資源:', resource.name, resource.duration + 'ms');
      // 上報性能問題
    }
  }
}).observe({ entryTypes: ['resource'] });

4. 捕獲長任務(卡頓原因)

new PerformanceObserver((list) => {
  for (const task of list.getEntries()) {
    if (task.duration > 100) {
      console.log('長任務:', task.duration + 'ms', task.attribution);
    }
  }
}).observe({ entryTypes: ['longtask'] });

需要先註冊長任務支持(部分瀏覽器需 polyfill):

if (PerformanceObserver.supportedEntryTypes.includes('longtask')) {
  // 啓用觀察
}

🌐 瀏覽器兼容性

  • ✅ Chrome / Edge:全面支持(包括 Web Vitals)
  • ✅ Firefox:支持基礎類型(resourcenavigation),Web Vitals 支持較弱
  • ✅ Safari 15+:支持 LCP、CLS、FCP 等核心指標
  • ❌ IE:不支持

推薦使用 Google 的 web-vitals 庫 跨瀏覽器採集 Core Web Vitals。


📦 與 performance.getEntries() 對比

ScreenShot_2026-01-05_110013_030

現代性能監控應優先使用 PerformanceObserver


🚀 最佳實踐

  1. 儘早註冊:在 <head> 中或頁面頂部初始化,避免漏掉早期指標。
  2. 使用 buffered: true:確保捕獲 FCP、LCP、CLS 等可能在監聽前已發生的指標。
  3. 避免內存泄漏:通常不需要 disconnect(),因為性能條目是一次性的。
  4. 結合 RUM(真實用户監控) :將數據上報到分析平台(如 GA4、Sentry、自建服務)。

🛠️ 工具推薦

  • web-vitals npm 包:Google 官方封裝,一行代碼獲取 Web Vitals。
import { getLCP, getCLS, getFCP } from 'web-vitals';
getLCP(console.log);

React(使用 Hook) 和 Vue 3(使用 Composition API)

✅ 共同前提

我們使用 Google 官方的 web-vitals 庫,它已封裝好 PerformanceObserver 的兼容邏輯。

npm install web-vitals

🟦 React 版本:useWebVitals

// hooks/useWebVitals.ts
import { useEffect } from 'react';
import { getCLS, getFCP, getLCP, getFID, getINP } from 'web-vitals';

type WebVitalsMetric = {
  id: string;
  name: string;
  value: number;
  delta: number;
  entries: PerformanceEntry[];
  attribution: Record<string, unknown>;
};

type WebVitalsOptions = {
  onReport?: (metric: WebVitalsMetric) => void;
  reportAll?: boolean; // 是否上報所有指標(默認只上報一次)
};

export const useWebVitals = ({
  onReport,
  reportAll = false
}: WebVitalsOptions = {}) => {
  useEffect(() => {
    // 定義上報函數
    const report = (metric: WebVitalsMetric) => {
      onReport?.(metric);
      if (process.env.NODE_ENV === 'development') {
        console.log('Web Vitals:', metric);
      }
    };

    // 啓動監聽(Web Vitals 內部使用 PerformanceObserver)
    getCLS(report, reportAll);
    getFCP(report, reportAll);
    getLCP(report, reportAll);
    getFID(report); // FID 只觸發一次
    getINP(report, reportAll); // INP 替代 FID(未來標準)

    // 注意:web-vitals 的指標是自動管理生命週期的,無需 cleanup
  }, [onReport, reportAll]);
};

📌 使用示例

// App.tsx
import { useWebVitals } from './hooks/useWebVitals';

function App() {
  useWebVitals({
    onReport: (metric) => {
      // 上報到分析平台(如 GA4、Sentry、自建 API)
      fetch('/api/performance', {
        method: 'POST',
        body: JSON.stringify(metric),
        headers: { 'Content-Type': 'application/json' }
      });
    }
  });

  return <div>你的應用</div>;
}

✅ 優點:自動處理瀏覽器兼容性、只上報有效指標、支持開發環境日誌。


🟩 Vue 3 版本:useWebVitals

// composables/useWebVitals.ts
import { onMounted } from 'vue';
import { getCLS, getFCP, getLCP, getFID, getINP } from 'web-vitals';

type WebVitalsMetric = {
  id: string;
  name: string;
  value: number;
  delta: number;
  entries: PerformanceEntry[];
  attribution: Record<string, unknown>;
};

export function useWebVitals(
  onReport?: (metric: WebVitalsMetric) => void,
  reportAll = false
) {
  onMounted(() => {
    const report = (metric: WebVitalsMetric) => {
      onReport?.(metric);
      if (import.meta.env.DEV) {
        console.log('Web Vitals:', metric);
      }
    };

    getCLS(report, reportAll);
    getFCP(report, reportAll);
    getLCP(report, reportAll);
    getFID(report);
    getINP(report, reportAll);
  });
}

📌 使用示例

<!-- App.vue -->
<script setup>
import { useWebVitals } from './composables/useWebVitals';

useWebVitals((metric) => {
  fetch('/api/performance', {
    method: 'POST',
    body: JSON.stringify(metric),
    headers: { 'Content-Type': 'application/json' }
  });
});
</script>

<template>
  <div>你的應用</div>
</template>

🧩 高級:監控慢資源加載(自定義 PerformanceObserver)

如果你還想監控 JS/CSS/圖片等資源加載性能,可以額外封裝一個 Hook:

React: useResourcePerformance

// hooks/useResourcePerformance.ts
import { useEffect } from 'react';

export const useResourcePerformance = (onSlowResource: (entry: PerformanceResourceTiming) => void) => {
  useEffect(() => {
    if (!PerformanceObserver.supportedEntryTypes.includes('resource')) return;

    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries() as PerformanceResourceTiming[]) {
        if (entry.duration > 2000) {
          onSlowResource(entry);
        }
      }
    });

    observer.observe({ entryTypes: ['resource'] });

    return () => {
      observer.disconnect();
    };
  }, [onSlowResource]);
};

ue 版本類似,用 onMounted + onUnmounted 管理生命週期。


📊 上報建議

  • LCP、FCP、CLS:每個頁面會話上報一次(reportAll: false)。
  • INP/FID:用户每次交互可能觸發,可採樣上報。
  • 慢資源:可聚合後批量上報,避免頻繁請求。

🚀 部署提示

  • 在 生產環境 使用,開發環境僅用於調試。
  • 避免阻塞主渲染邏輯(web-vitals 是異步非阻塞的)。
  • 配合 Google Analytics 4 的 Web Vitals 自動採集 更省事。

8. requestIdleCallback

requestIdleCallback 是一個瀏覽器提供的 API,用於在瀏覽器主線程空閒時執行低優先級任務,避免影響關鍵操作(如用户輸入、動畫、佈局等),從而提升頁面流暢性和響應性。

💡 它是實現“協作式調度(Cooperative Scheduling) ”的關鍵工具,React 16+ 的 Fiber 架構就受其啓發(儘管 React 最終未直接使用它)。


🧩 基本用法

function doLowPriorityWork(deadline) {
  // deadline.timeRemaining():返回當前空閒時段還剩多少毫秒(通常 < 50ms)
  // deadline.didTimeout:是否因超時而強制執行(配合 timeout 使用)

  while (deadline.timeRemaining() > 0 || deadline.didTimeout) {
    if (hasWork()) {
      performUnitOfWork();
    } else {
      break; // 沒有更多工作,退出
    }
  }

  // 如果還有剩餘任務,繼續調度
  if (hasMoreWork()) {
    requestIdleCallback(doLowPriorityWork);
  }
}

// 啓動任務
requestIdleCallback(doLowPriorityWork, { timeout: 2000 });

⚙️ 參數説明

1. 回調函數參數:deadline

  • deadline.timeRemaining():返回一個估算值(單位:毫秒),表示當前幀剩餘的空閒時間(通常 ≤ 50ms)。
  • deadline.didTimeout:如果設置了 timeout 且超時,則為 true,此時應儘快完成任務。

2. 可選配置對象

{
  timeout: 2000 // 最大等待時間(毫秒)。超時後即使沒有空閒也會執行回調。
}

⚠️ timeout 會降低優先級優勢,僅用於“最終必須執行”的兜底場景。


✅ 典型應用場景

1. 非關鍵數據預加載

requestIdleCallback(() => {
  // 預加載下一頁數據、圖片、代碼分割 chunk
  import('./NextPageComponent');
});

2. 埋點/日誌批量上報

let logs = [];

function sendLogs() {
  if (logs.length > 0) {
    navigator.sendBeacon('/log', JSON.stringify(logs));
    logs = [];
  }
}

function addLog(event) {
  logs.push(event);
  requestIdleCallback(sendLogs, { timeout: 5000 });
}

3. 大型列表虛擬滾動的緩存計算

在用户停止滾動後,利用空閒時間預計算可視區域外的 item 尺寸。

4. 分析用户行為(非實時)

如統計停留時長、點擊熱力圖聚合等。


🌐 瀏覽器兼容性(截至 2025 年)

ScreenShot_2026-01-05_110302_089

🔥 現實:僅 Chrome/Edge 支持,Firefox 和 Safari 永遠不會支持!

可通過 caniuse.com/requestidle… 查看。


🔄 降級方案(Polyfill / 替代方案)

由於兼容性差,生產環境必須提供 fallback

方案 1:使用 setTimeout 模擬(簡單但不精確)

const requestIdleCallback =
  window.requestIdleCallback ||
  function (callback) {
    const start = Date.now();
    return setTimeout(() => {
      callback({
        didTimeout: false,
        timeRemaining: () => Math.max(0, 50 - (Date.now() - start))
      });
    }, 1);
  };

const cancelIdleCallback =
  window.cancelIdleCallback ||
  function (id) {
    clearTimeout(id);
  };

方案 2:使用 requestAnimationFrame + 時間切片(更接近原生行為)

適用於需要精細控制的任務調度(如 React Fiber 的思路)。

方案 3:直接使用 setTimeout(fn, 0) 或 queueMicrotask

適用於非關鍵但需異步執行的任務,但無法利用“空閒時間”。


⚠️ 注意事項

  1. 不要執行高優先級任務:如用户輸入響應、動畫更新。
  2. 避免長時間運行:即使 timeRemaining() 返回較大值,也應分片處理。
  3. 不要依賴精確時間timeRemaining() 是估算值,可能突然變為 0。
  4. 移動端效果有限:低端設備空閒時間極少,可能長期不觸發。

🆚 與 requestAnimationFrame 對比

ScreenShot_2026-01-05_110419_984

✅ 兩者互補:rAF 保證流暢動畫,rIC 避免阻塞動畫。


📦 在現代框架中的使用

  • React:內部調度器受 rIC 啓發,但使用自定義實現(因兼容性問題)。
  • Vue / Svelte:一般不直接使用,但可用於自定義性能優化邏輯。
  • 推薦:在業務代碼中謹慎使用,並做好降級。

✅ 最佳實踐模板

function scheduleIdleWork(workFn, timeout = 2000) {
  if ('requestIdleCallback' in window) {
    return requestIdleCallback((deadline) => {
      if (deadline.timeRemaining() > 0 || deadline.didTimeout) {
        workFn();
      }
    }, { timeout });
  } else {
    // fallback: 稍後執行(不阻塞當前任務)
    return setTimeout(workFn, 0);
  }
}

// 使用
const id = scheduleIdleWork(() => {
  console.log('在空閒時執行');
});

// 取消(如組件卸載時)
// cancelIdleCallback(id) 或 clearTimeout(id)

🔚 總結

  • 作用:在瀏覽器空閒時執行低優先級任務,提升用户體驗。

  • 現狀僅 Chrome/Edge 支持,Firefox/Safari 已放棄。

  • 建議

    • 可用於非關鍵優化(如預加載、日誌上報)。
    • 必須提供降級方案
    • 不要用於核心功能。

9.AbortController

AbortController 是 Web 平台提供的一個標準接口,用於中止(取消)一個或多個異步操作,比如 fetch() 請求、定時器、自定義任務等。它提供了一種統一、可組合的方式來處理取消邏輯,避免內存泄漏或無效操作。


🧠 核心概念

  • AbortController:控制器對象,用於觸發中止。
  • AbortSignal:信號對象,與控制器關聯,傳遞“是否已中止”的狀態,並可監聽 abort 事件。

✅ 基本用法

1. 創建控制器和信號

const controller = new AbortController();
const signal = controller.signal; // 只讀的 AbortSignal

2. 監聽中止信號(在異步操作中)

// 示例:自定義異步任務
function myAsyncTask(signal) {
  return new Promise((resolve, reject) => {
    // 檢查是否已經中止
    if (signal.aborted) {
      reject(new DOMException('操作已中止', 'AbortError'));
      return;
    }

    // 監聽中止事件
    signal.addEventListener('abort', () => {
      reject(new DOMException('操作已中止', 'AbortError'));
    });

    // 模擬異步操作
    const timer = setTimeout(() => {
      resolve('任務完成');
    }, 3000);

    // 可選:在中止時清理資源
    signal.addEventListener('abort', () => {
      clearTimeout(timer);
    });
  });
}

3. 觸發中止

myAsyncTask(controller.signal)
  .then(console.log)
  .catch(e => {
    if (e.name === 'AbortError') {
      console.log('任務被用户取消');
    } else {
      console.error('其他錯誤', e);
    }
  });

// 1 秒後取消
setTimeout(() => {
  controller.abort(); // 觸發 abort 事件,signal.aborted 變為 true
}, 1000);

🌐 實際應用場景

1. 取消 fetch 請求(最常見)

const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('請求被取消');
    } else {
      console.error('網絡錯誤', err);
    }
  });

// 取消請求
controller.abort();

✅ 所有現代瀏覽器都支持 fetch 的 signal 選項。


2. 取消多個操作(一對多)

一個 AbortController 可以控制多個異步任務:

const controller = new AbortController();

fetch('/api/1', { signal: controller.signal });
fetch('/api/2', { signal: controller.signal });
myAsyncTask(controller.signal);

// 一鍵取消所有
controller.abort();

3. 與 setTimeout / setInterval 結合

雖然 setTimeout 本身不支持 signal,但可以手動集成:

function delay(ms, signal) {
  return new Promise((resolve, reject) => {
    if (signal?.aborted) {
      reject(new DOMException('已中止', 'AbortError'));
      return;
    }

    const id = setTimeout(resolve, ms);
    signal?.addEventListener('abort', () => {
      clearTimeout(id);
      reject(new DOMException('已中止', 'AbortError'));
    });
  });
}

// 使用
const ctrl = new AbortController();
delay(5000, ctrl.signal).catch(console.error);
ctrl.abort(); // 立即取消

🔁 與 TaskController(來自 scheduler.postTask)的關係

  • TaskController 是 AbortController 的子類,專為調度任務設計。
  • 它額外支持 priority 設置,並返回 TaskSignal(繼承自 AbortSignal)。
  • 因此,AbortController 是更通用的取消機制,而 TaskController 是其在任務調度場景下的擴展。
// TaskController 用法(實驗性)
const taskCtrl = new TaskController({ priority: 'background' });
scheduler.postTask(myTask, { signal: taskCtrl.signal });

// 也可以直接 abort()
taskCtrl.abort();

⚠️ 注意事項

  • abort() 只能調用一次,多次調用無副作用。
  • 中止後,signal.aborted 永遠為 true
  • 被中止的操作不會自動停止,你需要在代碼中主動監聽並清理資源(如清除定時器、關閉流等)。
  • 不要重複使用同一個 AbortController 實例處理不相關的任務,建議按邏輯分組使用。

在React中的應用

在 React 中,AbortController 是處理組件卸載後仍可能完成的異步操作(如 fetch 請求、定時器、動畫等)的關鍵工具。它的主要目的是 避免“內存泄漏”或“狀態更新已卸載組件” 的警告(例如經典的 Can't perform a React state update on an unmounted component)。

✅ 典型使用場景

1. 取消數據請求(最常見)

當組件在請求完成前被卸載(如用户快速切換路由),應取消請求。

import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController(); // 創建控制器

    const fetchUser = async () => {
      try {
        const res = await fetch(`/api/users/${userId}`, {
          signal: controller.signal // 傳入 signal
        });
        const data = await res.json();
        setUser(data);
      } catch (err) {
        if (err.name !== 'AbortError') {
          console.error('請求失敗:', err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchUser();

    // 清理函數:組件卸載時中止請求
    return () => {
      controller.abort();
    };
  }, [userId]);

  if (loading) return <div>加載中...</div>;
  return <div>用户名:{user?.name}</div>;
}

✅ 這樣即使組件卸載,也不會嘗試調用 setUser,避免警告。


2. 取消多個並行請求

useEffect(() => {
  const controller = new AbortController();

  Promise.all([
    fetch('/api/posts', { signal: controller.signal }),
    fetch('/api/comments', { signal: controller.signal })
  ])
  .then(/* ... */)
  .catch(err => {
    if (err.name !== 'AbortError') {
      // 處理真實錯誤
    }
  });

  return () => controller.abort();
}, []);

3. 結合自定義 Hook 封裝

可以創建一個可複用的 useAbortableFetch

// hooks/useAbortableFetch.js
import { useEffect, useState } from 'react';

export function useAbortableFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    const fetchData = async () => {
      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error('請求失敗');
        const json = await res.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

使用:

function App() {
  const { data, loading } = useAbortableFetch('/api/data');
  // ...
}

4. 取消定時器或動畫

雖然 setTimeout 不原生支持 signal,但可以手動集成:

useEffect(() => {
  const controller = new AbortController();

  const timer = setTimeout(() => {
    if (!controller.signal.aborted) {
      setData('更新了!');
    }
  }, 3000);

  return () => {
    controller.abort(); // 標記為中止
    clearTimeout(timer); // 清理定時器
  };
}, []);

或者封裝一個支持 signal 的 delay 工具函數(見前文)。


5. 與 React Router(v6)結合

在路由切換時自動取消請求:

// 不需要額外操作!只要在 useEffect 中正確使用 AbortController,
// 路由切換導致組件卸載時,清理函數會自動執行。

⚠️ 注意事項

  1. 不要忽略 AbortError
    .catch() 中要判斷是否是 AbortError,避免把“正常取消”當作錯誤處理。
  2. 每個 effect 使用獨立的 controller
    避免多個 effect 共用同一個 AbortController,除非你明確需要批量取消。
  3. 不適用於同步操作
    AbortController 只對異步、可中斷的操作有效。
  4. React 18 嚴格模式下的雙重調用
    在開發模式下,React 18 的嚴格模式會故意 mount → unmount → remount 組件,此時 AbortController 能確保第一次請求被正確取消,是正常行為,不是 bug。

🔄 替代方案(現代 React)

  • React Query / SWR:這些數據獲取庫內部已集成取消邏輯,無需手動管理 AbortController
  • useEffect cleanup:仍是處理取消的核心機制,AbortController 是其實現細節之一。

✅ 總結

ScreenShot_2026-01-05_110736_075

 

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.