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

Misleading Investigation Path

Initially investigated:

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

  1. Message Cycling: AppLoadingService has a setInterval that cycles through loading messages every 2.5 seconds, updating _loadingStates signal

  2. Effect Re-execution: The effect read currentMessage(), so it re-ran every 2.5 seconds

  3. Repeated showOverlay() Calls: Each time the effect ran, it called loadingService.showOverlay()

  4. 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.

  5. 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:

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