This document describes the scheduling system that coordinates component updates in the rendering engine, focusing on the Scheduler and GlobalScheduler classes.
Note: This document is part of a series on the rendering architecture. See also Component Lifecycle, Rendering Mechanism, and the integration document Component Rendering and Lifecycle Integration.
The scheduling system consists of two main classes:
- GlobalScheduler - Manages the animation frame loop and coordinates multiple schedulers
- Scheduler - Manages updates for a specific component tree
classDiagram
GlobalScheduler <-- Scheduler
Scheduler -- Tree
Tree -- ITree
CoreComponent ..|> ITree
Component --|> CoreComponent
class GlobalScheduler {
-schedulers: IScheduler[][]
-_cAFID: number
-visibilityChangeHandler: Function | null
+addScheduler(scheduler, index)
+removeScheduler(scheduler, index)
+start()
+stop()
+destroy()
+tick()
+performUpdate()
-setupVisibilityListener()
-handleVisibilityChange()
-cleanupVisibilityListener()
}
class Scheduler {
-scheduled: boolean
-root: Tree
+setRoot(root)
+start()
+stop()
+update()
+iterator(node)
+scheduleUpdate()
+performUpdate()
}
class Tree {
+data: ITree
+traverseDown(iterator)
}
class ITree {
<<interface>>
+iterate(): boolean
}
class CoreComponent {
+iterate()
}
class Component {
+iterate()
}
The scheduler system integrates with the browser's requestAnimationFrame API (or setTimeout in non-browser environments):
sequenceDiagram
participant Browser
participant GlobalScheduler
participant Scheduler
participant Component
Browser->>GlobalScheduler: requestAnimationFrame
GlobalScheduler->>GlobalScheduler: tick()
GlobalScheduler->>GlobalScheduler: performUpdate()
GlobalScheduler->>Scheduler: performUpdate()
Scheduler->>Scheduler: update()
Scheduler->>Tree: traverseDown(iterator)
Tree->>Component: iterate()
Component->>Component: process lifecycle
Browser->>GlobalScheduler: requestAnimationFrame (next frame)
⚠️ Important: Browsers throttle or completely pauserequestAnimationFrameexecution when a tab is in the background. This is a critical consideration for the scheduler system.
When a browser tab is not visible (e.g., opened in background, user switched to another tab), browsers implement the following optimizations:
| Browser | Behavior | Impact on Scheduler |
|---|---|---|
| Chrome | Throttles rAF to 1 FPS | Severe slowdown, ~60x slower |
| Firefox | Pauses rAF completely | Complete halt until tab visible |
| Safari | Pauses rAF completely | Complete halt until tab visible |
| Edge | Throttles rAF to 1 FPS | Severe slowdown, ~60x slower |
Browsers pause or throttle requestAnimationFrame in background tabs for several reasons:
- Battery Life - Reduces CPU usage on mobile devices and laptops
- Performance - Frees up resources for the active tab
- Fairness - Prevents background tabs from consuming too many resources
- User Experience - Prioritizes the visible tab
To handle this behavior, GlobalScheduler integrates with the Page Visibility API:
// In GlobalScheduler constructor
private setupVisibilityListener(): void {
if (typeof document === "undefined") {
return; // Not in browser environment
}
this.visibilityChangeHandler = this.handleVisibilityChange;
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
}
private handleVisibilityChange(): void {
// Only update if page becomes visible and scheduler is running
if (!document.hidden && this._cAFID) {
// Perform immediate update when tab becomes visible
this.performUpdate();
}
}sequenceDiagram
participant User
participant Browser
participant Document
participant GlobalScheduler
participant Components
User->>Browser: Switch to another tab
Browser->>Document: Set document.hidden = true
Browser->>GlobalScheduler: Pause requestAnimationFrame
Note over GlobalScheduler: rAF callbacks stop executing
User->>Browser: Switch back to graph tab
Browser->>Document: Set document.hidden = false
Document->>GlobalScheduler: Fire 'visibilitychange' event
GlobalScheduler->>GlobalScheduler: handleVisibilityChange()
GlobalScheduler->>GlobalScheduler: performUpdate() (immediate)
GlobalScheduler->>Components: Update all components
Browser->>GlobalScheduler: Resume requestAnimationFrame
The scheduling system coordinates when component updates happen:
- Component calls
performRender()to schedule an update - This sets a
scheduledflag in the Scheduler - On the next animation frame, GlobalScheduler triggers updates for all scheduled components
- The Scheduler traverses the component tree in a depth-first order
- Each component's
iterate()method is called during traversal - Components can control whether their children are processed by returning true/false from
iterate()
The GlobalScheduler supports multiple priority levels (0-4):
- Lower priority levels (0, 1) are processed before higher levels (3, 4)
- This allows critical updates to be processed first
- Default priority is 2
- Multiple schedulers can exist at different priority levels
export enum ESchedulerPriority {
HIGHEST = 0,
HIGH = 1,
MEDIUM = 2,
LOW = 3,
LOWEST = 4,
}The scheduler system maintains a task queue that is processed each animation frame. Understanding how tasks are queued and executed is crucial for performance optimization.
The GlobalScheduler maintains 5 separate queues (one per priority level):
private schedulers: [
IScheduler[], // Priority 0 (HIGHEST)
IScheduler[], // Priority 1 (HIGH)
IScheduler[], // Priority 2 (MEDIUM)
IScheduler[], // Priority 3 (LOW)
IScheduler[] // Priority 4 (LOWEST)
];Within each animation frame, tasks are executed in this order:
flowchart TD
A[Animation Frame Start] --> B[Process HIGHEST Priority Queue]
B --> C[Process HIGH Priority Queue]
C --> D[Process MEDIUM Priority Queue]
D --> E[Process LOW Priority Queue]
E --> F[Process LOWEST Priority Queue]
F --> G[Process Deferred Removals]
G --> H[Wait for Next Frame]
H --> A
Tasks can be added to the queue through several mechanisms:
- Component Schedulers - When
performRender()is called on a component - Manual Schedule - Using
schedule()utility function - Debounced Functions - Using
debounce()with frame-based timing - Throttled Functions - Using
throttle()with frame-based timing
// Example: Adding tasks to different priority queues
// 1. Component rendering (uses component's scheduler priority)
component.setState({ value: newValue });
// Internally calls: scheduler.scheduleUpdate()
// 2. Manual schedule - runs once after N frames
const removeTask = schedule(
() => console.log('Task executed'),
{
priority: ESchedulerPriority.HIGH,
frameInterval: 2, // Execute after 2 frames
once: true,
}
);
// 3. Debounced function - queues task when conditions met
const debouncedUpdate = debounce(
() => updateUI(),
{
priority: ESchedulerPriority.LOW,
frameInterval: 3,
frameTimeout: 100,
}
);
// Each call resets the frame counter
debouncedUpdate(); // Queues task
// 4. Throttled function - executes immediately, then queues next execution
const throttledScroll = throttle(
() => handleScroll(),
{
priority: ESchedulerPriority.MEDIUM,
frameInterval: 1,
}
);
throttledScroll(); // Executes immediately
throttledScroll(); // Ignored, waiting for frame intervalEach task in the queue has a performUpdate() method that is called with the elapsed time since frame start:
public performUpdate() {
const startTime = getNow();
// Process each priority level sequentially
for (let i = 0; i < this.schedulers.length; i += 1) {
const schedulers = this.schedulers[i];
// Execute all tasks at this priority level
for (let j = 0; j < schedulers.length; j += 1) {
const elapsedTime = getNow() - startTime;
schedulers[j].performUpdate(elapsedTime);
}
}
// Clean up removed schedulers
this.processRemovals();
}The scheduler uses frame counters to implement frame-based delays:
// Inside debounce implementation
const debouncedScheduler = {
performUpdate: () => {
frameCounter++; // Increment on each frame
const elapsedTime = getNow() - startTime;
// Execute when BOTH conditions met
if (frameCounter >= frameInterval && elapsedTime >= frameTimeout) {
fn(...latestArgs); // Execute the function
frameCounter = 0; // Reset counter
removeScheduler(); // Remove from queue
}
},
};| Aspect | Behavior | Impact |
|---|---|---|
| Tasks per frame | All queued tasks execute each frame | O(n) where n = total tasks |
| Priority processing | Sequential, highest to lowest | Higher priority tasks execute first |
| Frame budget | No time limit per frame | Can cause frame drops if too many tasks |
| Task removal | Deferred until after all tasks execute | Prevents array mutation during iteration |
-
Use Appropriate Priorities
// ✅ Critical rendering updates schedule(updateCanvas, { priority: ESchedulerPriority.HIGHEST }); // ✅ UI feedback schedule(updateSidebar, { priority: ESchedulerPriority.MEDIUM }); // ✅ Analytics, logging schedule(trackEvent, { priority: ESchedulerPriority.LOWEST });
-
Avoid Queue Saturation
// ❌ Bad: Creates new task every time function onMouseMove(e) { schedule(() => updatePosition(e.x, e.y), { frameInterval: 1 }); } // ✅ Good: Reuses same debounced function const updatePosition = debounce( (x, y) => console.log(x, y), { frameInterval: 1 } ); function onMouseMove(e) { updatePosition(e.x, e.y); }
-
Clean Up Tasks
// Always clean up when component unmounts useEffect(() => { const remove = schedule(task, { priority: ESchedulerPriority.LOW }); return () => remove(); // Cleanup }, []); // Or use cancel method const debounced = debounce(fn, { frameInterval: 5 }); return () => debounced.cancel();
-
Monitor Frame Budget
// In development, monitor task execution time const debouncedScheduler = { performUpdate: (elapsedTime: number) => { if (elapsedTime > 16) { console.warn('Frame budget exceeded:', elapsedTime); } // ... task logic }, };
sequenceDiagram
participant App
participant Queue
participant Frame
participant Tasks
App->>Queue: debounce() - adds to MEDIUM queue
App->>Queue: schedule() - adds to HIGH queue
App->>Queue: component.setState() - adds to MEDIUM queue
Frame->>Queue: requestAnimationFrame trigger
Queue->>Tasks: Execute HIGH priority (1 task)
Queue->>Tasks: Execute MEDIUM priority (2 tasks)
Tasks-->>App: Tasks completed
Frame->>Queue: Next frame
Note over Queue: Queues may have new tasks
Queue->>Tasks: Process all priorities again
The traversal process follows these rules:
- Components are visited in depth-first order
- Children are ordered by z-index for proper rendering layering
- Component's
iterate()return value controls whether children are processed - This allows for efficient partial updates of the tree
For detailed flow diagrams showing these processes, see Component Rendering and Lifecycle Integration.
flowchart TD
A[New Scheduler] --> B[Constructor]
B --> C[Register with GlobalScheduler]
C --> D[Wait for updates]
D --> E{Is scheduled?}
E -->|Yes| F[Update tree]
E -->|No| G[Wait for next frame]
G --> D
F --> H[Reset scheduled flag]
H --> G
I[Component calls scheduleUpdate] --> J[Set scheduled flag]
J --> E
// Create a scheduler
const scheduler = new Scheduler();
// Set the root component
const rootComponent = CoreComponent.mount(MyRootComponent, props);
scheduler.setRoot(rootComponent.__comp.treeNode);
// Start the scheduler
scheduler.start();
// Later, stop the scheduler when done
scheduler.stop();class MyComponent extends Component {
public updateValue(newValue) {
this.setState({ value: newValue });
// This calls performRender() which schedules an update
}
protected render() {
// This will be called during the next animation frame
console.log('Rendering with value:', this.state.value);
}
}// Create two schedulers with different priority levels
const highPriorityScheduler = new Scheduler();
const lowPriorityScheduler = new Scheduler();
// Add the schedulers to the global scheduler with different priority levels
globalScheduler.addScheduler(highPriorityScheduler, 0); // High priority
globalScheduler.addScheduler(lowPriorityScheduler, 4); // Low priority
// Start the global scheduler
globalScheduler.start();
// Assign different components to the schedulers
highPriorityScheduler.setRoot(highPriorityRootComponent.__comp.treeNode);
lowPriorityScheduler.setRoot(lowPriorityRootComponent.__comp.treeNode);The scheduling system is designed for optimal performance:
- Updates are batched per animation frame
- Multiple state/props changes trigger only one render per frame
- Z-index ordering allows for efficient rendering of visual components
- The system only traverses branches that need updating (when components return
falsefromiterate()) - Multiple schedulers can be used for different update frequencies
The system adapts to different environments:
// For browser environments
const rAF = window.requestAnimationFrame;
const cAF = window.cancelAnimationFrame;
const getNow = window.performance.now.bind(window.performance);
// For non-browser environments
const rAF = (fn) => global.setTimeout(fn, 16);
const cAF = global.clearTimeout;
const getNow = global.Date.now.bind(global.Date);This allows the scheduler to work in both browser and non-browser JavaScript environments.
The scheduler system interacts with component lifecycle as follows:
sequenceDiagram
participant C as Component
participant S as Scheduler
participant G as GlobalScheduler
C->>C: setState/setProps
C->>S: performRender()
S->>S: Set scheduled flag
G->>S: Animation frame triggers performUpdate()
S->>C: Tree traversal causes iterate()
C->>C: Process lifecycle (checkData, render, etc.)
C->>C: Update complete
S->>S: Reset scheduled flag
The system provides a global scheduler instance for convenience:
export const globalScheduler = new GlobalScheduler();
export const scheduler = globalScheduler;This allows components to share a single scheduler instance and animation frame loop.
In rare cases where you need to completely destroy the scheduler (e.g., testing, cleanup):
// Stop scheduler and remove all event listeners
globalScheduler.destroy();Note: In normal application usage, you don't need to call
destroy(). The global scheduler is designed to run for the entire lifetime of the application.
Debugging issues with the scheduler can be challenging, but there are several techniques you can use to identify and resolve problems:
- Logging: Add console logs to the
performUpdate()anditerate()methods to track the order in which components are being updated. This can help you identify performance bottlenecks or unexpected update patterns. - Performance Profiling: Use the browser's performance profiling tools to identify areas where the scheduler is spending the most time. This can help you pinpoint inefficient components or rendering logic.
- Breakpoints: Set breakpoints in the scheduler's code to step through the update process and examine the state of components and the scheduler itself.
- Visualizations: Create visualizations to track the number of updates per frame, the time spent in each phase of the update process, and the number of components being processed. This can help you identify trends and patterns that might not be apparent from logging or profiling alone.
- Component Lifecycle - In-depth details about component lifecycle methods
- Rendering Mechanism - Architectural details of the rendering system
- Component Rendering and Lifecycle Integration - Comprehensive guide showing how these systems work together