Page Freeze Overlay Effect Bug
Comprehensive documentation system guides
🐛 Critical Bug: Page Freeze Due to Effect + LoadingOverlay Pattern
Date Fixed: 2026-01-29
Session: 73.25-73.26
Severity: Critical (Page Unresponsive)
Components: AppComponent, AppLoadingService, LoadingOverlayService
📋 Summary
The docs-angular app was freezing immediately after loading the root URL (/), causing Chrome to show "Page Unresponsive" dialogs. The issue was a subtle interaction between Angular signals, effects, and the loading overlay service.
🔴 The Problem
Symptoms
- App loads, navigation sidebar appears
- Content area shows "Loading Public" overlay
- Browser freezes within seconds
- Chrome shows "Page Unresponsive" dialog
- CPU spikes to 100%
Misleading Investigation Path
Initially investigated:
- ❌ Routing issues (lazy loading)
- ❌ DynamicPageComponent initialization
- ❌ Guard race conditions
- ❌ NGXS state management timing
The actual component WAS loading (proven by adding debug output that appeared), but something else was freezing the page.
🎯 Root Cause
The issue was in AppComponent's constructor effect:
// ❌ BROKEN CODE - DO NOT USE THIS PATTERN
effect(() => {
const isLoading = this.appLoadingService.isLoading();
const currentMessage = this.appLoadingService.currentMessage(); // ⚠️ Changes every 2.5s!
const currentProject = this.currentProject();
if (isLoading) {
this.loadingService.showOverlay({...}); // ❌ Called EVERY time any signal changes!
} else {
this.loadingService.hideOverlay();
}
});
Why This Caused a Freeze
Message Cycling:
AppLoadingServicehas asetIntervalthat cycles through loading messages every 2.5 seconds, updating_loadingStatessignalEffect Re-execution: The effect read
currentMessage(), so it re-ran every 2.5 secondsRepeated
showOverlay()Calls: Each time the effect ran, it calledloadingService.showOverlay()Internal setInterval Cascade: The
LoadingOverlayService.show()method contains:if (this.isInitialLoaderActive()) { const checkInterval = setInterval(() => { // Poll every 50ms }, 50); }If called while the initial HTML loader is still active, it creates a NEW setInterval each time.
Interval Accumulation: Multiple 50ms intervals piled up, each running their callbacks, causing exponential CPU usage and browser freeze.
✅ The Fix
Track overlay state separately and only call showOverlay() once:
// ✅ CORRECT PATTERN
private readonly _overlayShown = signal(false);
effect(() => {
const isLoading = this.appLoadingService.isLoading();
// Only react to loading STATE changes, not message changes
if (isLoading && !this._overlayShown()) {
this._overlayShown.set(true);
// Read other signals INSIDE the guard
const currentProject = this.currentProject();
const currentMessage = this.appLoadingService.currentMessage();
this.loadingService.showOverlay({
title: `Loading ${effectiveLogoText}`,
subtitle: currentMessage,
// ...
});
} else if (!isLoading && this._overlayShown()) {
this._overlayShown.set(false);
this.loadingService.hideOverlay();
}
});
🛡️ Prevention Rules
Rule 1: Never Read Frequently-Changing Signals in Effects That Call Services
// ❌ BAD: Signal changes frequently, effect runs often
effect(() => {
const message = this.service.frequentlyChangingSignal();
this.someService.doSomething(); // Called too often!
});
// ✅ GOOD: Guard first, read inside
effect(() => {
const shouldAct = this.service.stateSignal();
if (shouldAct && !this._hasActed()) {
this._hasActed.set(true);
const message = this.service.frequentlyChangingSignal(); // Read inside guard
this.someService.doSomething();
}
});
Rule 2: Track "Already Done" State for One-Time Actions
private readonly _actionPerformed = signal(false);
effect(() => {
const trigger = this.someCondition();
if (trigger && !this._actionPerformed()) {
this._actionPerformed.set(true);
this.performExpensiveAction();
} else if (!trigger && this._actionPerformed()) {
this._actionPerformed.set(false);
this.cleanupAction();
}
});
Rule 3: Audit Services for Internal Intervals
Before calling a service method in an effect, check if that method:
- Creates
setIntervalorsetTimeout - Allocates DOM elements
- Makes HTTP requests
- Does any other non-idempotent operation
If yes, ensure the effect has guards to prevent repeated calls.
📁 Files Changed
| File | Change |
|---|---|
apps/docs-angular/src/app/app.component.ts |
Added _overlayShown signal, fixed effect logic |
apps/docs-angular/src/app/shared/docs-page/docs-page.component.ts |
Fixed effect to set lastLoadedPath BEFORE async operation |
apps/docs-angular/src/app/shared/enhanced-markdown-renderer/enhanced-markdown-renderer.component.scss |
Moved CSS declarations before @media rules (Sass deprecation fix) |
🔄 Related Issue: Circuit Breaker Triggering
Symptom: 🚨 EMERGENCY CIRCUIT BREAKER ACTIVATED for /docs/english/AI/index.mdc - 11 requests in 5s!
Cause: Same effect timing issue - lastLoadedPath was set AFTER the async operation completed, so the effect could re-run during the operation and trigger multiple loads.
Fix: Set lastLoadedPath = currentPath BEFORE calling handleRouteChange():
// ❌ WRONG - Multiple loads can happen
effect(() => {
if (currentPath !== this.lastLoadedPath) {
this.handleRouteChange(currentPath).then(() => {
this.lastLoadedPath = currentPath; // Set AFTER - too late!
});
}
});
// ✅ CORRECT - Prevent duplicate loads
effect(() => {
if (currentPath !== this.lastLoadedPath) {
this.lastLoadedPath = currentPath; // Set BEFORE - prevents re-entry
this.handleRouteChange(currentPath);
}
});
🔄 Related Issue: Missing Content Caching for Folder Loads
Symptom: Circuit breaker still triggers because loadFolderContentEfficiently was only caching MISSING files, not SUCCESSFUL ones.
Cause: When loading index.mdc files that exist, every request was hitting the server because successful loads weren't cached.
Fix: Added folderContentCache Map and activeFolderRequests deduplication:
// 🚀 CACHE: Store successfully loaded folder content
private folderContentCache: Map<string, { content: string; frontMatter: any }> = new Map();
// 🚫 DEDUPLICATE: Track in-flight requests
private activeFolderRequests: Map<string, Promise<...>> = new Map();
private async loadFolderContentEfficiently(...) {
// Check cache first
if (this.folderContentCache.has(cacheKey)) {
return this.folderContentCache.get(cacheKey)!;
}
// Deduplicate in-flight requests
if (this.activeFolderRequests.has(cacheKey)) {
return this.activeFolderRequests.get(cacheKey)!;
}
// Load and cache
const result = await this.doLoadFolderContent(...);
this.folderContentCache.set(cacheKey, result); // Cache successful load
return result;
}
🏷️ Tags
#angular #signals #effects #performance #page-freeze #loading-overlay #docs-angular #critical-bug
📚 Related
- Angular Signals Documentation
- Effect Best Practices
@libs-ux/ux-loadinglibraryAppLoadingServicemessage cycling feature