Skip to main content

确认:根因是 Chrome Freeze(标签页冻结)机制

你的判断非常准确!我之前分析的"macOS 睡眠导致 GPU Context 丢失"是触发条件,而 Chrome Freeze直接机制。两者叠加导致了图片不渲染的问题。


先看快速复现结果及简单fix lazyimage

Freeze 标签页后恢复前页面截图 1

Freeze 标签页后恢复前页面截图 2

Freeze 标签页后恢复前页面截图 3

Chrome Freeze 机制详解

什么是 Chrome Freeze?

Chrome 133 开始,Chrome 引入了更激进的标签页冻结(Tab Freezing) 机制:

当标签页在后台停留时间过长,或被系统判定为"非活跃"状态时,Chrome 会冻结(Freeze) 该标签页的所有 JavaScript 任务、定时器、渲染更新和 GPU 资源。

Freeze 和 Discard 的区别

机制触发条件效果恢复方式
Freeze(冻结)后台停留 5 分钟以上 / 系统内存压力大暂停 JS 执行、渲染、GPU 合成;DOM 和内存状态保留切回标签页时自动解冻,但不会自动重绘
Discard(废弃)内存严重不足 / chrome://discards 手动触发完全卸载页面,释放所有资源;需要重新加载切回标签页时重新加载整个页面

你的场景:Chrome Freeze + macOS 睡眠

完整链条:

合盖睡眠 (macOS)

Chrome 进入后台 → Chrome 判定为"非活跃"

触发 Freeze(冻结标签页)

暂停渲染、GPU 合成、JS 任务

GPU 显存中的图片像素数据被释放(但不释放 DOM 和 HTTP 缓存)

唤醒后,Chrome 解冻标签页

❌ 但合成器(Compositor)没有主动触发重绘

图片空白(因为 DOM 在,但像素数据没了)

为什么滚动一下就好了?

滚动触发 IntersectionObserver → 虚拟列表 unmount/remount <img>强制合成器重新绘制 → 图片恢复。

我们的解决方案就是:不等用户滚动,在唤醒时主动触发可见图片的重绘。


验证方法

最快验证:使用 chrome://discards

  1. 打开新标签页,访问 chrome://discards
  2. 找到你的页面标签页
  3. 点击 Discard(这会立即触发类似 Freeze 的资源回收)
  4. 切回你的页面
  5. 如果图片空白 → 确认是 Freeze/Discard 机制导致

查看标签页状态

chrome://discards 中,你可以看到每个标签页的:

  • 状态:Active / Frozen / Discarded
  • 最后活跃时间
  • 内存占用

如果你的标签页显示为 Frozen,说明被冻结了。


修正确认后的解决方案

既然根因是 Chrome Freeze + 唤醒后未自动重绘,我们的方案可以更简单、更精准

简化版 Hook(针对 Freeze 场景)

💡 注意:Freeze 唤醒时一定会触发 visibilitychange(因为页面从后台切回前台),所以不需要心跳检测和 pageshow 事件了。

import { useState, useEffect, useRef, useCallback } from 'react';

interface UseWakeUpRefreshOptions {
scrollRoot?: HTMLElement | null;
rootMargin?: string;
threshold?: number;
}

export function useWakeUpRefresh(options: UseWakeUpRefreshOptions = {}) {
const { scrollRoot, rootMargin = '100px', threshold = 0.01 } = options;

const [inView, setInView] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const [node, setNode] = useState<HTMLElement | null>(null);
const inViewRef = useRef(inView);
inViewRef.current = inView;

// 主动检测元素是否在视口内
const checkInView = useCallback((): boolean => {
if (!node) return false;
const rect = node.getBoundingClientRect();
const rootEl = scrollRoot ?? document.documentElement;
if (!rootEl) return false;
const rootRect = rootEl.getBoundingClientRect();
return rect.top < rootRect.bottom && rect.bottom > rootRect.top;
}, [node, scrollRoot]);

// 强制刷新:仅在页面可见且元素在视口内时触发
const forceRefresh = useCallback(() => {
if (document.visibilityState === 'visible') {
const visible = checkInView();
if (visible) {
// 如果元素可见但 inView 为 false,先修正
if (!inViewRef.current) {
setInView(true);
}
// 触发图片重绘
setRefreshKey(prev => prev + 1);
}
}
}, [checkInView]);

// 监听唤醒(Freeze 解冻 + 切回前台)
useEffect(() => {
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// 使用 requestAnimationFrame 确保布局稳定
requestAnimationFrame(() => {
forceRefresh();
});
}
};

// focus 事件捕获用户点击/唤醒
const onFocus = () => {
requestAnimationFrame(() => {
forceRefresh();
});
};

document.addEventListener('visibilitychange', onVisibilityChange);
window.addEventListener('focus', onFocus);

// 初始化检测
const timer = setTimeout(() => {
requestAnimationFrame(() => {
const visible = checkInView();
if (visible) setInView(true);
});
}, 100);

return () => {
clearTimeout(timer);
document.removeEventListener('visibilitychange', onVisibilityChange);
window.removeEventListener('focus', onFocus);
};
}, [forceRefresh, checkInView]);

// IntersectionObserver 用于滚动时的懒加载
useEffect(() => {
if (!node) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0]?.isIntersecting) {
setInView(true);
}
},
{ root: scrollRoot ?? null, rootMargin, threshold }
);
observer.observe(node);
return () => observer.disconnect();
}, [node, scrollRoot, rootMargin, threshold]);

return {
ref: setNode,
inView,
refreshKey,
forceRefresh,
};
}

为什么这次一定能解决?

之前的问题现在的修复
inView 是单向门,唤醒后不更新唤醒时主动调用 checkInView() 修正
visibilitychange 检测到但 inView 为 falseforceRefresh 中主动检测并修正 inView
依赖 IntersectionObserver 被动触发唤醒时主动检测,不等待 Observer
担心 visibilitychange 不触发Freeze 唤醒时必定触发 visibilitychange

测试方法(10 秒验证)

# 1. 打开页面,滚动让图片加载
# 2. 新标签页打开 chrome://discards
# 3. 找到你的页面,点击 "Discard"
# 4. 切回你的页面
# 5. ✅ 图片应该自动恢复

如果还是空白,在 Console 中执行:

// 手动触发刷新
document.dispatchEvent(new Event('visibilitychange'));

看图片是否恢复。如果恢复了,说明 forceRefresh 逻辑没问题,只是事件监听没触发,可以检查 visibilityState 是否正确。


总结

维度说明
根因Chrome 在后台冻结(Freeze)标签页,释放 GPU 显存,唤醒后未主动重绘
触发条件合盖睡眠 / 后台停留过久 / 系统内存压力
关键证据chrome://discards 中标签页显示为 Frozen / Discarded
解决方案监听 visibilitychange + focus,唤醒时主动检测可见性并刷新 key
检测方法chrome://discards 点击 Discard,10 秒内验证