Skip to main content

IdleMonitorService

The IdleMonitorService hooks into Angular’s Zone.js to detect when your application becomes idle. When all HTTP requests complete and there are no pending asynchronous tasks, Scully knows the page is ready to be rendered.

Overview

When Scully renders your Angular application in Puppeteer, it needs to know when to capture the page content. The IdleMonitorService monitors Zone.js macro tasks and dispatches events when Angular goes idle, signaling to Scully that rendering can begin. This ensures that:
  • All HTTP requests have completed
  • All async operations have finished
  • The DOM is fully updated
  • The page is in its final rendered state

How It Works

  1. AngularInitialized Event: Dispatched when the service initializes
  2. Zone Monitoring: The service monitors Zone.js macro tasks, specifically XMLHttpRequest tasks
  3. AngularReady Event: Dispatched when the application becomes idle (all async tasks complete)
  4. AngularTimeout Event: Dispatched if the app doesn’t become idle within 30 seconds

Configuration

The service is automatically enabled when you import ScullyLibModule. Configure it through the forRoot() options:
import { ScullyLibModule } from '@scullyio/ng-lib';

@NgModule({
  imports: [
    ScullyLibModule.forRoot({
      useTransferState: true,
      alwaysMonitor: false,   // Only monitor during Scully rendering
      manualIdle: false       // Use automatic idle detection
    })
  ]
})
export class AppModule { }

Configuration Options

alwaysMonitor
boolean
default:"false"
When true, idle monitoring runs in both development and production. When false, it only runs during Scully rendering.
manualIdle
boolean
default:"false"
When true, disables automatic idle detection and requires manual triggering of the ready event.

Automatic Idle Detection

By default, the service automatically detects when your app is idle:
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-blog-post',
  template: `
    <article *ngIf="post">
      <h1>{{ post.title }}</h1>
      <div>{{ post.content }}</div>
    </article>
  `
})
export class BlogPostComponent implements OnInit {
  post: any;

  constructor(private http: HttpClient) {}

  ngOnInit() {
    // Scully waits for this HTTP request to complete
    this.http.get('/api/post').subscribe(post => {
      this.post = post;
      // IdleMonitorService detects idle state automatically
      // Scully starts rendering after this
    });
  }
}

Manual Idle Control

For content loaded outside of Zone.js (e.g., third-party scripts, Web Workers), you can manually control when Scully should render.

Global Manual Idle

Disable automatic detection globally:
import { ScullyLibModule } from '@scullyio/ng-lib';

@NgModule({
  imports: [
    ScullyLibModule.forRoot({
      useTransferState: true,
      manualIdle: true  // Disable automatic detection
    })
  ]
})
export class AppModule { }
Then manually trigger the ready event:
import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-manual-idle',
  template: '<div>{{ content }}</div>'
})
export class ManualIdleComponent implements OnInit {
  content = 'Loading...';

  constructor(private idleMonitor: IdleMonitorService) {}

  ngOnInit() {
    // Simulate async operation outside Zone.js
    setTimeout(() => {
      this.content = 'Content loaded!';
      
      // Tell Scully the app is ready to render
      this.idleMonitor.fireManualMyAppReadyEvent();
    }, 3000);
  }
}

Per-Route Manual Idle

Enable manual idle for specific routes in scully.config.ts:
import { ScullyConfig } from '@scullyio/scully';

export const config: ScullyConfig = {
  routes: {
    '/blog/:slug': {
      type: 'contentFolder',
      slug: {
        folder: './blog'
      },
      // Enable manual idle check for this route
      manualIdleCheck: true
    },
    '/user/:userId': {
      type: 'json',
      userId: {
        url: 'http://localhost:8200/users',
        property: 'id'
      },
      // Enable manual idle check for this route
      manualIdleCheck: true
    }
  }
};
In your component:
import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-user-profile',
  template: '<div>{{ userData }}</div>'
})
export class UserProfileComponent implements OnInit {
  userData: any;

  constructor(private idleMonitor: IdleMonitorService) {}

  ngOnInit() {
    // Load data from external source
    this.loadExternalData().then(data => {
      this.userData = data;
      // Signal that rendering can begin
      this.idleMonitor.fireManualMyAppReadyEvent();
    });
  }

  private async loadExternalData(): Promise<any> {
    // Custom async operation outside Zone.js
    return new Promise(resolve => {
      setTimeout(() => resolve({ name: 'John Doe' }), 2000);
    });
  }
}

Methods

fireManualMyAppReadyEvent()

Manually triggers the AngularReady event to signal that Scully should start rendering.
fireManualMyAppReadyEvent(): Promise<boolean>
Returns: A promise that resolves to true when the event is dispatched Example:
import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-custom-loader'
})
export class CustomLoaderComponent implements OnInit {
  constructor(private idleMonitor: IdleMonitorService) {}

  async ngOnInit() {
    await this.loadCustomContent();
    await this.idleMonitor.fireManualMyAppReadyEvent();
  }

  private async loadCustomContent() {
    // Your custom loading logic
  }
}

init()

Returns a promise that resolves when the application first becomes idle.
init(): Promise<boolean>
Returns: A promise that resolves with the idle state

setPupeteerTimeoutValue()

Sets the timeout value used when Zone.js is not available.
setPupeteerTimeoutValue(milliseconds: number): void
Parameters:
  • milliseconds - The timeout duration in milliseconds (default: 5000)

Observables

idle$

An observable that emits the current idle state of the application.
idle$: Observable<boolean>
Example:
import { Component, OnInit } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-idle-indicator',
  template: `
    <div class="status" [class.idle]="isIdle$ | async">
      Status: {{ (isIdle$ | async) ? 'Idle' : 'Busy' }}
    </div>
  `
})
export class IdleIndicatorComponent implements OnInit {
  isIdle$ = this.idleMonitor.idle$;

  constructor(private idleMonitor: IdleMonitorService) {}

  ngOnInit() {
    this.isIdle$.subscribe(idle => {
      console.log('App idle state:', idle);
    });
  }
}

Events

The service dispatches the following custom DOM events:

AngularInitialized

Dispatched when Angular and the IdleMonitorService initialize.
window.addEventListener('AngularInitialized', () => {
  console.log('Angular is initialized');
});

AngularReady

Dispatched when the application becomes idle and is ready to be rendered.
window.addEventListener('AngularReady', () => {
  console.log('Angular is ready to render');
});

AngularTimeout

Dispatched if the application doesn’t become idle within 30 seconds.
window.addEventListener('AngularTimeout', () => {
  console.error('Angular took too long to become idle');
});

Fallback Behavior

If Zone.js is not available, the service falls back to a simple timeout mechanism:
// Logs a warning and uses a timeout
console.warn('Scully is using timeouts, add the needed polyfills instead!');
The default timeout is 5 seconds, but you can customize it:
import { Component } from '@angular/core';
import { IdleMonitorService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-root'
})
export class AppComponent {
  constructor(idleMonitor: IdleMonitorService) {
    // Set custom timeout to 10 seconds
    idleMonitor.setPupeteerTimeoutValue(10000);
  }
}

Troubleshooting

If Scully captures your page before content loads, your async operations might be running outside Zone.js. Solutions:
  1. Enable manual idle mode and call fireManualMyAppReadyEvent()
  2. Ensure HTTP requests use Angular’s HttpClient
  3. Run async operations inside NgZone:
constructor(private zone: NgZone, private idle: IdleMonitorService) {}

loadData() {
  someExternalLibrary.fetch().then(data => {
    this.zone.run(() => {
      this.data = data;
    });
  });
}
If you see AngularTimeout events, your app is taking more than 30 seconds to become idle. Solutions:
  1. Check for infinite HTTP polling or intervals
  2. Use manual idle mode for complex pages
  3. Defer non-critical operations until after rendering
If content is missing from rendered pages:
  1. Verify HTTP requests complete before idle state
  2. Check browser console for errors
  3. Enable alwaysMonitor to test in development
  4. Use manualIdleCheck for specific routes

Best Practices

  1. Use HttpClient: Always use Angular’s HttpClient for HTTP requests so they’re tracked by Zone.js
  2. Avoid Long Polling: Don’t use intervals or long polling on pages that Scully renders
  3. Defer Analytics: Load analytics and tracking scripts after the page renders:
    ngAfterViewInit() {
      // Load after rendering
      this.loadAnalytics();
    }
    
  4. Test Rendering: Enable alwaysMonitor during development to test idle detection:
    ScullyLibModule.forRoot({
      alwaysMonitor: true  // Test idle detection in development
    })
    
  5. Per-Route Configuration: Use route-specific manualIdleCheck for pages with special loading requirements

Source Code

View the complete source code:

Scully Library

Learn about the Angular library overview

Transfer State

Cache application state during rendering

Build docs developers (and LLMs) love