神级JS API,谁用谁好用

前端圈一年 365 天,天天出新框架,但真正能让你 少写代码、少引依赖、少踩性能坑 的,实则是浏览器爸爸早已偷偷内置的「 原生外挂 」。

1. ResizeObserver

ResizeObserver 是一个浏览器原生的 JavaScript API,用于 监听 DOM 元素尺寸的变化 。它类似于 MutationObserver ,但专门用于观察元素的大小(宽高)变化,而无需依赖 window.resize 事件(后者只对视口变化有效)。

基本用法

const resizeObserver = new ResizeObserver(entries => {
  for (let entry of entries) {
    const { width, height } = entry.con #技术分享tentRect;
    console.log(`元素尺寸:${width} x ${height}`);


    console.log('目标元素:', entry.target);
  }
});

resizeObserver.observe(document.querySelector('#my-element'));

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';

if (!window.ResizeObserver) { window.ResizeObserver = ResizeObserver; }

C 示例: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>; }

C 2.IntersectionObserver

IntersectionObserver 是一个强劲的浏览器原生 API,用于 异步监听目标元素与祖先元素(或视口)的交叉(相交)状态变化 。它常用于实现 懒加载、无限滚动、曝光统计、动画触发 等场景,性能远优于传统的 scroll 事件监听。

基本用法

const observer = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {

if (entry.isIntersecting) { console.log('元素进入视口:', entry.target); } else { console.log('元素离开视口'); } }); });

observer.observe(document.querySelector('#my-element'));

⚙️ 配置选项(可选)

const options = {
  root: null,
  rootMargin: '0px',
  threshold: 0.5
};

const observer = new IntersectionObserver(callback, options);

threshold 示例:

  • threshold: 0 :只要有一点进入就触发(默认)。
  • threshold: 1 :完全进入才触发。
  • threshold: [0, 0.25, 0.5, 0.75, 1] :在 0%、25%、50%… 时都触发。

停止观察

javascript
编辑
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;
      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
import 'intersection-observer';

注意:polyfill 会回退到 scroll + getBoundingClientRect() ,性能较差,仅用于兼容。


与 ResizeObserver / MutationObserver 对比

| API | 用途 | | —

| IntersectionObserver | 监听元素是否进入/离开视口(或指定容器) | | ResizeObserver | 监听元素尺寸变化 | | MutationObserver | 监听 DOM 结构或属性变化 |

三者互补,常结合使用。

小技巧

  • 使用 rootMargin: '100px' 可以 提前触发 (在元素距离视口还有 100px 时就加载)。
  • 在 <img loading=”lazy”> 普及的今天,简单图片懒加载可直接用 HTML 属性,但复杂逻辑仍需 IntersectionObserver 。

3.Page Visibility

Page Visibility API 是一个浏览器原生 API,用于检测 当前网页是否对用户可见 (即是否处于前台标签页或被最小化/切换到后台)。它可以协助开发者优化性能、节省资源,或实现特定业务逻辑(如暂停视频、停止轮询、统计停留时长等)。


核心属性与事件

1. document.visibilityState

返回当前页面的可见性状态,可能值包括:

| 值 | 含义 | | —

| 'visible' | 页面可见(处于前台标签页) | | 'hidden' | 页面不可见(切换到其他标签页、最小化窗口、锁屏等) | | 'prerender' | 页面正在预渲染(已废弃,现代浏览器基本不用) | | 'unloaded' | 页面即将卸载(极少使用) |

实际开发中主要关注 '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 )下使用。
  • 用户手势要求 :只能在 click 、 touchend 等用户操作回调中调用。
  • 字段非全部必需 :但至少要提供 title 、 text 、 url 中的一个(推荐提供 url )。
  • 无法控制目标应用 :分享目标由操作系统决定,开发者无法指定(如“只分享到微信”)。

支持情况(截至 2025 年)

| 平台 | 浏览器 | 支持情况 | | —

| Android | Chrome 61+

| iOS | Safari 12.2+

| Windows | Chrome 76+

| macOS | Safari 13+

| Linux | 部分浏览器 | ⚠️ 有限支持 |

可通过 caniuse.com/web-share 查看最新兼容性。

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

现代浏览器(Chrome 89+ 等)支持分享 文件 (如图片、PDF):

if (navigator.canShare && navigator.canShare({ files: [file] })) {
  await navigator.share({
    title: '图片分享',
    files: [file]
  });
}

注意:文件必须来自用户选择(如 <input type=”file”> )或由网页生成,不能是任意网络文件。

C 回退方案(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('链接已复制到剪贴板');
}

C 在框架中使用(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 中使用效果最佳,可实现“类原生”分享体验。

C 5. Wake Lock

Wake Lock API 是一个现代 Web API,允许网页 防止设备进入休眠状态 (如屏幕变暗、锁屏),常用于需要长时间保持活跃的场景,例如:

  • 视频播放器(避免播放时屏幕关闭)
  • 导航应用(持续显示路线)
  • 扫码/AR 应用(保持摄像头活跃)
  • 阅读器/电子书(长时间阅读不锁屏)

C 两种锁类型(目前主要支持 screen )

目前 只有 screen 类型 在主流浏览器中可用。


C ✅ 基本用法(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 年)

| 浏览器 | 支持情况 | | —

| Chrome | ✅ 84+(Android & Desktop) | | Edge | ✅ 84+

| Safari | ❌ 不支持(iOS/macOS 均未实现) | | Firefox | ❌ 默认禁用(需手动开启 dom.wakelock.enabled) |

移动端 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();
  }
});

C 降级方案(Fallback)

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

  • 提示用户“请手动关闭自动锁屏”
  • 使用全屏 API( requestFullscreen() )有时可延长屏幕活跃时间(非可靠)
  • 对于视频,可尝试使用 <video playsinline webkit-playsinline> 等属性优化体验

注意事项

  • 不要滥用 :长时间保持唤醒会显著增加耗电。
  • 始终提供关闭选项 :让用户能手动禁用“保持唤醒”。
  • 测试真实设备 :模拟器行为可能与真机不同。

检测是否支持

if ('wakeLock' in navigator) {

}

C 6. Broadcast Channel

BroadcastChannel 是一个现代 Web API,允许 同源(same-origin)的不同浏览器上下文

它类似于“发布-订阅”模式:一个上下文发送消息,所有监听同一频道的其他上下文都能收到。


基本用法

1. 创建频道并监听消息

const channel = new BroadcastChannel('my-app-channel');

channel.addEventListener('message', (event) => { console.log('收到消息:', event.data); });

2. 发送消息

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 年)

| 浏览器 | 支持情况 | | —

| Chrome | ✅ 54+

| Edge | ✅ 79+

| Firefox | ✅ 38+

| Safari | ✅ 15.4+(iOS 15.4+

| iOS WebView | ✅ 15.4+ |

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


降级方案(Fallback for older browsers)

利用 localStorage 的 storage 事件实现类似广播:

function broadcastFallback(message) {
  localStorage.setItem('broadcast-msg', JSON.stringify({
    ...message,
    timestamp: Date.now()
  }));
}

window.addEventListener('storage', (e) => { if (e.key === 'broadcast-msg') { const message = JSON.parse(e.newValue); console.log('Fallback 收到:', message); } });

缺点:只能传递字符串,且 storage 事件 不会在当前标签页触发 (正好避免自己收到自己发的消息)。


与其他通信方式对比

| 方式 | 适用场景 | 跨域 | 多标签 | Worker | | —

| BroadcastChannel | 同源多上下文广播 | ❌ | ✅ | ✅ | | window.postMessage | 准确点对点通信 | ✅(需验证 origin) | ✅(需持有 window 引用) | ✅ | | SharedWorker | 多页面共享逻辑 | ❌ | ✅ | ✅(作为中介) | | localStorage + storage | 简单广播(旧浏览器) | ❌ | ✅ | ❌ |


小技巧

  • 避免无限循环 :如果多个页面都响应消息并再次广播,可能形成循环。提议使用 type 字段区分消息来源或添加防重机制。
  • 结构化克隆 : postMessage 支持传输 ArrayBuffer 、 Blob 、 Map 等(遵循 结构化克隆算法 ),不只是 JSON。

C 在框架中使用(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

核心区别

| 特性 | BroadcastChannel | Vuex / Redux / Zustand 等 | | —

| 作用范围 | 跨浏览器上下文(多个标签页、iframe、Web Worker) | 单个页面/应用内部(组件之间) | | 通信方式 | 跨文档广播(类似“全局事件总线”) | 单向数据流 +

| 数据存储 | ❌ 不存储状态,只传递消息 | ✅ 聚焦式存储状态(store) | | 响应式更新 | ❌ 需手动监听消息并更新 UI | ✅ 状态变化自动触发组件重渲染 | | 同源限制 | ✅ 仅限同源页面 | 无(仅限当前页面) | | 典型用途 | 多标签页登录同步、跨 Worker 通信 | 组件间状态共享、复杂状态逻辑管理 |


举个例子说明差异

场景:用户登录后,所有打开的标签页都要显示用户名

  • 用 BroadcastChannel
  • 标签页 A 登录 → 通过 channel.postMessage({ type: 'LOGIN', user }) 广播。
  • 标签页 B、C(即使没用 Vue/React)监听到消息 → 各自更新自己的 UI
  • 每个页面 独立维护自己的状态 ,只是通过消息“同步”了登录事件。
  • 用 Vuex
  • 只在 当前标签页内 ,多个 Vue 组件共享 store.state.user 。
  • 标签页 A 的 Vuex 无法直接影响 标签页 B 的 Vuex。
  • 如果你打开两个标签页,它们有 两个完全独立的 Vuex 实例

✅ 所以: Vuex 管“页面内”,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


✅ 总结

| 你想做…… | 该用…… | | —

| 组件之间共享状态、触发更新 | Vuex / Redux / Context / Zustand | | 多个浏览器标签页同步登录状态 | BroadcastChannel(或 localStorage +

| 让 Vuex 状态在多标签页同步 | BroadcastChannel + Vuex 结合 |

C 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 及用途

| entryType | 说明 | 典型用途 | | —

| 'navigation' | 页面导航性能(如 DNS、TCP、DOMContentLoaded) | 分析首屏加载瓶颈 | | 'resource' | 所有资源加载(JS、CSS、图片、XHR、fetch) | 监控第三方资源、慢请求 | | 'paint' | 首次绘制(FP)、首次内容绘制(FCP) | 衡量视觉加载速度 | | 'longtask' | 超过 50ms 的主线程任务 | 识别卡顿、影响交互响应的缘由 | | 'largest-contentful-paint' (LCP) | 最大内容元素渲染时间 | 核心 Web Vitals 指标 | | 'layout-shift' (CLS) | 累积布局偏移 | 检测页面“抖动” | | 'first-input' (FID) / 'event' (INP) | 首次输入延迟 / 交互到下次绘制 | 衡量交互响应性 |

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):

“`javascript

if (PerformanceObserver.supportedEntryTypes.includes('longtask')) {

}

“`


浏览器兼容性

  • ✅ Chrome / Edge:全面支持(包括 Web Vitals)
  • ✅ Firefox:支持基础类型( resource , navigation ),Web Vitals 支持较弱
  • ✅ Safari 15+:支持 LCP、CLS、FCP 等核心指标
  • ❌ IE:不支持

推荐使用 Google 的 web-vitals 库 跨浏览器采集 Core Web Vitals。


与 performance.getEntries() 对比

| 方式 | 优点 | 缺点 | | —

| PerformanceObserver | 异步、实时、支持 Web Vitals、不阻塞主线程 | 需要提前注册监听 | | performance.getEntries() | 简单直接、可查询历史 | 无法获取动态指标(如 LCP 在发生时才能确定)、需轮询 |

现代性能监控应优先使用 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

C React 版本: useWebVitals

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); } };

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

}, [onReport, reportAll]); };

使用示例

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

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

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

优点 :自动处理浏览器兼容性、只上报有效指标、支持开发环境日志。


Vue 3 版本: useWebVitals

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

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]); };

Vue 版本类似,用 onMounted + onUnmounted 管理生命周期。


上报提议

  • LCP、FCP、CLS :每个页面会话上报一次( reportAll: false )。
  • INP/FID :用户每次交互可能触发,可采样上报。
  • 慢资源 :可聚合后批量上报,避免频繁请求。

部署提示

  • 生产环境 使用,开发环境仅用于调试。
  • 避免阻塞主渲染逻辑( web-vitals 是异步非阻塞的)。
  • 配合 Google Analytics 4 的 Web Vitals 自动采集 更省事。

C 8. requestIdleCallback

requestIdleCallback 是一个浏览器提供的 API,用于 在浏览器主线程空闲时执行低优先级任务 ,避免影响关键操作(如用户输入、动画、布局等),从而提升页面流畅性和响应性。

它是实现“ 协作式调度(Cooperative Scheduling) ”的关键工具,React 16+ 的 Fiber 架构就受其启发(尽管 React 最终未直接使用它)。


基本用法

function doLowPriorityWork(deadline) {

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(() => {

  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 年)

| 浏览器 | 支持情况 | | —

| Chrome | ✅ 47+

| Edge | ✅ 79+

| Firefox | ❌ 不支持(已明确拒绝实现) | | Safari | ❌ 不支持 | | iOS / Android WebView | ❌ 基本不可用 |

现实:仅 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 对比

| API | 时机 | 用途 | | —

| requestAnimationFrame | 每一帧开始前(约 16ms 一次) | 动画、视觉更新 | | requestIdleCallback | 每一帧结束后,若有空闲 | 低优先级后台任务 |

✅ 两者互补: 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 {

    return setTimeout(workFn, 0);
  }
}

const id = scheduleIdleWork(() => { console.log('在空闲时执行'); });

总结

  • 作用 :在浏览器空闲时执行低优先级任务,提升用户体验。
  • 现状仅 Chrome/Edge 支持 ,Firefox/Safari 已放弃。
  • 提议
  • 可用于 非关键优化 (如预加载、日志上报)。
  • 必须提供降级方案
  • 不要用于核心功能。

C 9.AbortController

AbortController 是 Web 平台提供的一个标准接口,用于 中止(撤销)一个或多个异步操作 ,列如 fetch() 请求、定时器、自定义任务等。它提供了一种统一、可组合的方式来处理撤销逻辑,避免内存泄漏或无效操作。


核心概念

  • AbortController :控制器对象,用于触发中止。
  • AbortSignal :信号对象,与控制器关联,传递“是否已中止”的状态,并可监听 abort 事件。

✅ 基本用法

1. 创建控制器和信号

const controller = new AbortController();
const signal = controller.signal;

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);
    }
  });

setTimeout(() => { controller.abort(); }, 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 是其在任务调度场景下的扩展。
const taskCtrl = new TaskController({ priority: 'background' });
scheduler.postTask(myTask, { signal: taskCtrl.signal });

taskCtrl.abort();

⚠️ 注意事项

  • abort() 只能调用一次 ,多次调用无副作用。
  • 中止后, signal.aborted 永远为 true 。
  • 被中止的操作 不会自动停止 ,你需要在代码中 主动监听并清理资源 (如清除定时器、关闭流等)。
  • 不要重复使用同一个 AbortController 实例处理不相关的任务,提议按逻辑分组使用。

C 在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 }); 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 :

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)结合

在路由切换时自动撤销请求:


⚠️ 注意事项

  1. 不要忽略 AbortError
    在 .catch() 中要判断是否是 AbortError ,避免把“正常撤销”当作错误处理。
  2. 每个 effect 使用独立的 controller
    避免多个 effect 共用同一个 AbortController ,除非你明确需要批量撤销。
  3. 不适用于同步操作
    AbortController 只对异步、可中断的操作有效。
  4. React 18 严格模式下的双重调用
    在开发模式下,React 18 的严格模式会故意 mount → unmount → remount 组件,此时 AbortController 能确保第一次请求被正确撤销,是 正常行为 ,不是 bug。

C 替代方案(现代 React)

  • React Query / SWR :这些数据获取库 内部已集成撤销逻辑 ,无需手动管理 AbortController 。
  • useEffect cleanup :仍是处理撤销的核心机制, AbortController 是实则现细节之一。

✅ 总结

| 场景 | 是否需要 AbortController | | —

| fetch 请求 | ✅ 强烈推荐 | | 自定义异步任务(如 WebSocket、定时器) | ✅ 推荐 | | 使用 React Query / SWR | ❌ 不需要(库已处理) | | 同步计算 | ❌ 不适用 |

最佳实践 :只要你在 useEffect 中发起异步操作并更新状态,就应思考使用 AbortController 或等效的撤销机制。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...