Web API 实战
前言
现代浏览器提供了强大的原生 API,让前端开发者能够高效地处理观察、监听、异步计算等场景。本文将深入讲解 IntersectionObserver、ResizeObserver、MutationObserver、Web Worker 和 SharedWorker 的实战用法,并通过对比和最佳实践帮助你写出高性能的 Web 应用。
一、IntersectionObserver
1.1 概述
IntersectionObserver 是一种异步观察目标元素与祖先元素或顶级文档视口交叉状态的 API。它能够高效地检测元素是否进入或离开视口,替代了传统的滚动事件监听 + getBoundingClientRect() 方案,避免了主线程的频繁布局计算。
1.2 基本用法
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('元素进入视口', entry.target)
}
})
}, {
root: null, // 相对于视口
rootMargin: '0px',
threshold: 0.1 // 10% 可见时触发
})
observer.observe(document.querySelector('.target'))
// observer.unobserve(el) // 停止观察
// observer.disconnect() // 断开所有观察1.3 实战:图片懒加载
懒加载是 IntersectionObserver 最经典的应用场景。当图片元素进入视口时,将 data-src 的真实地址赋给 src,触发浏览器加载。
<img data-src="https://example.com/hd-image.jpg" alt="lazy" class="lazy" />const lazyImages = document.querySelectorAll('.lazy')
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.classList.add('loaded')
imageObserver.unobserve(img)
}
})
}, { rootMargin: '200px' }) // 提前 200px 开始加载
lazyImages.forEach((img) => imageObserver.observe(img))关键点:
rootMargin: '200px'实现预加载,提升用户体验- 加载后立即
unobserve,避免重复触发 - 可以利用
classList添加加载完成动画
1.4 实战:无限滚动
无限滚动(Infinite Scroll)通过在列表底部放置一个哨兵元素,当哨兵进入视口时加载更多数据。
<div id="sentinel">加载中...</div>const sentinel = document.getElementById('sentinel')
let page = 1
const loadMoreObserver = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
const data = await fetch(`/api/items?page=${++page}`)
// 渲染数据...
}
})
loadMoreObserver.observe(sentinel)1.5 实战:曝光埋点
曝光埋点是广告和推荐系统中常见的需求——当元素真正被用户看到时才上报埋点。
const trackObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const { id, type } = entry.target.dataset
navigator.sendBeacon('/api/track', JSON.stringify({
elementId: id,
type,
timestamp: Date.now(),
ratio: entry.intersectionRatio
}))
trackObserver.unobserve(entry.target) // 仅上报一次
}
})
}, { threshold: [0, 0.5, 1] })利用 threshold 数组可以精确控制上报时机,例如在元素 50% 可见时才认为是"有效曝光"。
1.6 实战:广告可见性检测
广告可见性(Viewability)是广告投放的核心指标。IntersectionObserver 可以精确测量广告元素在视口中的可见比例和持续时间。
class AdViewabilityTracker {
constructor(adElement, options = {}) {
this.ad = adElement
this.visibleStart = null
this.totalVisibleTime = 0
this.minVisibleTime = options.minVisibleTime || 1000
this.minVisibleRatio = options.minVisibleRatio || 0.5
const observer = new IntersectionObserver(
(entries) => this.handleIntersect(entries),
{ threshold: [0, this.minVisibleRatio] }
)
observer.observe(this.ad)
this.observer = observer
}
handleIntersect(entries) {
entries.forEach((entry) => {
if (entry.intersectionRatio >= this.minVisibleRatio) {
if (!this.visibleStart) this.visibleStart = Date.now()
} else {
if (this.visibleStart) {
this.totalVisibleTime += Date.now() - this.visibleStart
this.visibleStart = null
}
}
})
}
isViewable() {
return this.totalVisibleTime >= this.minVisibleTime
}
destroy() {
this.observer.disconnect()
}
}二、ResizeObserver
2.1 概述
ResizeObserver 用于监听元素尺寸变化。与 window.resize 事件不同,它可以精确到单个元素,而且在元素显示状态变化(如 display: none 变为可见)时也能触发。
2.2 基本用法
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { width, height } = entry.contentRect
console.log(`元素尺寸: ${width} x ${height}`)
})
})
resizeObserver.observe(document.querySelector('.container'))
// resizeObserver.unobserve(el)
// resizeObserver.disconnect()2.3 实战:自适应布局
实现一个容器查询效果的组件,当容器宽度变化时自动调整内部布局:
const container = document.querySelector('.dashboard')
const ro = new ResizeObserver((entries) => {
const { width } = entries[0].contentRect
container.classList.remove('dashboard--xs', 'dashboard--sm', 'dashboard--md', 'dashboard--lg')
if (width < 480) container.classList.add('dashboard--xs')
else if (width < 768) container.classList.add('dashboard--sm')
else if (width < 1024) container.classList.add('dashboard--md')
else container.classList.add('dashboard--lg')
})
ro.observe(container).dashboard--xs .card { grid-template-columns: 1fr; }
.dashboard--sm .card { grid-template-columns: 1fr 1fr; }
.dashboard--md .card { grid-template-columns: 1fr 1fr 1fr; }
.dashboard--lg .card { grid-template-columns: 1fr 1fr 1fr 1fr; }2.4 实战:文本域自动伸缩
const textarea = document.querySelector('textarea')
const ro = new ResizeObserver(() => {
textarea.style.height = 'auto'
textarea.style.height = textarea.scrollHeight + 'px'
})
ro.observe(textarea, { box: 'border-box' })三、MutationObserver
3.1 概述
MutationObserver 用于监听 DOM 树的变化,包括子节点添加/移除、属性变更、文本内容变化等。它取代了已废弃的 Mutation Events,提供了更高效、更细粒度的 DOM 变化监控能力。
3.2 基本用法
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
switch (mutation.type) {
case 'childList':
console.log('子节点变化', mutation.addedNodes, mutation.removedNodes)
break
case 'attributes':
console.log(`属性 ${mutation.attributeName} 已变化`)
break
case 'characterData':
console.log('文本内容变化')
break
}
}
})
observer.observe(document.getElementById('target'), {
childList: true, // 子节点增删
attributes: true, // 属性变化
characterData: true, // 文本变化
subtree: true, // 监听后代节点
attributeFilter: ['class', 'style'] // 仅监听特定属性
})3.3 实战:动态内容检测
在富文本编辑器或第三方嵌入内容中,检测新插入的脚本或样式并做安全处理:
const contentObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) { // 元素节点
if (node.tagName === 'SCRIPT') {
node.remove() // 移除动态插入的脚本
console.warn('已阻止动态脚本执行')
}
if (node.tagName === 'IFRAME' && !node.src.startsWith('https://trusted.com')) {
node.remove() // 阻止非可信来源的 iframe
}
}
})
})
})
contentObserver.observe(document.getElementById('editor'), {
childList: true,
subtree: true
})3.4 实战:自定义指令的高效更新
// 监听某个容器内所有具有 data-bind 属性的元素
const bindObserver = new MutationObserver((mutations) => {
const changedEls = new Set()
mutations.forEach((mutation) => {
if (mutation.type === 'attributes') {
changedEls.add(mutation.target)
}
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.hasAttribute('data-bind')) {
changedEls.add(node)
}
})
})
changedEls.forEach((el) => {
const key = el.getAttribute('data-bind')
el.textContent = state[key] ?? ''
})
})四、Web Worker
4.1 概述
Web Worker 允许在后台线程中运行 JavaScript 代码,不会阻塞主线程。它适用于 CPU 密集型任务,如大数据处理、图像计算、文件解析等。
4.2 基本通信模式
主线程:
const worker = new Worker('worker.js')
worker.postMessage({ type: 'compute', data: 42 })
worker.onmessage = (event) => {
console.log('Worker 返回结果:', event.data)
}
worker.onerror = (error) => {
console.error('Worker 错误:', error.message)
}
// 关闭 Worker
// worker.terminate()Worker 线程(worker.js):
self.onmessage = (event) => {
const { type, data } = event.data
switch (type) {
case 'compute':
const result = heavyComputation(data)
self.postMessage({ type: 'result', data: result })
break
case 'cancel':
self.close()
break
}
}
function heavyComputation(n) {
// 耗时计算...
return n * n
}4.3 实战:大数据处理
对包含 100 万条记录的 JSON 数据进行过滤和聚合:
主线程:
async function processLargeDataset(url) {
const response = await fetch(url)
const data = await response.json()
const worker = new Worker('data-processor.js')
return new Promise((resolve, reject) => {
worker.onmessage = (event) => {
resolve(event.data)
worker.terminate()
}
worker.onerror = (err) => reject(err)
worker.postMessage({ data })
})
}data-processor.js:
self.onmessage = (event) => {
const { data } = event.data
// 聚合计算:按分类统计数量
const categoryCount = new Map()
data.forEach((item) => {
const cat = item.category
categoryCount.set(cat, (categoryCount.get(cat) || 0) + 1)
})
// 过滤出符合条件的记录
const filtered = data.filter((item) => item.value > 100)
self.postMessage({
total: data.length,
categoryCount: Object.fromEntries(categoryCount),
filteredCount: filtered.length
})
}4.4 Worker 通信进阶
Transferable Objects: 使用 transferable 传输 ArrayBuffer 等二进制数据,避免数据拷贝,零开销传递所有权。
// 主线程
const buffer = new ArrayBuffer(1024 * 1024 * 100) // 100MB
const worker = new Worker('worker.js')
worker.postMessage({ buffer }, [buffer])
// 此时 buffer.byteLength === 0,所有权已转移
// Worker 线程
self.onmessage = (event) => {
const { buffer } = event.data
const view = new Uint8Array(buffer)
// 处理 buffer...
self.postMessage({ result: 'done' }, [buffer])
}五、SharedWorker
5.1 概述
SharedWorker 是一种可以被多个浏览上下文(如多个标签页、iframe)共享的 Worker。每个 SharedWorker 在所有同源页面中仅存在一个实例,多个页面共享同一个 Worker 线程,通过 port 进行通信。
5.2 用法示例
主页面:
const sharedWorker = new SharedWorker('shared-worker.js')
sharedWorker.port.start()
sharedWorker.port.postMessage({ type: 'subscribe', channel: 'news' })
sharedWorker.port.onmessage = (event) => {
console.log('收到共享消息:', event.data)
}shared-worker.js:
const ports = new Set()
self.onconnect = (event) => {
const port = event.ports[0]
ports.add(port)
port.onmessage = (event) => {
const { type, channel, data } = event.data
if (type === 'broadcast') {
ports.forEach((p) => {
if (p !== port) p.postMessage({ channel, data })
})
}
}
port.onmessageerror = () => ports.delete(port)
}5.3 实战:多标签页状态同步
// 主页面 - 发送消息
const sw = new SharedWorker('sync-worker.js')
sw.port.start()
document.getElementById('logout-btn').onclick = () => {
sw.port.postMessage({ action: 'logout', timestamp: Date.now() })
}
sw.port.onmessage = (event) => {
if (event.data.action === 'forceLogout') {
alert('账号已在其他设备登录,即将登出')
location.href = '/login'
}
}// sync-worker.js
const connections = []
let sessionToken = null
self.onconnect = (event) => {
const port = event.ports[0]
connections.push(port)
port.onmessage = (event) => {
const { action, timestamp, token } = event.data
if (action === 'login') {
sessionToken = token
}
if (action === 'logout') {
sessionToken = null
// 通知所有其他标签页
connections.forEach((p) => {
if (p !== port) p.postMessage({ action: 'forceLogout', timestamp })
})
}
}
}六、四种 Observer 对比
| 特性 | IntersectionObserver | ResizeObserver | MutationObserver | PerformanceObserver |
|---|---|---|---|---|
| 观察目标 | 元素与视口/父元素的交叉状态 | 元素的尺寸变化 | DOM 树变化(子节点、属性、文本) | 性能条目(LCP、FCP 等) |
| 回调触发时机 | 目标元素交叉状态改变时 | 元素尺寸变化时 | DOM 变更后同步触发 | 性能条目被记录时 |
| 是否异步 | 异步(微任务队列) | 异步 | 同步(微任务前触发) | 异步 |
| 性能开销 | 极低,由浏览器合成器处理 | 低,仅触发时计算 | 中等,深度 DOM 子树可能较大 | 低 |
| 主要应用 | 懒加载、埋点、无限滚动、广告可见性 | 自适应布局、文本域伸缩、图表尺寸自适应 | 动态内容安全检测、自定义模板引擎、DOM diff | 性能监控、Core Web Vitals 采集 |
| 支持性 | 现代浏览器全支持 | 现代浏览器全支持 | 现代浏览器全支持 | 现代浏览器全支持 |
| 能否观察多个元素 | 一个实例可观察多个目标 | 一个实例可观察多个目标 | 一个实例可观察单个根节点及其子树 | 按类型监听 |
| 取消防抖 | 不支持(无连续触发问题) | 自带防抖 | 可用 takeRecords() 手动处理 | 不支持 |
6.1 如何选择
- 可见性类需求(懒加载、埋点、广告) → IntersectionObserver
- 尺寸变化类需求(自适应、响应式) → ResizeObserver
- DOM 结构变化(动态内容、安全检测) → MutationObserver
- 性能监控需求 → PerformanceObserver(本文未详述,但值得了解)
七、性能考量与最佳实践
7.1 IntersectionObserver
- 设置合理的
threshold: 避免使用过多精度点(如[0, 0.01, 0.02, ... , 1]),这会显著增加计算开销。通常0或[0, 1]已足够。 - 使用
rootMargin预加载: 对懒加载场景设置rootMargin: '200px'提前加载,提升用户体验。 - 及时解绑: 每次加载完成后调用
unobserve(),避免无用的观察器回调。 - 避免在回调中做昂贵操作: 回调中不要执行重排(reflow)或长任务(long task),必要时使用
requestIdleCallback延迟处理。
7.2 ResizeObserver
- 避免循环触发: 在回调中修改目标元素的尺寸会导致无限循环。建议在回调中根据尺寸变化切换 CSS class,而非直接修改尺寸属性。
- 限制观察数量: 不需要对所有元素都加 ResizeObserver,仅对需要自适应行为的关键容器进行观察。
- 使用
contentRect而非getBoundingClientRect:contentRect由回调直接提供,不会触发重排。
7.3 MutationObserver
- 减少观察范围: 尽量设置
subtree: false,仅在必要时启用子树观察。 - 使用
attributeFilter: 如果只关心特定属性(如class、style),使用attributeFilter过滤,避免不必要的回调。 - 批量处理: MutationObserver 的回调会自动批量处理同一事件循环内的所有变更,不要在回调中逐个处理,而是利用
takeRecords()批量处理。 - 不要长时间持有引用: 在 SPA 中,组件销毁时务必调用
disconnect(),防止内存泄漏。
7.4 Web Worker
- 按需创建: Worker 的创建有一定开销(约 10~50ms),避免为短时间的小任务创建 Worker。
- 使用 Transferable Objects: 传输较大数据(如
ArrayBuffer、ImageBitmap)时使用 Transferable 方式,避免数据拷贝的内存开销。 - Worker 池化: 对于频繁的并行任务,创建一个 Worker 池复用 Worker 实例,而非每次新建。
class WorkerPool {
constructor(script, size = 4) {
this.workers = Array.from({ length: size }, () => new Worker(script))
this.queue = []
this.available = this.workers.map((w, i) => i)
}
run(data) {
return new Promise((resolve) => {
if (this.available.length > 0) {
this.runTask(this.available.pop(), data, resolve)
} else {
this.queue.push({ data, resolve })
}
})
}
runTask(index, data, resolve) {
const worker = this.workers[index]
worker.onmessage = (event) => {
resolve(event.data)
if (this.queue.length > 0) {
this.runTask(index, this.queue.shift().data, this.queue.shift().resolve)
} else {
this.available.push(index)
}
}
worker.postMessage(data)
}
}- 错误处理: 始终监听
worker.onerror和worker.onmessageerror,Worker 中的未捕获错误不会自动传播到主线程。
7.5 SharedWorker
- 端口管理: SharedWorker 不会自动处理端口断开,需要在
onmessageerror或beforeunload事件中清理端口引用,避免内存泄漏。 - 生命周期注意: 当所有引用该 SharedWorker 的页面都关闭后,Worker 会被自动终止。
- 通信性能: 与普通 Worker 相比,SharedWorker 通过
MessagePort通信,性能差异不大,但消息传递是序列化的(结构化克隆),大数据量时需注意。
7.6 综合建议
- 优先使用浏览器原生 API:Observer 系列 API 由浏览器底层实现,性能远优于自定义的轮询或事件监听方案。
- 选择合适的 API:不要滥用 MutationObserver 去监听滚动实现懒加载——IntersectionObserver 才是正确的工具。
- 关注兼容性:虽然这些 API 已得到广泛支持,但在低版本浏览器(如 IE 11)中仍需要 polyfill。
- 内存管理:所有 Observer 和 Worker 在使用完毕后都应调用
disconnect()或terminate()释放资源。 - 开发体验:Worker 代码无法直接访问 DOM,调试时可利用
console.log输出日志,或在 Chrome DevTools 的 Sources → Workers 面板中查看。
八、综合演示
下面是一个结合了 IntersectionObserver(懒加载)和 Web Worker(斐波那契计算与素数判断)的交互式 Demo,展示了这些 API 在实际项目中的配合使用: