前端性能革命:10 个不可或缺的高级API让网页秒速响应

41 阅读8分钟

mp.weixin.qq.com/s/opeulgGGM…

本文将带你探索 10 个能够显著提升前端性能的高级 API

IntersectionObserver

image.png

解决的问题: 传统监听滚动事件实现懒加载会造成大量的性能开销,因为需要频繁触发并调用 getBoundingClientRect(),导致重排。

API 简介IntersectionObserver 接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。其祖先元素或视口被称为根(root)。

当一个 IntersectionObserver 对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦 IntersectionObserver 被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。

性能提升点: 避免滚动时的强制同步布局和频繁的 JavaScript 执行,极大提升滚动性能。

// 图片懒加载实战
const lazyImageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const img = entry.target; // 将 data-src 的值赋给 src
      img.src = img.dataset.src;
      img.classList.remove("lazy"); // 图片加载后停止观察
      lazyImageObserver.unobserve(img);
    }
  });
});

// 观察所有带有 lazy 类的图片
document.querySelectorAll("img.lazy").forEach((img) => {
  lazyImageObserver.observe(img);
});

VirtualKeyboard API

image.png

解决的问题: 在移动端,输入框聚焦时弹出的软键盘会挤压视口高度,导致页面布局错乱,常见于 position: fixed 的底部元素被顶起。

API 简介虚拟键盘 API 的 VirtualKeyboard 接口用于具有屏幕虚拟键盘的设备(如平板电脑、手机或其他没有物理键盘的设备)。

VirtualKeyboard 接口使你可以选择不使用浏览器自动处理屏幕虚拟键盘的方式——通过减少视口的高度来为虚拟键盘腾出空间。你可以阻止浏览器改变视口大小、检测虚拟键盘的位置和大小,并通过编程方式显示或隐藏虚拟键盘。

性能与体验提升点: 避免使用不稳定的 window.resize 事件或定时器去猜测键盘状态,实现平滑、自适应的布局调整。

if ("virtualKeyboard" in navigator) {
  const virtualKeyboard = navigator.virtualKeyboard; // 监听键盘几何变化

  virtualKeyboard.addEventListener("geometrychange", (event) => {
    const { x, y, width, height } = event.target.geometry; // 调整底部固定元素的位置
    const bottomElement = document.getElementById("bottom-bar");
    if (height > 0) {
      // 键盘弹出,将元素上推键盘的高度
      bottomElement.style.transform = `translateY(-${height}px)`;
    } else {
      // 键盘收起,恢复原位
      bottomElement.style.transform = "translateY(0)";
    }
  }); // 在输入框聚焦时显示虚拟键盘(如果需要)

  document.getElementById("my-input").addEventListener("focus", () => {
    virtualKeyboard.show();
  });
}

Content Visiblity: auto 与 contain-intrinsic-size

image.png

image.png

解决的问题: 长列表或复杂文档的初始渲染和滚动会非常卡顿,因为浏览器需要为所有元素进行布局和绘制,即使它们不在视口内。

API 简介CSS 属性 content-visibility 控制元素是否渲染其内容,以及施加一组强局限,由此允许用户代理有机会在不需要时省略大片的布局和渲染工作。此属性使用户代理得以在不需要时跳过元素的渲染工作(包括布局和绘制)——由此使页面的初始加载明显变快。

CSS 简写属性 contain-intrinsic-size 定义了元素受尺寸局限时浏览器用于布局的元素尺寸。以避免滚动条抖动。

性能提升点: 极大减少初始加载的渲染工作量,提升首屏渲染(FCP)和可交互时间(TTI),实现如原生应用般流畅的滚动。

<style>
  .long-list-item {
    content-visibility: auto;
    /* 提供一个近似的高度值,避免滚动条跳动 */
    contain-intrinsic-size: 200px;
  }
</style>

<div class="long-list">
  <div class="long-list-item">项目 1</div>

  <div class="long-list-item">项目 2</div>
  <!-- ... 成百上千个项目 -->
</div>

PerformanceObserver

image.png

解决的问题: 传统的 performance.getEntries() 只能获取历史性能数据,无法实时监控应用在整个生命周期中产生的性能指标。

API 简介PerformanceObserver 用于监测性能度量事件,在浏览器的性能时间轴记录新的 performance entry 的时候将会被通知。它是监控诸如 LCP(最大内容绘制)、FID(首次输入延迟)、CLS(累积布局偏移)等 Core Web Vitals 的推荐方式。

性能提升点: 提供精准、实时的性能数据,帮助你在生产环境中定位和解决性能问题。

// 监控 Largest Contentful Paint
const lcpObserver = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries(); // 最后一个条目是最大的一个
  const lastEntry = entries[entries.length - 1];
  console.log("LCP candidate:", lastEntry.startTime, lastEntry); // 在这里将 LCP 值发送到你的监控服务
});

lcpObserver.observe({ entryTypes: ["largest-contentful-paint"] });

// 监控 Layout Shifts
let clsValue = 0;
const clsObserver = new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
      console.log("Current CLS value:", clsValue);
    }
  }
});
clsObserver.observe({ entryTypes: ["layout-shift"] });

requestIdleCallback

image.png

解决的问题: 一些非紧急任务(如日志上报、预加载非关键资源)如果与用户的关键操作(如动画、输入)争抢主线程,会导致卡顿。

API 简介window.requestIdleCallback()  方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。它还会提供一个 IdleDeadline 参数,告诉你还剩多少空闲时间。

性能提升点: 将任务拆分并在空闲时间执行,确保主线程优先响应用户交互,提升应用的流畅度。

function scheduleNonCriticalWork() {
  requestIdleCallback((deadline) => {
    // 如果当前帧的空闲时间还足够,或者任务不是特别紧急
    while (deadline.timeRemaining() > 0 && tasks.length > 0) {
      doSomeNonCriticalTask(tasks.shift());
    } // 如果任务还没做完,继续安排到下一个空闲期
    if (tasks.length > 0) {
      scheduleNonCriticalWork();
    }
  });
}

MutationObserver

image.png

解决的问题: 使用 setInterval 或监听不特定事件来检测 DOM 变化,效率低下且不准确。

API 简介MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

性能提升点: 批量、异步地处理 DOM 变化,避免在每次微小变化时都触发高开销的回调函数。

// 动态加载第三方插件时,确保其所需的 DOM 已存在
const pluginContainer = document.getElementById("plugin-container");
const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    if (mutation.type === "childList") {
      // 检查是否有新的节点添加,并且是第三方插件需要的结构
      if (document.querySelector(".third-party-widget")) {
        loadThirdPartyPlugin(); // 加载后即可断开观察
        observer.disconnect();
      }
    }
  }
});

observer.observe(pluginContainer, { childList: true, subtree: true });

Broadcast Channel API

image.png

解决的问题: 在多个同源标签页间共享状态(如登录状态、主题设置)通常使用 LocalStorage 事件,但该事件仅在非当前标签页触发,且同步 API 有性能风险。

API 简介BroadcastChannel 接口表示给定的任何浏览上下文都可以订阅的命名频道。它允许同源的不同浏览器窗口、标签页、frame 或者 iframe 下的不同文档之间相互通信。消息通过 message 事件进行广播,该事件在侦听该频道的所有 BroadcastChannel 对象上触发,发送消息的对象除外。

性能与体验提升点: 提供了一种比 LocalStorage 更直接、更高效、无性能风险的通信方式,避免了不必要的存储操作。

// 标签页 A - 发送消息
const broadcast = new BroadcastChannel("app-channel");
broadcast.postMessage({ type: "USER_LOGGED_IN", userId: 123 });

// 标签页 B - 接收消息
const broadcast = new BroadcastChannel("app-channel");
broadcast.onmessage = (event) => {
  if (event.data.type === "USER_LOGGED_IN") {
    // 更新本页面的用户状态
    updateUIForLoggedInUser(event.data.userId);
  }
};

Web Worker

image.png

解决的问题: JavaScript 是单线程的。复杂的计算(如图像处理、数据排序、加密)会阻塞主线程,导致页面无响应。

API 简介:Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,它们可以使用 XMLHttpRequest(尽管 responseXML 和 channel 属性总是为空)或 fetch(没有这些限制)执行 I/O。一旦创建,一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理器(反之亦然)。

性能提升点: 解放主线程,确保 UI 始终流畅,即使在进行繁重计算时。

主线程代码:

const myWorker = new Worker("worker.js");
// 向 Worker 发送数据
myWorker.postMessage(largeDataArray);

// 接收来自 Worker 的结果
myWorker.onmessage = function (e) {
  const processedData = e.data;
  console.log("处理完成的数据:", processedData);
};

worker.js:

// 在 Worker 内部监听消息
onmessage = function (e) {
  const data = e.data; // 执行一些昂贵的计算,不会阻塞主线程
  const result = expensiveCalculation(data); // 将结果发送回主线程
  postMessage(result);
};

function expensiveCalculation(data) {
  // ... 复杂的处理逻辑 ...
  return processedData;
}

ResizeObserver

image.png

解决的问题: 使用 window.resize 监听整个窗口变化,然后通过 getBoundingClientRect() 获取元素尺寸,效率低下且无法直接响应特定元素的大小变化。

API 简介ResizeObserver 接口监视 Element 内容盒或边框盒或者 SVGElement 边界尺寸的变化。

性能提升点: 提供了一种高性能、针对性的方式来响应布局变化,避免了在全局 resize 事件中执行大量重复的布局查询。

// 响应式图表重绘
const chartElement = document.getElementById("my-chart");
const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect; // 当图表容器尺寸变化时,重新绘制图表
    myChart.resize(width, height);
  }
});
resizeObserver.observe(chartElement);

fetchpriority

image.png

解决的问题: 浏览器虽然有自己的资源加载优先级算法,但并非总是完美。对于关键资源(如首屏英雄图像、关键 CSS),我们希望给予浏览器明确的提示。

API 简介<link> 元素的 fetchpriority 属性为浏览器提供了一个提示,指示它应如何相对于同类型的其他资源来优先获取特定资源。

性能提升点: 通过优先加载关键资源,延迟加载非关键资源,优化 LCP 和 FCP 指标。

<!-- 告诉浏览器这是一个高优先级的 LCP 候选元素 -->
<img src="hero-image.jpg" fetchpriority="high" alt="Hero Image">

<!-- 一个在页面底部的不重要图片,可以低优先级加载 -->
<img src="decoration.png" fetchpriority="low" alt="Decoration">