Giter Club home page Giter Club logo

idle-task's Introduction

idle-task

idle-task

npm version npm bundle size Test License: MIT semantic-release: angular

Improve your website performance by executing JavaScript during a browser's idle periods.

Table of Contents

Features

idle-task wraps requestIdleCallback .

The features are as follows.

Manage tasks priority

import { setIdleTask } from 'idle-task';
setIdleTask(yourLowPrioryFunction, { priority: 'low' });
setIdleTask(yourHighPrioryFunction, { priority: 'high' });

Get result by using Promise based API

import { getResultFromIdleTask } from 'idle-task';
// get result asynchronously
const result = await getResultFromIdleTask(yourFunction);

Cache

import { setIdleTask, waitForIdleTask } from 'idle-task';
const taskKey = setIdleTask(yourFunction);
const result1 = await waitForIdleTask(taskKey);
// from cache
const result2 = await waitForIdleTask(taskKey);

Optimize executing tasks

import { setIdleTask } from 'idle-task';
setIdleTask(longTask);
// these functions will be executed during next browser's idle time.
setIdleTask(shortTask);
setIdleTask(shortTask);

Analyze tasks execution time

import { setIdleTask, configureIdleTask } from 'idle-task';
configureIdleTask({ debug: true })
// output the execution time to the web console.
setIdleTask(yourFunction1);

Install

npm i idle-task

Quick Start

The simplest way is to use setIdleTask .

import { setIdleTask } from 'idle-task';

const sendAnalyticsData = () =>
        console.log("send analytics data during a browser's idle periods.");
setIdleTask(sendAnalyticsData);

If you want to get the result of a task, please use waitForIdleTask .

import { setIdleTask, waitForIdleTask } from 'idle-task';

const taskKey = setIdleTask(yourFunction);
const result = await waitForIdleTask(taskKey);

API

setIdleTask

const sendAnalyticsData = () => console.log("send analytics data");
const options = {
    priority: 'high',
    revalidateInterval: 5000,
    revalidateWhenExecuted: true,
};
const taskKey = setIdleTask(sendAnalyticsData, options);

idle-task has a FIFO(First-In-First-Out) queue.

setIdleTask enqueues a task which idle-task will dequeue and run when the browser is idle.

setIdleTask returns TaskKey Object which is necessary for cancelIdleTask , getIdleTaskStatus and waitForIdleTask.

I recommend less than 50 ms to execute a task because of RAIL model . If you want to know how long did it take to finish a task, please use debug mode .

setIdleTask can also be set options as below.

priority?: 'low' | 'high'

You can run a task preferentially using priority: 'high' (default is false) option. setIdleTask adds it to the head of the queue.

revalidateInterval?: number

You can reregister your task by using revalidateInterval .

If you set revalidateInterval: 5000 , idle-task will enqueue your task every 5000 ms .

const saveUserArticleDraft = () => {
    // save user editing article data to database.
}

// saveUserArticleDraft will be executed when the browser is idle.
// In addition, idle-task registers saveUserArticleDraft task every 5000 ms.
setIdleTask(saveUserArticleDraft, { revalidateInterval: 5000 });

revalidateWhenExecuted?: boolean

You can reregister your task by using revalidateWhenExecuted which default is false.

idle-task will enqueue your task when it had been executed.

const saveUserArticleDraft = () => {
    // save user editing article data to database.
}

// saveUserArticleDraft will be executed when the browser is idle.
// In addition, idle-task registers saveUserArticleDraft task when it had been executed.
setIdleTask(saveUserArticleDraft, { revalidateWhenExecuted: true });

overwriteTask?: IdleTaskKey

You can overwrite registered task by using overwriteTask.

If the task have already been executed, idle-task remove its result from the cache and enqueue the new task, otherwise idle-task will remove it from the queue and enqueue the new task.

const generateRandomNumber = () => Math.floor( Math.random() * 100 );

const taskKey = setIdleTask(generateRandomNumber);
const randomNumber1 = await waitForIdleTask(taskKey);

setIdleTask(generateRandomNumber, { overwriteTask: taskKey });
const randomNumber2 = await waitForIdleTask(taskKey);

waitForIdleTask

const generateRandomNumber = () => Math.floor( Math.random() * 100 );
const taskKey = setIdleTask(generateRandomNumber);
const randomNumber = await waitForIdleTask(taskKey);

You can get the result of the task by using waitForIdleTask .

waitForIdleTask can also be set options as below.

timeout?: number

waitForIdleTask maybe wait for the task eternally because it will be finished when the browser is idle. timeout option can prevent it.

const generateRandomNumber = () => Math.floor( Math.random() * 100 );
const taskKey = setIdleTask(generateRandomNumber);
try {
    const firstRandomNumber = await waitForIdleTask(taskKey, { timeout: 1000 });
} catch (e) {
    if (e instanceof WaitForIdleTaskTimeoutError) {
        console.error('this is timeout error')
    }
}

In this case, waitForIdleTask will throw WaitForIdleTaskTimeoutError as default if the task can't be finished within 1000 ms.

timeoutStrategy?: 'error' | ’forceRun'

const generateRandomNumber = () => Math.floor( Math.random() * 100 );
const taskKey = setIdleTask(generateRandomNumber);
const firstRandomNumber = await waitForIdleTask(taskKey, { timeout: 1000, timeoutStrategy: 'error' });

You can choose the movement when the idle task is timeout.

waitForIdleTask executes the task even if having not yet run it after the time has come.

If you set error, waitForIdleTask throws an error if the task can't be finished within the time which you set.

getResultFromIdleTask

const generateRandomNumber = () => Math.floor( Math.random() * 100 );
const randomNumber = await getResultFromIdleTask(generateRandomNumber, {
    priority: 'high',
    timeout: 3000,
    timeoutStrategy: 'error'
});

// same
const taskKey = setIdleTask(generateRandomNumber, { priority: 'high' });
const randomNumber = await waitForIdleTask(taskKey, { timeout: 3000, timeoutStrategy: 'error' });

You can get the result by using getResultFromIdleTask if you don't need the task id.

getResultFromIdleTask can also be set options which is SetIdleTaskOptions.priority and WaitForIdleTaskOptions.timeout .

forceRunIdleTask

const generateRandomNumber = () => Math.floor( Math.random() * 100 );
const taskKey = setIdleTask(generateRandomNumber);
const randomNumber = await forceRunIdleTask(taskKey);

You can get the result immediately whether the task was executed during a browser's idle periods or not.

forceRunIdleTask gets result from cache if the task was executed.

cancelIdleTask

const taskKey = setIdleTask(() => console.log("task will be canceled."));
cancelIdleTask(taskKey);

You can stop to run a task by using cancelIdleTask if it is not executed.

cancelAllIdleTasks

setIdleTask(() => console.log("task 1 will be canceled."));
setIdleTask(() => console.log("task 2 will be canceled."));
setIdleTask(() => console.log("task 3 will be canceled."));
cancelAllIdleTasks();

You can stop to run all tasks by using cancelAllIdleTasks if they are not executed.

getIdleTaskStatus

const taskKey = setIdleTask(() => console.log("task"));
const idleTaskStatus = getIdleTaskStatus(taskKey);
// execute immediately if the task has not been executed yet.
if (idleTaskStatus === 'ready') {
  forceRunIdleTask(taskKey)
}

You can know the task status by using getIdleTaskStatus .

getIdleTaskStatus returns string as following.

  • ready
    • The task has not been executed.
  • executed
    • The task has been executed.
    • This doesn't mean that the task has been completed because JavaScript don't have API which help us to know the promise result like fullfilled .
  • unknown
    • idle-task doesn't know the task status because its result doesn't exist anywhere.
    • This case means that the task was canceled by API like cancelIdleTask .

configureIdleTask

configureIdleTask({
  interval: 1000, // ms
  debug: process.env.NODE_ENV === 'development',
  timeout: 3000,
});

configureIdleTask configures idle-task . You can set properties as below.

interval?: number

idle-task checks tasks which was registered by setIdleTask during a browser's idle periods, so they will not always be executed .

Please set interval if you want to guarantee to run tasks as much as possible.

Even if the browser is not idle, idle-task checks tasks every 1000 ms when interval is 1000 and will execute tasks without negative impact on performance.

debug?: boolean

If debug is true, you can know how long did it take to finish the task via the web console.

I recommend less than 50 ms to execute a task because of RAIL model .

The default is false .

timeout?: number

This option configures timeout of waitForIdleTask and getResultFromIdleTask as default setting.

configureIdleTask({ timeout: 3000 });

const taskKey = setIdleTask(yourFunction);
// timeout is 3000
const result = await waitForIdleTask(taskKey);

// timeout is 5000 if you set timeout as option
const result = await waitForIdleTask(taskKey, { timeout: 5000 });

timeoutStrategy?: 'error' | ’forceRun'

This option configures timeoutStrategy of waitForIdleTask and getResultFromIdleTask as default setting.

configureIdleTask({ timeout: 3000, timeoutStrategy: 'forceRun' });

const taskKey = setIdleTask(yourFunction);
// run task in 3000 ms regardless of whether the task has already been executed or not.
const result = await waitForIdleTask(taskKey);

// timeoutStrategy is 'error' if you set timeoutStrategy as option
try {
  const result = await waitForIdleTask(taskKey, { timeoutStrategy: 'error' });  
} catch {
  console.error('timeout!')
}

Recipes

Vanilla JS

dynamic import

import { setIdleTask } from 'idle-task';

// this module is loaded during a browser's idle periods because it is not important for UI.
const taskKey = setIdleTask(() => import('./sendAnalyticsData'))

const button = document.getElementById('button');
button.addEventListener('click', async () => {
    // You should use waitForIdleTask if the module is not important.
    // On the other hand, I recommend to use forceRunIdleTask if the module is important. 
    const { default: sendAnalyticsData } = await waitForIdleTask(taskKey);
    // Send analytics data to server when the browser is idle.
    setIdleTask(sendAnalyticsData);
})

fetch external resources

import { getResultFromIdleTask } from 'idle-task';

const checkAccessTokenWhenIdle = (accessToken: string): Promise<any> => {
    const fetchCheckAccessToken = async (): Promise<any> => {
        const response = await fetch(`https://yourdomain/api/check?accessToken=${accessToken}`);
        // Promise callback will execute immediately after fetching completely even if the browser is busy.
        // One of the solutions is to run it when next browser's idle time.
        return getResultFromIdleTask(() => response.json());
    };
    return getResultFromIdleTask(fetchCheckAccessToken);
}

const { isSuccess } = await checkAccessTokenWhenIdle('1234');

React

fetch external resources

import {useState, useEffect} from 'react';
import {setIdleTask, cancelIdleTask, waitForIdleTask} from 'idle-task';

const fetchNewsList = async () => {
  const response = await fetch('https://yourdomain/api/news');
  return response.json();
}

// this is not important UI for the website main content like e-commerce sites.
export default function WebsiteNewsList() {
  const [newsList, setNewsList] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // fetch news list when the browser is idle and cache it.
    const taskKey = setIdleTask(fetchNewsList)
    waitForIdleTask(taskKey)
        .then(setNewsList)
        .finally(() => setIsLoading(false));
    return () => {
        // stop to fetch news list and remove the cache when the component re-render.
        cancelIdleTask(taskKey)
    };
  }, [])
  
  if (isLoading) {
      return <div>Loading...</div>
  }
  return newsList.map(news => (
      <div id={news.id}>
        {news.publiedDate}
        {news.title}
        {news.description}
      </div>
  ))
}

React.lazy

import {useState, useEffect, lazy, Suspense} from 'react';
import {setIdleTask, waitForIdleTask, forceRunIdleTask} from 'idle-task';

const taskKey = setIdleTask(() => import('~/components/Modal'))
const taskPromise = waitForIdleTask(taskKey)
const Modal = lazy(() => taskPromise);

export default function WebsiteNewsList() {
  const [isClicked, setIsClicked] = useState(false);
  const onClick = () => setIsClicked(true);

  useEffect(() => {
    if (isClicked) {
      // Import Modal immediately whether importing it was completed during the browser's idle periods or not.
      forceRunIdleTask(taskKey);
    }
  }, [isClicked])

  return (
      <>
        <button type='button' onClick={onClick} />
        <Suspense>
          {isClicked && <Modal />}
        </Suspense>
      </>
  )
}

Contributing

Please see CONTRIBUTING.md .

Thank you for contributing!!

@joeinnes @m5r @yuchi

License

Released under the MIT license.

idle-task's People

Contributors

hiroki0525 avatar joeinnes avatar kodiakhq[bot] avatar m5r avatar renovate[bot] avatar semantic-release-bot avatar yuchi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

idle-task's Issues

BREAKING CHANGE: `timeoutStrategy` is `forceRun` as default

Summary

timeoutStrategy is forceRun as default.
Here is an example.

configureIdleTask({ timeout: 3000 });
const taskKey = setIdleTask(yourFunction);

// Before
// After 3000 ms,  `idle-task` will throw Error if the task has not been run when the browser is idle.
try {
  const result = await waitForIdleTask(taskKey);
} catch (e) {
  // error handling
}

// After
// After 3000 ms,  `idle-task` will execute the task if it has not been run when the browser is idle.
const result = await waitForIdleTask(taskKey);

What is the problem?

I think that everyone sets forceRun option in most cases.
Here is an example of React.lazy .

import { getResultFromIdleTask, configureIdleTask } from 'idle-task';
import { lazy } from 'react';

configureIdleTask({ timeout: 3000 });

// Wrap `React.lazy`
const lazyWhenIdle = (factory: Parameters<typeof lazy>[0]) => {
  const idleTaskPromise = getResultFromIdleTask(factory);
  return lazy(() => idleTaskPromise);
}

// Problem: It maybe will throw Error.
const YourComponent = lazyWhenIdle(() => import('~/components/YourComponent'));

In this case, It will be solved like belows.

configureIdleTask({ timeout: 3000, timeoutStrategy: 'forceRun' });
// or
const idleTaskPromise = getResultFromIdleTask(factory, { timeoutStrategy: 'forceRun' });

But this disgust us.
Ideally, timeoutStrategy is forceRun as default.

Why is this problem happening?

This is because timeoutStrategy is error as default.

// `timeoutStrategy` is `error`
configureIdleTask({ timeout: 3000 });

What is the solution?

timeoutStrategy will be forceRun instead of error as default.

// `timeoutStrategy` is `forceRun`
configureIdleTask({ timeout: 3000 });

BREAKING CHANGE: Manage caches automatically

Summary

You don't have to manage caches manually.
Here is an example.

const yourFunction = () => console.log('yourFunction');

// Before
// set `{ cache: false }` because the result isn't needed
setIdleTask(yourFunction, { cache: false });

// After
// this will remove the cache automatically
setIdleTask(yourFunction);

What is the problem?

We must manage caches manually as belows.

// example1
configureIdleTask({ cache: false });

// example2
setIdleTask(yourFunction, { cache: false })

// example3
const taskId = setIdleTask(yourFunction);
const result1 = await forceRunIdleTask(taskId);
const result2 = await forceRunIdleTask(taskId, {
    cache: false,
});

This disgust us.
Ideally, the cache would be deleted when it's no longer needed.

Why is this problem happening?

This is because idle-task is using Map to cache results.

// Map.set(taskId, result);
const taskId = setIdleTask(yourFunction);
// Map.delete(taskId, result);
const result = await forceRunIdleTask(taskId, {
    cache: false,
});

What is the solution?

idle-task will use WeakMap instead of Map .

// taskKey is `Object` because the key of `WeakMap` must be `Object` .
// WeakMap.set(taskKey, result);
let taskKey = setIdleTask(yourFunction);
const result = await forceRunIdleTask(taskId);
// The cached result will be remove automatically because of garbage collection.
taskKey = null;

How to bind the task with an user event

There are some common cases like

  • A mobile menu to be initiated and used when user click on the button
  • A search JS overlay need to be initiated and when user input the search, it will start interact with users immediately

I'm looking a way to optimize some UI JS for those and found this together with
https://github.com/GoogleChromeLabs/idlize

Both are nice idea and exactly what I'm approaching. However in both cases, I can not see an interface API which allow to do these when CPU is idle while allow the initialization can be triggered immediately if users interact (and it was not initiated).

Do you have any idea? @hiroki0525 @yuchi @joeinnes @m5r

A question about this library

Hi 👋

TLDR: Does it run in inactive tab?


I'm looking for a solution to the following problem:

Before closing a tab, I need to send an API request from the browser to server to the make a graceful teardown of resources allocated on the server side for the specific browser tab.
I used beforeunload event for that, but this event doesn't emit when the user closes the tab when they are on another tab.
The solution that comes to my mind is to implement some kind of periodic heartbeat from browser to server.

My first thought was to implement it using Web Workers running in the background. After a bit more thinking on this option, it looks like it won't solve my problem.

I wonder, can this library help me to solve my task?

I'm not familiar with the requestIdleCallback browser API.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.