├── .editorconfig ├── .gitignore ├── .travis.yml ├── builder.js ├── example └── index.html ├── license ├── package.json ├── readme.md ├── src └── index.js └── test └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.lock 4 | *.log 5 | dist 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | -------------------------------------------------------------------------------- /builder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const mkdir = require('mkdirplz'); 3 | const pretty = require('pretty-bytes'); 4 | const { minify } = require('terser'); 5 | const sizer = require('gzip-size'); 6 | const pkg = require('./package'); 7 | 8 | let data = fs.readFileSync('src/index.js', 'utf8'); 9 | 10 | mkdir('dist').then(() => { 11 | // Copy as is for ESM 12 | fs.writeFileSync(pkg.module, data); 13 | 14 | // Mutate exports for CJS 15 | data = data.replace(/export default /, 'module.exports ='); 16 | fs.writeFileSync(pkg.main, data); 17 | 18 | // Minify & print gzip-size 19 | let { code } = minify(data, { toplevel:true }); 20 | console.log(`> gzip size: ${pretty(sizer.sync(code))}`); 21 | 22 | // Write UMD bundle 23 | let name = pkg['umd:name'] || pkg.name; 24 | let UMD = `!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.${name}=t()}(this,function(){`; 25 | UMD += code.replace(/module.exports=/, 'return ') + '});'; 26 | fs.writeFileSync(pkg.unpkg, UMD); 27 | }); 28 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Workshy 5 | 6 | 7 |
8 | 9 |
10 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Terkel Gjervig (terkel.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workshy", 3 | "version": "1.0.4", 4 | "repository": "terkelg/workshy", 5 | "description": "A small (354B) lazy function scheduler for a butter smooth main thread", 6 | "unpkg": "dist/workshy.min.js", 7 | "module": "dist/workshy.mjs", 8 | "main": "dist/workshy.js", 9 | "license": "MIT", 10 | "author": { 11 | "name": "Terkel Gjervig", 12 | "email": "terkel@terkel.com", 13 | "url": "https://terkel.com" 14 | }, 15 | "engines": { 16 | "node": ">=6" 17 | }, 18 | "scripts": { 19 | "build": "node builder", 20 | "pretest": "npm run build", 21 | "prepublishOnly": "npm run build", 22 | "test": "tape test/*.js | tap-spec" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "keywords": [ 28 | "scheduler", 29 | "lazy", 30 | "idle", 31 | "thread", 32 | "worker", 33 | "workshy", 34 | "throttle" 35 | ], 36 | "devDependencies": { 37 | "gzip-size": "^5.1.1", 38 | "mkdirplz": "1.0.1", 39 | "pretty-bytes": "^5.3.0", 40 | "tap-spec": "^5.0.0", 41 | "tape": "^4.13.2", 42 | "terser": "^4.6.6" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # workshy [![Build Status](https://travis-ci.org/terkelg/workshy.svg?branch=master)](https://travis-ci.org/terkelg/workshy) [![Version](https://badgen.now.sh/npm/v/workshy)](https://npmjs.com/package/workshy) 2 | 3 | > A small (376B) lazy function scheduler for a butter smooth main thread 4 | 5 | Workshy is a `throttle` utility that **rate limit**, **queue**, and **distribute** function executions over time to prevent the main thread from becoming unresponsive. 6 | 7 | Unlike a standard throttle function, and to ensure non-blocking rendering and responsive UIs, `workshy` break up functions into smaller chunks executed over time if necessary. 8 | 9 | This module is available in three formats: 10 | 11 | * **ES Module**: `dist/workshy.mjs` 12 | * **CommonJS**: `dist/workshy.js` 13 | * **UMD**: `dist/workshy.min.js` 14 | 15 | 16 | ## Install 17 | 18 | ``` 19 | $ npm install --save workshy 20 | ``` 21 | 22 | The script can also be directly included from [unpkg.com](https://unpkg.com/workshy): 23 | ```html 24 | 25 | ``` 26 | 27 | 28 | ## Usage 29 | 30 | ```js 31 | import workshy from 'workshy'; 32 | 33 | // dummy function doing heavy work 34 | const greet = () => 'hello world'; 35 | 36 | // queue and call function 37 | workshy(greet)(); 38 | // => 'hello world' 39 | 40 | // tasks are only called once, but 41 | // multiple calls increases priority 42 | const a = workshy(x => console.log(`A: ${x}`)); 43 | const b = workshy(x => console.log(`B: ${x}`)); 44 | b(1); 45 | a(1); 46 | a(2); 47 | // => A: 2 48 | // => B: 1 49 | 50 | // manually define priority 51 | const func = workshy(greet, {priority: 2}); 52 | 53 | // force it to be called immediately 54 | const func = workshy(greet, {force: true}); 55 | 56 | // workshy distribute the work over time to 57 | // make sure the main thread runs butter smooth 58 | for (let i = 0; i < 5000; i++) { 59 | workshy(greet)(); // => this won't block UI 60 | } 61 | 62 | ``` 63 | 64 | 65 | ## API 66 | 67 | ### workshy(task, [options]) 68 | Returns: `function` 69 | 70 | #### task 71 | Type: `function` 72 | 73 | Accepts any function a returns a `function` (a function that wraps your original function). Call returned function to queue task. 74 | 75 | The returned `function` will execute your function with the latest arguments provided to it as soon as possible based on queue length and prioroty. 76 | 77 | > **Important:** Task are only called _once_.
Calling the same task multiple times increases its priority. 78 | 79 | #### options.priority 80 | Type: `Number`
81 | Default: `0` 82 | 83 | Tasks are sorted by priority. Functions with high priority are called first. 84 | 85 | > **Important:** Priority also increase if a task is called multiple times. 86 | 87 | ```js 88 | workshy(() => console.log('Hello World'), {force: false, priority: 2}); 89 | //=> 'Hello World' 90 | ``` 91 | 92 | #### options.force 93 | Type: `Boolean`
94 | Default: `false` 95 | 96 | ```js 97 | workshy(() => console.log('Hello World'), {force: false, priority: 2}); 98 | //=> 'Hello World' 99 | ``` 100 | 101 | 102 | ## Inspiration 103 | 104 | This is inspired by the talk [The Virtue of Laziness: Leveraging Incrementality for Faster Web UI](https://youtu.be/ypPRdtjGooc?t=510) 105 | 106 | 107 | ## License 108 | 109 | MIT © [Terkel Gjervig](https://terkel.com) 110 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const onIdle = window.requestIdleCallback || function (handler) { 2 | const start = Date.now(); 3 | return setTimeout(() => handler({ didTimeout: false, timeRemaining: () => Math.max(0, 50 - (Date.now() - start)) }), 1); 4 | }; 5 | 6 | function workshy() { 7 | const tasks = []; 8 | let running = false; 9 | 10 | const sort = () => tasks.sort((a, b) => b._priority - a._priority); 11 | const start = () => (onIdle(process, { timeout: 50 }), running = true); 12 | 13 | function process(deadline) { 14 | while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) { 15 | const fn = tasks.shift(); 16 | fn._scheduled = false; 17 | fn(...fn._args); 18 | } 19 | tasks.length > 0 ? start() : running = false; 20 | } 21 | 22 | function run(fn) { 23 | if (!fn._scheduled) { 24 | tasks.push(fn); 25 | fn._scheduled = true; 26 | fn._priority = 0; 27 | } else { 28 | fn._priority++; 29 | sort(); 30 | } 31 | 32 | if (tasks.length && !running) start(); 33 | } 34 | 35 | return function(fn, {force = false, priority = 0} = {}) { 36 | return (...args) => { 37 | fn._args = args; 38 | if (force) { 39 | fn._scheduled = false; 40 | fn(...args) 41 | } else if (!fn._scheduled) { 42 | tasks.push(fn); 43 | fn._scheduled = true; 44 | fn._priority = priority; 45 | priority !== 0 && sort(); 46 | } else { 47 | fn._priority++; 48 | sort() 49 | } 50 | 51 | if (tasks.length && !running) start(); 52 | } 53 | } 54 | } 55 | 56 | export default workshy(); 57 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | global.window = { requestIdleCallback: false } 2 | 3 | const test = require('tape'); 4 | const fn = require('../dist/workshy'); 5 | 6 | test('workshy', t => { 7 | t.is(typeof fn, 'function', 'exports a function'); 8 | t.is(typeof fn(), 'function', '~> returns function output'); 9 | t.end(); 10 | }); 11 | --------------------------------------------------------------------------------