如何使用 JavaScript 检测空闲的浏览器标签页

2025-06-10

如何使用 JavaScript 检测空闲的浏览器标签页

在某些情况下,我们会发现自己在用户与最终​​产品或应用程序交互时执行了大量密集且耗时的 CPU 任务。
触发轮询器、建立 WebSocket 连接,甚至加载视频或图片等媒体文件都可能成为性能瓶颈,尤其是在这些任务毫无必要地消耗资源的情况下。当用户未主动与界面交互时,将主线程从不必要的工作负载或网络请求中释放出来是一种非常好且有意义的做法。另一方面,在大多数托管服务提供商都引入基于配额的定价模式的行业中,减少网络请求也可以降低运行应用程序或服务的成本。

页面可见性 API

所有现代网络浏览器都已集成页面可见性 API,它允许我们检测浏览器选项卡何时被隐藏,此外,我们还可以注册事件监听器以检测可见性变化时的信号。

document.visibilityState

document.visibilityState可能是visible当页面位于
非最小化窗口的前台选项卡中时,或者hidden当页面实际上对用户不可见时。

我们可以直接访问document.visibilityState

console.log(document.visibilityState);
// => It could be `visible` or `hidden`

visibilitychange 事件

我们还可以使用事件监听器轻松检测可见性属性的变化。

const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    console.log('> The window is hidden.');
  } else {
    console.log('> The window is visible.');
  }
};
document.addEventListener('visibilitychange', onVisibilityChange, false);

轮询示例

假设我们正在轮询 API 以获取更新,并且希望避免对空闲用户进行不必要的调用。一个简单的示例如下:

const poll = () => {
  const interval = 1500;
  let _poller = null;
  const repeat = () => {
    console.log(`~ Polling: ${Date.now()}.`);
  };

  return {
    start: () => {
      _poller = setInterval(repeat, interval);
    },
    stop: () => {
      console.log('~ Poller stopped.');
      clearInterval(_poller);
    }
  };
};

const poller = poll();
poller.start();

const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    poller.stop();
  } else {
    poller.start();
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

后台异步加载

但有时,我们可以通过反过来来加速用户的终端体验。与其取消所有作业和请求,不如异步加载外部依赖项或资源。这样,用户再次访问时,终端体验会更加“充实”、更加丰富。

Webpack

使用 ES2015 动态导入提案以及适当的 Webpack 配置清单,我们可以轻松地在后台加载其他模块或资产

let loaded = false;
const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    // Aggresively preload external assets ans scripts
    if (loaded) {
      return;
    }
    Promise.all([
      import('./async.js'),
      import('./another-async.js'),
      import(/* webpackChunkName: "bar-module" */ 'modules/bar'),
      import(/* webpackPrefetch: 0 */ 'assets/images/foo.jpg')
    ]).then(() => {
      loaded = true;
    });
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

汇总

Rollup 也支持开箱即用的动态导入。

let loaded = false;
const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    // Aggresively preload external assets ans scripts
    if (loaded) {
      return;
    }
    Promise.all([
      import('./modules.js').then(({default: DefaultExport, NamedExport}) => {
        // do something with modules.
      })
    ]).then(() => {
      loaded = true;
    });
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

使用 Javascript 进行预加载

除了使用捆绑器,我们还可以使用几行 JavaScript 预加载图像等静态资产。

let loaded = false;

const preloadImgs = (...imgs) => {
  const images = [];
  imgs.map(
    url =>
      new Promise((resolve, reject) => {
        images[i] = new Image();
        images[i].src = url;
        img.onload = () => resolve();
        img.onerror = () => reject();
      })
  );
};

const onVisibilityChange = () => {
  if (document.visibilityState === 'hidden') {
    // Aggresively preload external assets ans scripts
    if (loaded) {
      return;
    }
    Promise.all(
      preloadImgs(
        'https://example.com/foo.jpg',
        'https://example.com/qux.jpg',
        'https://example.com/bar.jpg'
      )
    )
      .then(() => {
        loaded = true;
      })
      .catch(() => {
        console.log('> Snap.');
      });
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

微交互

最后,吸引用户注意力的一个巧妙方法是动态地改变图标,只需使用几个像素就可以保持交互。

const onVisibilityChange = () => {
  const favicon = document.querySelector('[rel="shortcut icon"]');
  if (document.visibilityState === 'hidden') {
    favicon.href = '/come-back.png';
  } else {
    favicon.href = '/example.png';
  }
};

document.addEventListener('visibilitychange', onVisibilityChange, false);

参考

你也可以在vorillaz.com上找到这篇文章

鏂囩珷鏉ユ簮锛�https://dev.to/vorillaz/how-to-detect-idle-browser-tabs-with-javascript-88n
PREV
TypeScript 中的依赖注入
NEXT
JavaScript 和 IoT 入门