perf: Socket 连接异步化,使用 requestIdleCallback 不阻塞首屏
- NotificationContext: 将 Socket 初始化包裹在 requestIdleCallback 中 - 设置 3 秒超时保护,确保连接不会被无限延迟 - 不支持 requestIdleCallback 的浏览器自动降级到 setTimeout(0) - socket/index.js: 移除模块加载时的 console.log,减少首屏阻塞感知 - 所有页面的首屏渲染都不再被 Socket 连接阻塞 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -649,183 +649,213 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
}, [adaptEventToNotification]);
|
}, [adaptEventToNotification]);
|
||||||
|
|
||||||
|
|
||||||
// ========== 连接到 Socket 服务(⚡ 方案2: 只执行一次) ==========
|
// ========== 连接到 Socket 服务(⚡ 异步初始化,不阻塞首屏) ==========
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.info('NotificationContext', '初始化 Socket 连接(方案2:只注册一次)');
|
let cleanupCalled = false;
|
||||||
|
let idleCallbackId;
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
// ========== 监听连接成功(首次连接 + 重连) ==========
|
// ⚡ Socket 初始化函数(将在浏览器空闲时执行)
|
||||||
socket.on('connect', () => {
|
const initSocketConnection = () => {
|
||||||
setIsConnected(true);
|
if (cleanupCalled) return; // 防止组件卸载后执行
|
||||||
setReconnectAttempt(0);
|
|
||||||
|
|
||||||
// 判断是首次连接还是重连
|
logger.info('NotificationContext', '初始化 Socket 连接(异步执行,不阻塞首屏)');
|
||||||
if (isFirstConnect.current) {
|
|
||||||
logger.info('NotificationContext', '首次连接成功', {
|
|
||||||
socketId: socket.getSocketId?.()
|
|
||||||
});
|
|
||||||
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
|
|
||||||
isFirstConnect.current = false;
|
|
||||||
} else {
|
|
||||||
logger.info('NotificationContext', '重连成功');
|
|
||||||
setConnectionStatus(CONNECTION_STATUS.RECONNECTED);
|
|
||||||
|
|
||||||
// 清除之前的定时器
|
// ========== 监听连接成功(首次连接 + 重连) ==========
|
||||||
if (reconnectedTimerRef.current) {
|
socket.on('connect', () => {
|
||||||
clearTimeout(reconnectedTimerRef.current);
|
setIsConnected(true);
|
||||||
|
setReconnectAttempt(0);
|
||||||
|
|
||||||
|
// 判断是首次连接还是重连
|
||||||
|
if (isFirstConnect.current) {
|
||||||
|
logger.info('NotificationContext', '首次连接成功', {
|
||||||
|
socketId: socket.getSocketId?.()
|
||||||
|
});
|
||||||
|
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
|
||||||
|
isFirstConnect.current = false;
|
||||||
|
} else {
|
||||||
|
logger.info('NotificationContext', '重连成功');
|
||||||
|
setConnectionStatus(CONNECTION_STATUS.RECONNECTED);
|
||||||
|
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (reconnectedTimerRef.current) {
|
||||||
|
clearTimeout(reconnectedTimerRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2秒后自动变回 CONNECTED
|
||||||
|
reconnectedTimerRef.current = setTimeout(() => {
|
||||||
|
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
|
||||||
|
logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status');
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2秒后自动变回 CONNECTED
|
// ⚡ 重连后只需重新订阅,不需要重新注册监听器
|
||||||
reconnectedTimerRef.current = setTimeout(() => {
|
logger.info('NotificationContext', '重新订阅事件推送');
|
||||||
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
|
|
||||||
logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⚡ 重连后只需重新订阅,不需要重新注册监听器
|
if (socket.subscribeToEvents) {
|
||||||
logger.info('NotificationContext', '重新订阅事件推送');
|
socket.subscribeToEvents({
|
||||||
|
eventType: 'all',
|
||||||
if (socket.subscribeToEvents) {
|
importance: 'all',
|
||||||
socket.subscribeToEvents({
|
onSubscribed: (data) => {
|
||||||
eventType: 'all',
|
logger.info('NotificationContext', '订阅成功', data);
|
||||||
importance: 'all',
|
},
|
||||||
onSubscribed: (data) => {
|
});
|
||||||
logger.info('NotificationContext', '订阅成功', data);
|
} else {
|
||||||
},
|
logger.error('NotificationContext', 'socket.subscribeToEvents 方法不可用');
|
||||||
});
|
}
|
||||||
} else {
|
|
||||||
logger.error('NotificationContext', 'socket.subscribeToEvents 方法不可用');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 监听断开连接 ==========
|
|
||||||
socket.on('disconnect', (reason) => {
|
|
||||||
setIsConnected(false);
|
|
||||||
setConnectionStatus(CONNECTION_STATUS.DISCONNECTED);
|
|
||||||
logger.warn('NotificationContext', 'Socket 已断开', { reason });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 监听连接错误 ==========
|
|
||||||
socket.on('connect_error', (error) => {
|
|
||||||
logger.error('NotificationContext', 'Socket connect_error', error);
|
|
||||||
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
|
|
||||||
|
|
||||||
const attempts = socket.getReconnectAttempts?.() || 0;
|
|
||||||
setReconnectAttempt(attempts);
|
|
||||||
logger.info('NotificationContext', `重连中... (第 ${attempts} 次尝试)`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 监听重连失败 ==========
|
|
||||||
socket.on('reconnect_failed', () => {
|
|
||||||
logger.error('NotificationContext', '重连失败');
|
|
||||||
setConnectionStatus(CONNECTION_STATUS.FAILED);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: '连接失败',
|
|
||||||
description: '无法连接到服务器,请检查网络连接',
|
|
||||||
status: 'error',
|
|
||||||
duration: null,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数) ==========
|
// ========== 监听断开连接 ==========
|
||||||
socket.on('new_event', (data) => {
|
socket.on('disconnect', (reason) => {
|
||||||
logger.info('NotificationContext', '收到 new_event 事件', {
|
setIsConnected(false);
|
||||||
id: data?.id,
|
setConnectionStatus(CONNECTION_STATUS.DISCONNECTED);
|
||||||
title: data?.title,
|
logger.warn('NotificationContext', 'Socket 已断开', { reason });
|
||||||
eventType: data?.event_type || data?.type,
|
|
||||||
importance: data?.importance
|
|
||||||
});
|
});
|
||||||
logger.debug('NotificationContext', '原始事件数据', data);
|
|
||||||
|
|
||||||
// ⚠️ 防御性检查:确保 ref 已初始化
|
// ========== 监听连接错误 ==========
|
||||||
if (!addNotificationRef.current || !adaptEventToNotificationRef.current) {
|
socket.on('connect_error', (error) => {
|
||||||
logger.error('NotificationContext', 'Ref 未初始化,跳过处理', {
|
logger.error('NotificationContext', 'Socket connect_error', error);
|
||||||
addNotificationRef: !!addNotificationRef.current,
|
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
|
||||||
adaptEventToNotificationRef: !!adaptEventToNotificationRef.current,
|
|
||||||
|
const attempts = socket.getReconnectAttempts?.() || 0;
|
||||||
|
setReconnectAttempt(attempts);
|
||||||
|
logger.info('NotificationContext', `重连中... (第 ${attempts} 次尝试)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========== 监听重连失败 ==========
|
||||||
|
socket.on('reconnect_failed', () => {
|
||||||
|
logger.error('NotificationContext', '重连失败');
|
||||||
|
setConnectionStatus(CONNECTION_STATUS.FAILED);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: '连接失败',
|
||||||
|
description: '无法连接到服务器,请检查网络连接',
|
||||||
|
status: 'error',
|
||||||
|
duration: null,
|
||||||
|
isClosable: true,
|
||||||
});
|
});
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Socket层去重检查 ==========
|
// ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数) ==========
|
||||||
const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
socket.on('new_event', (data) => {
|
||||||
|
logger.info('NotificationContext', '收到 new_event 事件', {
|
||||||
if (!data.id) {
|
id: data?.id,
|
||||||
logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
|
title: data?.title,
|
||||||
eventId,
|
eventType: data?.event_type || data?.type,
|
||||||
eventType: data.type,
|
importance: data?.importance
|
||||||
title: data.title,
|
|
||||||
});
|
});
|
||||||
}
|
logger.debug('NotificationContext', '原始事件数据', data);
|
||||||
|
|
||||||
if (processedEventIds.current.has(eventId)) {
|
// ⚠️ 防御性检查:确保 ref 已初始化
|
||||||
logger.warn('NotificationContext', '重复事件已忽略', { eventId });
|
if (!addNotificationRef.current || !adaptEventToNotificationRef.current) {
|
||||||
return;
|
logger.error('NotificationContext', 'Ref 未初始化,跳过处理', {
|
||||||
}
|
addNotificationRef: !!addNotificationRef.current,
|
||||||
|
adaptEventToNotificationRef: !!adaptEventToNotificationRef.current,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
processedEventIds.current.add(eventId);
|
// ========== Socket层去重检查 ==========
|
||||||
logger.debug('NotificationContext', '事件已记录,防止重复处理', { eventId });
|
const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
// 限制 Set 大小,避免内存泄漏
|
if (!data.id) {
|
||||||
if (processedEventIds.current.size > MAX_PROCESSED_IDS) {
|
logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
|
||||||
const idsArray = Array.from(processedEventIds.current);
|
eventId,
|
||||||
processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS));
|
eventType: data.type,
|
||||||
logger.debug('NotificationContext', 'Cleaned up old processed event IDs', {
|
title: data.title,
|
||||||
kept: MAX_PROCESSED_IDS,
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
// ========== Socket层去重检查结束 ==========
|
|
||||||
|
|
||||||
// ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱)
|
if (processedEventIds.current.has(eventId)) {
|
||||||
logger.debug('NotificationContext', '正在转换事件格式');
|
logger.warn('NotificationContext', '重复事件已忽略', { eventId });
|
||||||
const notification = adaptEventToNotificationRef.current(data);
|
return;
|
||||||
logger.debug('NotificationContext', '转换后的通知对象', notification);
|
}
|
||||||
|
|
||||||
// ✅ 使用 ref.current 访问最新的 addNotification 函数
|
processedEventIds.current.add(eventId);
|
||||||
logger.debug('NotificationContext', '准备添加通知到队列');
|
logger.debug('NotificationContext', '事件已记录,防止重复处理', { eventId });
|
||||||
addNotificationRef.current(notification);
|
|
||||||
logger.info('NotificationContext', '通知已添加到队列');
|
|
||||||
|
|
||||||
// ⚡ 调用所有注册的事件更新回调(用于通知其他组件刷新数据)
|
// 限制 Set 大小,避免内存泄漏
|
||||||
if (eventUpdateCallbacks.current.size > 0) {
|
if (processedEventIds.current.size > MAX_PROCESSED_IDS) {
|
||||||
logger.debug('NotificationContext', `触发 ${eventUpdateCallbacks.current.size} 个事件更新回调`);
|
const idsArray = Array.from(processedEventIds.current);
|
||||||
eventUpdateCallbacks.current.forEach(callback => {
|
processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS));
|
||||||
try {
|
logger.debug('NotificationContext', 'Cleaned up old processed event IDs', {
|
||||||
callback(data);
|
kept: MAX_PROCESSED_IDS,
|
||||||
} catch (error) {
|
});
|
||||||
logger.error('NotificationContext', '事件更新回调执行失败', error);
|
}
|
||||||
}
|
// ========== Socket层去重检查结束 ==========
|
||||||
});
|
|
||||||
logger.debug('NotificationContext', '所有事件更新回调已触发');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ========== 监听系统通知(兼容性) ==========
|
// ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱)
|
||||||
socket.on('system_notification', (data) => {
|
logger.debug('NotificationContext', '正在转换事件格式');
|
||||||
logger.info('NotificationContext', '收到系统通知', data);
|
const notification = adaptEventToNotificationRef.current(data);
|
||||||
|
logger.debug('NotificationContext', '转换后的通知对象', notification);
|
||||||
|
|
||||||
if (addNotificationRef.current) {
|
// ✅ 使用 ref.current 访问最新的 addNotification 函数
|
||||||
addNotificationRef.current(data);
|
logger.debug('NotificationContext', '准备添加通知到队列');
|
||||||
} else {
|
addNotificationRef.current(notification);
|
||||||
logger.error('NotificationContext', 'addNotificationRef 未初始化');
|
logger.info('NotificationContext', '通知已添加到队列');
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info('NotificationContext', '所有监听器已注册(只注册一次)');
|
// ⚡ 调用所有注册的事件更新回调(用于通知其他组件刷新数据)
|
||||||
|
if (eventUpdateCallbacks.current.size > 0) {
|
||||||
|
logger.debug('NotificationContext', `触发 ${eventUpdateCallbacks.current.size} 个事件更新回调`);
|
||||||
|
eventUpdateCallbacks.current.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(data);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('NotificationContext', '事件更新回调执行失败', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
logger.debug('NotificationContext', '所有事件更新回调已触发');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ========== 获取最大重连次数 ==========
|
// ========== 监听系统通知(兼容性) ==========
|
||||||
const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity;
|
socket.on('system_notification', (data) => {
|
||||||
setMaxReconnectAttempts(maxAttempts);
|
logger.info('NotificationContext', '收到系统通知', data);
|
||||||
logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts });
|
|
||||||
|
|
||||||
// ========== 启动连接 ==========
|
if (addNotificationRef.current) {
|
||||||
logger.info('NotificationContext', '调用 socket.connect()');
|
addNotificationRef.current(data);
|
||||||
socket.connect();
|
} else {
|
||||||
|
logger.error('NotificationContext', 'addNotificationRef 未初始化');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('NotificationContext', '所有监听器已注册');
|
||||||
|
|
||||||
|
// ========== 获取最大重连次数 ==========
|
||||||
|
const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity;
|
||||||
|
setMaxReconnectAttempts(maxAttempts);
|
||||||
|
logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts });
|
||||||
|
|
||||||
|
// ========== 启动连接 ==========
|
||||||
|
logger.info('NotificationContext', '调用 socket.connect()');
|
||||||
|
socket.connect();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⚡ 使用 requestIdleCallback 在浏览器空闲时初始化 Socket
|
||||||
|
// 降级到 setTimeout(0) 以兼容不支持的浏览器(如 Safari)
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
idleCallbackId = window.requestIdleCallback(initSocketConnection, {
|
||||||
|
timeout: 3000 // 最多等待 3 秒,确保连接不会延迟太久
|
||||||
|
});
|
||||||
|
logger.debug('NotificationContext', 'Socket 初始化已排入 requestIdleCallback');
|
||||||
|
} else {
|
||||||
|
timeoutId = setTimeout(initSocketConnection, 0);
|
||||||
|
logger.debug('NotificationContext', 'Socket 初始化已排入 setTimeout(0)(降级模式)');
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 清理函数(组件卸载时) ==========
|
// ========== 清理函数(组件卸载时) ==========
|
||||||
return () => {
|
return () => {
|
||||||
|
cleanupCalled = true;
|
||||||
logger.info('NotificationContext', '清理 Socket 连接');
|
logger.info('NotificationContext', '清理 Socket 连接');
|
||||||
|
|
||||||
|
// 取消待执行的初始化
|
||||||
|
if (idleCallbackId && 'cancelIdleCallback' in window) {
|
||||||
|
window.cancelIdleCallback(idleCallbackId);
|
||||||
|
}
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
// 清理 reconnected 状态定时器
|
// 清理 reconnected 状态定时器
|
||||||
if (reconnectedTimerRef.current) {
|
if (reconnectedTimerRef.current) {
|
||||||
clearTimeout(reconnectedTimerRef.current);
|
clearTimeout(reconnectedTimerRef.current);
|
||||||
|
|||||||
@@ -10,25 +10,12 @@ import { socketService } from '../socketService';
|
|||||||
export const socket = socketService;
|
export const socket = socketService;
|
||||||
export { socketService };
|
export { socketService };
|
||||||
|
|
||||||
// ⚡ 新增:暴露 Socket 实例到 window(用于调试和验证)
|
// ⚡ 暴露 Socket 实例到 window(用于调试和验证)
|
||||||
|
// 注意:移除首屏加载时的日志,避免阻塞感知
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.socket = socketService;
|
window.socket = socketService;
|
||||||
window.socketService = socketService;
|
window.socketService = socketService;
|
||||||
|
// 日志已移除,如需调试可在控制台执行: console.log(window.socket)
|
||||||
console.log(
|
|
||||||
'%c[Socket Service] ✅ Socket instance exposed to window',
|
|
||||||
'color: #4CAF50; font-weight: bold; font-size: 14px;'
|
|
||||||
);
|
|
||||||
console.log(' 📍 window.socket:', window.socket);
|
|
||||||
console.log(' 📍 window.socketService:', window.socketService);
|
|
||||||
console.log(' 📍 Socket.IO instance:', window.socket?.socket);
|
|
||||||
console.log(' 📍 Connection status:', window.socket?.connected ? '✅ Connected' : '❌ Disconnected');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印当前使用的服务类型
|
|
||||||
console.log(
|
|
||||||
'%c[Socket Service] Using REAL Socket Service',
|
|
||||||
'color: #4CAF50; font-weight: bold; font-size: 12px;'
|
|
||||||
);
|
|
||||||
|
|
||||||
export default socket;
|
export default socket;
|
||||||
|
|||||||
Reference in New Issue
Block a user