diff --git a/apps/docs/docs/cdk/render-strategies/rx-schedule-task.md b/apps/docs/docs/cdk/render-strategies/rx-schedule-task.md new file mode 100644 index 0000000000..8a0ca9ac27 --- /dev/null +++ b/apps/docs/docs/cdk/render-strategies/rx-schedule-task.md @@ -0,0 +1,114 @@ +# rxScheduleTask + +`rxScheduleTask` provides a helper function to schedule function execution. It is a minimal building block for making performance optimizations in your code. + +## Motivation + +Chromium based browsers consider all tasks that take more than 50ms as long tasks. If task runs for more than 50ms, users will start noticing lags. Optimally all user interactions should happen at 30 fps frame-rate with 32ms budget per browser task. In an ideal world, it should be 60 fps and 16ms budget. + +> 💡 To achieve 30 fps or 60 fps in web apps, you can't just focus on JavaScript execution time. Remember to account for the browser's other tasks, like style recalculations, layout, and painting. + +## Scheduling mechanisms in browser + +Most common ways of delaying a task execution are: + +- `setTimeout` +- `requestAnimationFrame` +- `requestIdleCallback` +- `Promise.resolve` +- `queueMicrotask` + +`rxScheduleTask` provides a similar API but comes with huge benefits of notion of frame budget and priority configuration. + +## Concurrent strategies + +> 💡 Under the hood all our concurrent strategies are based on MessageChannel technology. + +To address the problem of long tasks and help browser split the work @rx-angular/cdk provides concurrent strategies. These strategies will help browser to chunk the work into non-blocking tasks whenever it's possible. + +You can read detailed information about concurrent strategies [here](strategies/concurrent-strategies.md). + +## Usage examples + +### Default usage + +```typescript +import { rxScheduleTask } from '@rx-angular/cdk/render-strategies'; +... + +updateStateAndBackup(data: T) { + this.stateService.set(data); + + rxScheduleTask(() => localStorage.setItem('state', JSON.stringify(state))); +} +``` + +### Input params + +- Just as common delaying apis this method `accepts` a work function that should be scheduled. +- It also accepts configuration object as an optional second parameter + - `strategy` which will be used for scheduling (`normal` is default, for full list of available strategies see [concurrent strategies documentation](strategies/concurrent-strategies.md)) + - `delay` which is responsible for delaying the task execution (default is 0ms) + - `ngZone` if you want your function be executed within ngzone (default scheduling runs out of zone) + +### Return type + +Function returns a callback that you can use to cancel already scheduled tasks. + +### Usage with non-default strategy + +```typescript +import { rxScheduleTask } from '@rx-angular/cdk/render-strategies'; +... + +function updateStateAndBackup(data: T) { + this.stateService.set(data); + + rxScheduleTask( + () => localStorage.setItem('state', JSON.stringify(state)), + {strategy: 'idle'} + ); +} +``` + +### Usage with options + +```typescript +import { rxScheduleTask } from '@rx-angular/cdk/render-strategies'; +... + +function updateStateAndBackup(data: T) { + this.stateService.set(data); + + rxScheduleTask( + () => localStorage.setItem('state', JSON.stringify(state)), + { delay: 200, zone: this.ngZone, strategy: 'idle' } + ); +} +``` + +### Cancel scheduled task + +```typescript +import { rxScheduleTask } from '@rx-angular/cdk/render-strategies'; +... + +let saveToLocalStorageCallback; + +function updateStateAndBackup(data: T) { + this.stateService.set(data); + + if (saveToLocalStorageCallback) { + saveToLocalStorageCallback(); + } + + saveToLocalStorageCallback = rxScheduleTask(() => + localStorage.setItem('state', JSON.stringify(state)) + ); +} +``` + +## Links + +- [Detailed information about strategies](https://github.com/rx-angular/rx-angular/tree/master/libs/cdk/render-strategies) +- [MessageChannel documentation](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel) diff --git a/apps/docs/docs/cdk/render-strategies/rx-strategy-provider.md b/apps/docs/docs/cdk/render-strategies/rx-strategy-provider.md index f7a22cfd11..94670b24b2 100644 --- a/apps/docs/docs/cdk/render-strategies/rx-strategy-provider.md +++ b/apps/docs/docs/cdk/render-strategies/rx-strategy-provider.md @@ -8,7 +8,7 @@ Chromium based browsers considers all tasks that taking more than 50ms as long tasks. If task runs more than 50ms, users will start noticing lags. Optimally all user interactions should happen at 30 fps framerate with 32ms budget per browser task. In ideal world it should be 60 fps and 16ms budget. -> 💡 In reality browser has a reserved overhead of 4ms, try to stick to 28ms of work for 30 fps and 12ms for 60 fps. +> 💡 To achieve 30 fps or 60 fps in web apps, you can't just focus on JavaScript execution time. Remember to account for the browser's other tasks, like style recalculations, layout, and painting. ## Scheduling mechanisms in browser diff --git a/libs/cdk/render-strategies/spec/rx-schedule-task.spec.ts b/libs/cdk/render-strategies/spec/rx-schedule-task.spec.ts new file mode 100644 index 0000000000..795872c638 --- /dev/null +++ b/libs/cdk/render-strategies/spec/rx-schedule-task.spec.ts @@ -0,0 +1,67 @@ +import { NgZone } from '@angular/core'; +import * as scheduler from '@rx-angular/cdk/internals/scheduler'; +import { rxScheduleTask } from '../src'; + +describe('rxScheduleTask', () => { + let work: jest.Mock; + let ngZone: NgZone; + let scheduleSpy: jest.SpyInstance; + let cancelSpy: jest.SpyInstance; + + beforeEach(() => { + work = jest.fn(); + // Mocking NgZone + ngZone = { run: (fn: Function) => fn() } as any; + + // Spying on the scheduleCallback and cancelCallback functions + scheduleSpy = jest.spyOn(scheduler, 'scheduleCallback'); + cancelSpy = jest.spyOn(scheduler, 'cancelCallback'); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should use normal strategy as default', () => { + rxScheduleTask(work); + expect(scheduleSpy).toHaveBeenCalledWith( + expect.anything(), + expect.any(Function), + { delay: undefined, ngZone: undefined } + ); + }); + + it('should schedule work with the specified strategy', () => { + rxScheduleTask(work, { strategy: 'low' }); + expect(scheduleSpy).toHaveBeenCalledWith( + expect.anything(), + expect.any(Function), + { delay: undefined, ngZone: undefined } + ); + }); + + it('should schedule work with the specified delay', () => { + const delay = 200; + rxScheduleTask(work, { delay }); + expect(scheduleSpy).toHaveBeenCalledWith( + expect.anything(), + expect.any(Function), + { delay, ngZone: undefined } + ); + }); + + it('should schedule work inside the specified NgZone', () => { + rxScheduleTask(work, { ngZone }); + expect(scheduleSpy).toHaveBeenCalledWith( + expect.anything(), + expect.any(Function), + { delay: undefined, ngZone } + ); + }); + + it('should cancel the scheduled work', () => { + const cancel = rxScheduleTask(work); + cancel(); + expect(cancelSpy).toHaveBeenCalled(); + }); +}); diff --git a/libs/cdk/render-strategies/src/index.ts b/libs/cdk/render-strategies/src/index.ts index 34b64934dc..70d0aef34c 100644 --- a/libs/cdk/render-strategies/src/index.ts +++ b/libs/cdk/render-strategies/src/index.ts @@ -1,19 +1,28 @@ -export { RxStrategyProvider } from './lib/strategy-provider.service'; -export { ScheduleOnStrategyOptions } from './lib/model'; export { RX_CONCURRENT_STRATEGIES, RxConcurrentStrategies, } from './lib/concurrent-strategies'; -export { RX_NATIVE_STRATEGIES, RxNativeStrategies } from './lib/native-strategies'; +export { + RX_RENDER_STRATEGIES_CONFIG, + RxRenderStrategiesConfig, +} from './lib/config'; +export { + RxConcurrentStrategyNames, + RxCustomStrategyCredentials, + RxDefaultStrategyNames, + RxNativeStrategyNames, + RxRenderBehavior, + RxRenderWork, + RxStrategies, + RxStrategyCredentials, + RxStrategyNames, + ScheduleOnStrategyOptions, +} from './lib/model'; +export { + RX_NATIVE_STRATEGIES, + RxNativeStrategies, +} from './lib/native-strategies'; export { onStrategy } from './lib/onStrategy'; +export { rxScheduleTask } from './lib/rx-schedule-task'; export { strategyHandling } from './lib/strategy-handling'; -export { RxStrategies } from './lib/model'; -export { RxStrategyNames } from './lib/model'; -export { RxDefaultStrategyNames } from './lib/model'; -export { RxConcurrentStrategyNames } from './lib/model'; -export { RxNativeStrategyNames } from './lib/model'; -export { RxCustomStrategyCredentials } from './lib/model'; -export { RxStrategyCredentials } from './lib/model'; -export { RxRenderBehavior } from './lib/model'; -export { RxRenderWork } from './lib/model'; -export { RX_RENDER_STRATEGIES_CONFIG, RxRenderStrategiesConfig } from './lib/config'; +export { RxStrategyProvider } from './lib/strategy-provider.service'; diff --git a/libs/cdk/render-strategies/src/lib/rx-schedule-task.ts b/libs/cdk/render-strategies/src/lib/rx-schedule-task.ts new file mode 100644 index 0000000000..68ef8806bd --- /dev/null +++ b/libs/cdk/render-strategies/src/lib/rx-schedule-task.ts @@ -0,0 +1,52 @@ +import { NgZone } from '@angular/core'; +import { + PriorityLevel, + cancelCallback, + scheduleCallback, +} from '@rx-angular/cdk/internals/scheduler'; +import { RxConcurrentStrategyNames } from './model'; + +type StrategiesPriorityRecord = Record< + RxConcurrentStrategyNames, + PriorityLevel +>; + +const strategiesPrio: StrategiesPriorityRecord = { + immediate: PriorityLevel.ImmediatePriority, + userBlocking: PriorityLevel.UserBlockingPriority, + normal: PriorityLevel.NormalPriority, + low: PriorityLevel.LowPriority, + idle: PriorityLevel.IdlePriority, +}; + +const defaultStrategy: keyof StrategiesPriorityRecord = 'normal'; + +/** + * @description + * This function is used to schedule a task with a certain priority. + * It is useful for tasks that can be done asynchronously. + * + * ```ts + * const task = rxScheduleTask(() => localStorage.setItem(state, JSON.stringify(state))); + * ``` + */ +export const rxScheduleTask = ( + work: (...args: any[]) => void, + { + strategy = defaultStrategy, + delay, + ngZone, + }: { + strategy?: keyof StrategiesPriorityRecord; + delay?: number; + ngZone?: NgZone; + } = {} +) => { + const task = scheduleCallback(strategiesPrio[strategy], () => work(), { + delay, + ngZone, + }); + return () => { + cancelCallback(task); + }; +};