npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

worker-f

v0.1.10

Published

Runs a function in a separate thread in a web browser or Node.js

Readme

npm downloads

worker-f

Runs a function in a separate thread in a web browser or Node.js.

Demo

Why

  • The code is universal and could be run in any environment: in a web browser or Node.js. The easy-to-use API hides the complexity of dealing with each particular environment.

  • Doesn't require moving code to a separate file for it to be able to run in a worker. This prevents introducing the unnecessary and redundant concept of being able to access a "filesystem" or having to run a "web server". The code must not concern itself with such things that're completely irrelevant to its purpose. It shouldn't even know that a concept of a "file path" exists. Running a function in a separate thread should be as simple as it is in other programming languages.

  • Why use workers at all? In a web browser, it's about not freezing the app while doing "heavy computation". In Node.js, it's about not freezing the server while doing "heavy computation". And while freezing an app in a web browser is somewhat bearable, the main appeal of Node.js from the point of its creation has been that it doesn't "fork" the process for each incoming HTTP request, outperforming "classic" web servers by using an "event loop" instead, which quickly backstabs if that "event loop" is accidentally blocked by a "heavy computation".

Install

npm install worker-f

Use

Basic usage:

const workerFn = workerFunction((a, b) => a + b)

await workerFn.callOnce(1, 2) === 3

Advanced usage scenarios like "calling multiple times", "streaming", "caching", "transferring data" are described further in this document.

Import

This package provides a separate import path for each different environment, as described below.

Browser

import workerFunction from 'worker-f/browser'

const workerFn = workerFunction((a, b) => a + b)

Node

import workerFunction from 'worker-f/node'

const workerFn = workerFunction((a, b) => a + b)

API

Call Once

Use this when the function will only be called once. Attempting to call the function second time will throw an error.

The function could be synchronous or asynchronous.

import workerFunction from 'worker-f/node'

const workerFn = workerFunction((a, b) => a + b)

await workerFn.callOnce(1, 2) === 3

Call

When the function will be called multiple times, it should be started, then called as many times as needed, then stopped.

import workerFunction from 'worker-f/node'

const workerFn = workerFunction((a, b) => a + b)

workerFn.start()

await workerFn.call(1, 2) === 3
await workerFn.call(4, 5) === 9

workerFn.stop()

If the function rejects or throws an error, it will automatically stop.

If a developer forgets to stop a worker function that is no longer used, it will still stop automatically when the code no longer holds any "reference" to it. It will also stop automatically when the web browser tab is closed, or the Node.js process is killed. But until stopped, it will keep holding its memory.

Stream

If a function is designed to produce multiple outputs over time, it should use "stream" API rather than "call" API.

import workerFunction, { type Send } from 'worker-f/node/stream'

const workerFn = workerFunction(
  // Use `send()` to output anything from the function
  (send: Send<number>) => {
    // The code here will be run on `start()`
    // and anything declared here will exist until `stop()`,
    // so this is the place to define the function's state
    let counter = 0
    // Return an "input handler" function
    return (input: number) => {
      counter++
      send(input*input)
    }
  }
)

// The sum of all responses from the function
let total = 0

// How many responses it expects from the function
let pending = 0

// Listen to the responses from the function
workerFn.onOutput((output) => {
  total += output
  pending--
  // When all responses are received
  if (pending === 0) {
    // Validate the end result
    total === 14
    // Stop the worker function
    workerFn.stop()
  }
})

// Listen to any errors thrown by the function
workerFn.onError((error) => {
  // Handle the error here
  console.error(error)
  // The worker function will automatically stop after this
})

workerFn.start()

pending++
workerFn.send(1) // 1 * 1 === 1

pending++
workerFn.send(2) // 2 * 2 === 4

pending++
workerFn.send(3) // 3 * 3 === 9

External Dependencies

Sometimes, a function is not "self-contained", and it references some variables or other functions from outside of the function body. In that case, those variables or functions must be specified as the function's "dependencies", or else it would throw a ReferenceError: <name> is not defined.

// External variable
const c = 3
// External function
const d = () => 4

// This worker function references `c` and `d` which are outside of its body
const workerFn = workerFunction((a, b) => a + b + c + d())

// Without declaring `c` and `d` as "dependencies",
// it throws: "ReferenceError: c is not defined"
await workerFn.callOnce(1, 2)

// How to fix the error:
workerFn.addDependencies(() => [c, d])
// Now it works
await workerFn.callOnce(1, 2) === 10

Any external dependencies that're functions must either be "self-contained" functions or specify their own external dependencies in the .addDependencies(...) call. And the loop continues until the very last sub-sub-sub-dependency function is finally "self-contained".

// External function that is "self-contained" because it doesn't reference anything outside of its body
const d = () => 4

// External function that references `d` which is outside of its body
const c = () => 3 + d()

// This worker function references `c` which is outside of its body
const workerFn = workerFunction((a, b) => a + b + c())

// Specifying just `c` is not enough because `d` is also an external sub-dependency
workerFn.addDependencies(() => [c])
// throws: "ReferenceError: d is not defined"
await workerFn.callOnce(1, 2)

// How to fix the error:
workerFn.addDependencies(() => [c, d])
// Now it works
await workerFn.callOnce(1, 2) === 10

To reduce the number of external dependencies, one could put everyting into a single large "self-contained" function that doesn't reference anything outside of its body. Or if it does reference anything outside of its body, those references themselves must be "self-contained" functions that don't reference anything outside of their body, etc.

With that in mind, one could see how specifying all the dependencies correctly could become a tedious task in a typical modular application where the code is spread over countless smaller modules, each of them importing other smaller modules, etc. So a better approach would be to just move everything — the function itself and most of its dependencies — into a single big "wrapper" function, as if we're back in 2000s, and then create a worker from it.

// A "wrapper" function that has the same arguments as the original function
export function fn_(a, b) {
  // Any dependencies are put right here, inside the wrapper function body
  const d = () => 4
  const c = () => 3 + d()

  // The original function is also put here
  const fn = (a, b) => a + b + c()

  // Call the original function with the arguments
  return fn(a, b)
}
// Create a worker from the "wrapper" function.
// No need to specify any external dependencies because there're none. Simple.
const workerFn = workerFunction(fn_)

Needless to say that after a worker function has been started, none of its dependencies should change because those changes won't be reflected inside the worker function's thread, i.e. it won't "see" any changes.

Caching

Every time a new worker function is created, it has to stringify the function body in order to generate the worker's code. If the application plans on creating many workers from same function, and that function has no external dependencies or those dependencies are constant, then it would make sense to only generate the function's source code once and then reuse it every time a new worker is created from this function.

To "cache" a function's source code for creating future workers, call .alias() method on the worker function.

const c = () => 1
const sum = (a, b) => a + b + c()

const sumFn = workerFunction(sum)
sumFn.addDependencies(() => [c])

// Calling `.alias()` creates a snapshot of this worker function.
// After assigning an alias, one could instantiate this type of worker function from the snapshot.
// The snapshot will include both the `sum` function body and any of its dependencies.
//
// Creating a new worker function from an alias will be slightly faster than from a function body
// because it won't have to redo the stringification of the function body and any of its dependencies.
// Does it really matter performance-wise? I didn't bother checking.
//
sumFn.alias('sum')

// (optional)
await sumFn.callOnce(2, 3) === 6

// Create a new worker function by the alias.
const sumFn2 = workerFunction<Args, Result>('sum')
await sumFn2.callOnce(4, 5) === 10

// Create a new worker function by the alias.
const sumFn3 = workerFunction<Args, Result>('sum')
await sumFn3.callOnce(6, 7) === 14

Performance

By default, any input passed to a worker function is cloned behind the scenes. And same goes for any output.

Because of how seamless the "cloning" is, developers don't even have to bother knowing that it takes place.

Yet, in some situations, the data being passed between the main thread and the worker thread might become large-enough to justify tinkering with potential optimization.

How large is "large-enough"?

  • For JSON objects, the deeper the object is, the more costly it is to "serialize" and "deserialize" it back. There're some benchmarks from 2019 where it shows how "serializing"/"deserializing" a 10 MB JSON object with 6 levels of nesting is about 50 ms on a desktop or 100 ms on a phone.

  • For ArrayBuffers, the cloning is said to be "incredibly quick" without any further details.

So the short answer is: "I personally don't really know or care". The rule of thumb is to keep the data being sent between the main thread and the worker thread to a minimum.

Sidenote: The "cloning" is "synchronous" so it blocks the main thread until it finishes cloning.

Transfer List

When passing ArrayBuffers, there's an optional feature called "transfer". When it "transfers" a buffer, it doesn't clone it, but instead it simply "transfers" the ownership of the buffer from the "main thread" to the "worker thread", and vice versa, which is a "free" operation. Although note that after a buffer has been "transferred", it's no longer usable in the code that "transferred" it.

To enable "transfer" for certain input/output buffers, call inputTransferList() / outputTransferList() methods on a worker function.

// A worker function with some input and output.
const parser = workerFunction((arrayBuffer, dataType) => {
  // ... Parse the data from the buffer according to the data type ...
  return data
})

// Pass a function that returns a `transferList` for the input of the worker function.
// By default, an empty `transferList` is used for the input of the worker function.
parser.inputTransferList((arrayBuffer, dataType) => [arrayBuffer])

// Pass a function that returns a `transferList` for the output of the worker function.
// By default, an empty `transferList` is used for the output of the worker function.
parser.outputTransferList((data) => [])

// Now, when passing an `arrayBuffer` to the worker function,
// it will be "transferred" from the main thread to the worker thread.
const data = await parser.callOnce(arrayBuffer, 'document')

Errors

When using "call" API, if the function rejects or throws an error, the returned Promise will be rejected and the worker function will automatically stop.

When using "stream" API, the optional .onError() listener will be called and the worker function will automatically stop.

Development

npm install
npm test

It uses vitest to run unit tests, which also comes with a bug on Windows when it doesn't know how to properly handle lowercase drive letter. The error message is Error: Vitest failed to find the current suite. One of the following is possible. The workaround is to cd into same directory but with an uppercase drive letter.