神级JS API,谁用谁好用

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

1. ResizeObserver

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

🧩 基本用法

js 复制代码
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() 使用。

🛑 停止观察

js 复制代码
// 停止观察某个元素
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:

bash 复制代码
npm install @juggle/resize-observer
js 复制代码
import ResizeObserver from '@juggle/resize-observer';

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

示例:React 中使用

jsx 复制代码
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 事件监听。

🧩 基本用法

js 复制代码
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'));

⚙️ 配置选项(可选)

js 复制代码
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%... 时都触发。

🛑 停止观察

scss 复制代码
javascript
编辑
observer.unobserve(element); // 停止单个元素
observer.disconnect();      // 停止所有并释放资源

建议 :在组件销毁时调用 disconnect(),防止内存泄漏。

✅ 典型应用场景

1. 图片懒加载

js 复制代码
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 团队维护):

bash 复制代码
npm install intersection-observer
js 复制代码
// 在应用入口引入(自动填充 window.IntersectionObserver)
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 事件

当页面可见性状态改变时触发。

✅ 基本用法示例

js 复制代码
function handleVisibilityChange() {
  if (document.visibilityState === 'visible') {
    console.log('页面回到前台');
    // 恢复视频播放、重启定时器、刷新数据等
  } else if (document.visibilityState === 'hidden') {
    console.log('页面进入后台');
    // 暂停视频、停止轮询、保存状态等
  }
}

// 监听可见性变化
document.addEventListener('visibilitychange', handleVisibilityChange);

🌟 典型应用场景

1. 暂停/恢复媒体播放

js 复制代码
const video = document.querySelector('video');

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

2. 停止不必要的轮询或定时任务

js 复制代码
let intervalId;

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

function stopPolling() {
  clearInterval(intervalId);
}

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

startPolling(); // 初始启动

3. 用户停留时长统计

js 复制代码
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 更精准判断

js 复制代码
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,允许网页调用操作系统级别的分享功能,让用户将内容(如链接、文本、标题等)快速分享到设备上安装的其他应用(如微信、邮件、短信、笔记等)。

✅ 基本用法

js 复制代码
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 年)

平台 浏览器 支持情况
Android Chrome 61+ ✅ 完整支持
iOS Safari 12.2+ ✅(需 iOS 12.2+)
Windows Chrome 76+ / Edge 79+ ✅(调用系统分享面板)
macOS Safari 13+ / Chrome ✅(部分版本)
Linux 部分浏览器 ⚠️ 有限支持

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

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

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

js 复制代码
if (navigator.canShare && navigator.canShare({ files: [file] })) {
  await navigator.share({
    title: '图片分享',
    files: [file] // File 对象数组
  });
}

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

🔄 回退方案(Fallback)

当不支持 Web Share 时,可提供复制链接或自定义分享按钮:

js 复制代码
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 示例)

jsx 复制代码
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

js 复制代码
// 1. Screen Wake Lock(屏幕唤醒锁) ← 当前唯一广泛支持的类型
// 2. System Wake Lock(系统唤醒锁) ← 尚未标准化,基本不可用

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


✅ 基本用法(Screen Wake Lock)

js 复制代码
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),不能在页面加载时自动请求。


🛑 释放锁(可选,通常自动释放)

js 复制代码
if (wakeLock) {
  await wakeLock.release(); // 显式释放
  wakeLock = null;
}

锁会在以下情况自动释放

  • 页面进入后台(visibilitychangehidden
  • 浏览器标签页关闭
  • 用户手动锁屏
  • 页面失去焦点(部分浏览器)

🌐 浏览器兼容性(截至 2025 年)

浏览器 支持情况
Chrome ✅ 84+(Android & Desktop)
Edge ✅ 84+
Safari ❌ 不支持(iOS/macOS 均未实现)
Firefox ❌ 默认禁用(需手动开启 dom.wakelock.enabled

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

可通过 caniuse.com/wake-lock 查看最新状态。


🛡️ 安全与权限要求

  • 必须在 HTTPS 下使用(localhost 除外)
  • 必须由用户手势触发(如点击、触摸)
  • 仅在页面可见时有效(页面切到后台会自动释放)
  • 不会绕过系统锁屏密码,仅防止屏幕变暗/休眠

💡 实际应用场景示例

场景:视频播放时不锁屏

js 复制代码
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 自动管理

js 复制代码
document.addEventListener('visibilitychange', () => {
  if (document.hidden && wakeLock) {
    wakeLock.release(); // 页面隐藏时主动释放
  }
});

🔄 降级方案(Fallback)

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

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

📌 注意事项

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

🔍 检测是否支持

js 复制代码
if ('wakeLock' in navigator) {
  // 支持 Wake Lock API
}

6. Broadcast Channel

BroadcastChannel 是一个现代 Web API,允许同源(same-origin)的不同浏览器上下文 (如多个标签页、iframe、Web Worker)之间进行简单、高效的跨文档通信。 它类似于"发布-订阅"模式:一个上下文发送消息,所有监听同一频道的其他上下文都能收到。


🧩 基本用法

1. 创建频道并监听消息

js 复制代码
// 所有页面/worker 使用相同的频道名
const channel = new BroadcastChannel('my-app-channel');

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

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

2. 发送消息

js 复制代码
// 任意同源页面或 worker 中
channel.postMessage({ type: 'USER_LOGIN', userId: 123 });

3. 关闭频道(可选,推荐在页面卸载时调用)

js 复制代码
window.addEventListener('beforeunload', () => {
  channel.close(); // 释放资源
});

自动广播 :消息会发送给所有 监听 'my-app-channel' 的同源上下文(包括发送者自己,除非你过滤)。


🔐 安全限制

  • 同源策略:只有协议 + 域名 + 端口完全相同的页面才能通信。

    • https://example.com/page1https://example.com/page2
    • https://example.comhttps://sub.example.com
    • http://localhost:3000http://localhost:8080
  • 不支持跨域 :不能用于跨域 iframe 通信(此时应考虑 postMessage + origin 验证)。


✅ 典型应用场景

1. 用户登录/登出同步

当用户在一个标签页登录,其他标签页自动更新状态:

js 复制代码
// 登录页
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+ / macOS Monterey+)
iOS WebView ✅ 15.4+

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


🔄 降级方案(Fallback for older browsers)

利用 localStoragestorage 事件实现类似广播:

js 复制代码
// 发送消息(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 事件不会在当前标签页触发(正好避免自己收到自己发的消息)。


🆚 与其他通信方式对比

方式 适用场景 跨域 多标签 Worker
BroadcastChannel 同源多上下文广播
window.postMessage 精确点对点通信 ✅(需验证 origin) ✅(需持有 window 引用)
SharedWorker 多页面共享逻辑 ✅(作为中介)
localStorage + storage 简单广播(旧浏览器)

💡 小技巧

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

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

jsx 复制代码
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 管"页面间"


🤝 它们可以结合使用!

实际项目中,两者常配合使用

js 复制代码
// 在 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:通过 localStorageBroadcastChannel 同步 Vuex 的 mutations。
  • 自定义方案:监听 storage 事件或 BroadcastChannel,触发本地 store 更新。

但核心思想不变:跨标签页通信靠 BroadcastChannel(或 storage),状态管理靠 Vuex


✅ 总结

你想做...... 该用......
组件之间共享状态、触发更新 Vuex / Redux / Context / Zustand
多个浏览器标签页同步登录状态 BroadcastChannel (或 localStorage + storage 事件)
让 Vuex 状态在多标签页同步 BroadcastChannel + Vuex 结合

7. PerformanceObserver

PerformanceObserver 是一个强大的 Web API,用于异步监听性能相关的事件和指标 ,而无需轮询 performance.getEntries()。它是现代 Web 性能监控(如 Core Web Vitals)的核心工具。


🎯 核心作用

监听浏览器自动记录的 Performance Timeline(性能时间线) 中的新条目,例如:

  • 资源加载(resource
  • 导航 timing(navigation
  • 长任务(longtask
  • 元素曝光(element,实验性)
  • 最重要: CLS、LCP、FCP、INP 等 Web Vitals 指标

🧩 基本用法

js 复制代码
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(最大内容绘制)

js 复制代码
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(累积布局偏移)

js 复制代码
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. 监控慢资源加载

js 复制代码
new PerformanceObserver((list) => {
  for (const resource of list.getEntries()) {
    if (resource.duration > 2000) {
      console.warn('慢资源:', resource.name, resource.duration + 'ms');
      // 上报性能问题
    }
  }
}).observe({ entryTypes: ['resource'] });

4. 捕获长任务(卡顿原因)

js 复制代码
new PerformanceObserver((list) => {
  for (const task of list.getEntries()) {
    if (task.duration > 100) {
      console.log('长任务:', task.duration + 'ms', task.attribution);
    }
  }
}).observe({ entryTypes: ['longtask'] });

需要先注册长任务支持(部分浏览器需 polyfill):

js 复制代码
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、自建服务)。

🛠️ 工具推荐

js 复制代码
import { getLCP, getCLS, getFCP } from 'web-vitals';
getLCP(console.log);

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

✅ 共同前提

我们使用 Google 官方的 web-vitals 库,它已封装好 PerformanceObserver 的兼容逻辑。

bash 复制代码
npm install web-vitals

🟦 React 版本:useWebVitals

tsx 复制代码
// 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]);
};

📌 使用示例

tsx 复制代码
// 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

ts 复制代码
// 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);
  });
}

📌 使用示例

vue 复制代码
<!-- 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

ts 复制代码
// 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]);
};

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


📊 上报建议

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

🚀 部署提示

8. requestIdleCallback

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

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


🧩 基本用法

js 复制代码
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. 可选配置对象

js 复制代码
{
  timeout: 2000 // 最大等待时间(毫秒)。超时后即使没有空闲也会执行回调。
}

⚠️ timeout 会降低优先级优势,仅用于"最终必须执行"的兜底场景。


✅ 典型应用场景

1. 非关键数据预加载

js 复制代码
requestIdleCallback(() => {
  // 预加载下一页数据、图片、代码分割 chunk
  import('./NextPageComponent');
});

2. 埋点/日志批量上报

js 复制代码
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 模拟(简单但不精确)

js 复制代码
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:一般不直接使用,但可用于自定义性能优化逻辑。
  • 推荐:在业务代码中谨慎使用,并做好降级。

✅ 最佳实践模板

js 复制代码
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 已放弃。

  • 建议

    • 可用于非关键优化(如预加载、日志上报)。
    • 必须提供降级方案
    • 不要用于核心功能。
相关推荐
冬至已至5 小时前
AI 时代的自动化信息获取与整合
前端
用户6600676685395 小时前
从送花表白实例读懂 JavaScript 对象字面量与代理模式
javascript
我是日安5 小时前
从零到一打造 Vue3 响应式系统 Day 29 - readonly:数据保护实现
前端·javascript·vue.js
时代拖油瓶5 小时前
我劝你必须知道——Intl.Segmenter
前端·javascript
韭菜炒大葱5 小时前
对象字面量与JSON:JavaScript中最强大的数据结构
javascript
海在掘金611275 小时前
从"万能函数"到"精准工具":泛型如何消除重复代码
前端
云心雨禅5 小时前
DNS工作原理:从域名到IP
运维·前端·网络协议·tcp/ip·github
Dorian_Ov05 小时前
Mybatis操作postgresql的postgis的一些总结
前端·gis
Moshow郑锴6 小时前
从 “瞎埋点” 到 “精准分析”:WebTagging 设计 + 页面埋点指南(附避坑清单)
前端