Angular多浏览器窗口下@ng-idle/core定时器卡顿冻结问题求助
解决方案
问题根源分析
- 多窗口倒计时冲突:每个窗口独立启动
setInterval,同时修改localStorage中的倒计时数值引发竞态条件;点击"保持登录"后仅当前窗口清除定时器,其他窗口的定时器仍在运行,导致数值混乱。 - @ng-idle配置错误:代码中
idle.setIdle(60)设置为1分钟无活动进入空闲,与需求的20分钟不符,提前触发逻辑导致异常。 - 性能与节流问题:频繁读写
localStorage+setInterval在后台标签页被浏览器节流,引发窗口冻结或倒计时卡顿。
具体修复步骤
1. 修正@ng-idle基础配置
将空闲触发时间改为20分钟(1200秒),匹配需求:
// 原错误配置 // idle.setIdle(60); // 修改为 idle.setIdle(1200); // 20分钟无活动进入空闲 idle.setTimeout(60); // 空闲后60秒倒计时登出
2. 重构多窗口同步逻辑
- 存储倒计时结束时间戳而非剩余秒数,减少
localStorage读写次数 - 用窗口唯一标识标记主导倒计时的窗口,避免多窗口重复启动定时器
- 通过
storage事件同步所有窗口的状态
3. 优化定时器实现
用requestAnimationFrame替代setInterval,适配浏览器后台节流机制,避免定时器累积导致的性能问题。
修改后的完整代码示例
import { ChangeDetectorRef, Component, OnInit, OnDestroy } from "@angular/core"; import { MenuItem } from "primeng/api"; import { LoginService } from "./components/login/login.service"; import { Idle, DEFAULT_INTERRUPTSOURCES } from "@ng-idle/core"; import { DialogService } from "primeng/dynamicdialog"; import { Keepalive } from "@ng-idle/keepalive"; @Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.css"], providers: [DialogService], }) export class AppComponent implements OnInit, OnDestroy { items: MenuItem[]; title = "maincontainer"; idleState = "NOT_STARTED"; countdown?: number = null; lastPing?: Date = null; showAutoLogout: boolean = false; showSpinner: boolean = false; private countdownTimer: any; private windowId: string = Math.random().toString(36).substring(2, 15); // 当前窗口唯一标识 constructor( public loginService: LoginService, private idle: Idle, private keepalive: Keepalive, private cd: ChangeDetectorRef ) { // 初始化localStorage状态 if (!localStorage.getItem('countdownEndTime')) localStorage.setItem('countdownEndTime', '0'); if (!localStorage.getItem('isCounting')) localStorage.setItem('isCounting', 'false'); // 修正空闲配置:20分钟无活动进入空闲,空闲后60秒倒计时 idle.setIdle(1200); idle.setTimeout(60); idle.setInterrupts(DEFAULT_INTERRUPTSOURCES); idle.onIdleStart.subscribe(() => { this.idleState = "IDLE"; idle.clearInterrupts(); this.checkLogin(); }); idle.onIdleEnd.subscribe(() => { if (localStorage.getItem("bearerToken")) { this.idleState = "NOT_IDLE"; this.reset(); } else { this.logout(); } }); idle.onTimeout.subscribe(() => { this.idleState = "TIMED_OUT"; this.countdown = null; this.showAutoLogout = false; this.logout(); }); idle.onTimeoutWarning.subscribe(() => { this.checkLogin(); this.showAutoLogout = true; // 仅当无窗口在倒计时时,当前窗口启动倒计时 if (localStorage.getItem('isCounting') === "false") this.startCountdown(); }); keepalive.onPing.subscribe(() => { this.lastPing = new Date(); }); } ngOnInit(): void { // 监听localStorage变化,同步所有窗口状态 window.addEventListener('storage', (event) => { if (event.key === 'countdownEndTime' || event.key === 'isCounting') { this.updateCountdownDisplay(); } else if (event.key === "loggedIn") { localStorage.getItem("loggedIn") === 'true' ? window.location.reload() : window.location.href = '/login'; } }); if (this.loginService.isLoggedIn()) this.reset(); this.loginService.userLoggedIn.subscribe(() => this.reset()); this.loginService.userLoggedOut.subscribe(() => { localStorage.clear(); window.localStorage.setItem('loggedIn', 'false'); window.location.reload(); }); // 初始化显示倒计时 this.updateCountdownDisplay(); } ngOnDestroy(): void { // 组件销毁时清除定时器并重置状态 this.clearCountdownTimer(); if (localStorage.getItem('countdownOwner') === this.windowId) { localStorage.setItem('isCounting', 'false'); localStorage.setItem('countdownOwner', ''); } } private startCountdown(): void { // 设置倒计时结束时间:当前时间+60秒 const endTime = Date.now() + 60 * 1000; localStorage.setItem('countdownEndTime', endTime.toString()); localStorage.setItem('isCounting', 'true'); localStorage.setItem('countdownOwner', this.windowId); // 使用requestAnimationFrame实现倒计时,适配浏览器节流 const updateCountdown = () => { const now = Date.now(); const remaining = Math.max(0, Math.floor((+localStorage.getItem('countdownEndTime')! - now) / 1000)); this.countdown = remaining; this.cd.detectChanges(); if (remaining <= 0) { this.showAutoLogout = false; this.logout(); this.clearCountdownTimer(); localStorage.setItem('isCounting', 'false'); localStorage.setItem('countdownOwner', ''); } else { this.countdownTimer = requestAnimationFrame(updateCountdown); } }; this.clearCountdownTimer(); updateCountdown(); } private updateCountdownDisplay(): void { if (localStorage.getItem('isCounting') === "true") { const now = Date.now(); const remaining = Math.max(0, Math.floor((+localStorage.getItem('countdownEndTime')! - now) / 1000)); this.countdown = remaining; this.showAutoLogout = true; this.cd.detectChanges(); } else { this.countdown = null; this.showAutoLogout = false; } } private clearCountdownTimer(): void { if (this.countdownTimer) { cancelAnimationFrame(this.countdownTimer); this.countdownTimer = null; } } checkLogin(): void { if (!this.loginService.isLoggedIn()) { this.idle.stop(); window.location.href = "/login"; } } reset(): void { // 重置所有倒计时状态 this.clearCountdownTimer(); localStorage.setItem('isCounting', 'false'); localStorage.setItem('countdownEndTime', '0'); localStorage.setItem('countdownOwner', ''); this.idle.stop(); this.idle.setInterrupts(DEFAULT_INTERRUPTSOURCES); this.idleState = "NOT_IDLE"; this.cd.detectChanges(); this.idle.watch(); this.countdown = null; this.lastPing = null; this.showAutoLogout = false; } logout(): void { this.showSpinner = true; const token = localStorage.getItem("bearerToken"); const logoutData = { loginId: localStorage.getItem("loginId"), loginReportId: localStorage.getItem("loginReportId"), }; if (!this.loginService.isLoggedIn()) { this.loginService.userLoggedOut.emit(); return; } this.loginService.logout(logoutData, token).subscribe( () => { this.showSpinner = false; this.loginService.userLoggedOut.emit(); this.idle.stop(); }, () => { this.showSpinner = false; this.loginService.userLoggedOut.emit(); this.idle.stop(); } ); } }
关键修改说明
- 倒计时逻辑重构:通过结束时间戳计算剩余秒数,大幅减少
localStorage读写操作,避免竞态条件。 - 多窗口协作:用
windowId标记主导倒计时的窗口,配合storage事件同步所有窗口的显示状态。 - 性能优化:
requestAnimationFrame适配浏览器后台节流,避免定时器累积导致的窗口冻结;组件销毁时清理定时器,防止内存泄漏。 - 配置修正:将空闲触发时间改为20分钟,完全匹配需求。
内容的提问来源于stack exchange,提问作者Saurin Shah
相关产品推荐
相关产品推荐

