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

@knockdata/lightgbmjs

v0.2.1

Published

LightGBM rewrite in C, fully wasm in Node and Browser

Readme

lightgbmjs

LightGBM for JavaScript. Trains and predicts gradient-boosted decision trees using the same histogram-based, leaf-wise algorithm as LightGBM, and reads and writes model files that are interchangeable with LightGBM's own text (.txt) and JSON (dump_model) formats.

What is published is the WebAssembly engine: the C core built as freestanding wasm32, which runs the same everywhere — a browser, Node, a worker, a bundle. One binary, 21 KB, no per-platform install and nothing to compile.

Behind it is the pure-JS trainer, and it is not a lesser thing: whatever the C core does not implement falls through to it rather than failing. So the objectives, categorical features and missing values wasm does not cover are covered anyway, by the engine already in the package. The same fallback covers wasm not loading at all — a strict Content-Security-Policy without wasm-unsafe-eval, or a runtime with no WebAssembly — so init() never throws; it just leaves the pure-JS engine as the one train() uses.

| | What it is | Objectives | Categorical / missing | | --- | --- | --- | --- | | wasm | the C core as freestanding wasm32 | regression, multiclass | falls back to js | | js | pure JavaScript, zero dependencies | regression, binary, multiclass, GOSS | yes |

Both hand back a model in the same shape, so prediction, model.txt and dump_model JSON are the same code either way.

Install

npm install @knockdata/lightgbmjs

The WebAssembly module ships inside the package. There is no build step, no compiler, and no per-platform binary to install.

Usage

X is one row per sample, y is one label per row. Everything else — the objective, the tree size, how many boosting rounds — goes in the first argument, under LightGBM's own parameter names.

init() is awaited once before the first train(): the wasm module grows its own memory to fit the data, and train() is synchronous and so cannot await anything itself.

import lgbm from '@knockdata/lightgbmjs'

await lgbm.init()                      // once, before train()

const booster = lgbm.train(
    { objective: 'regression', num_leaves: 31, learning_rate: 0.1, num_iterations: 100 },
    { data: X, label: y, featureNames: ['a', 'b', 'c'] },
)

const preds = booster.predict(X)
const text = booster.toText()          // LightGBM-compatible .txt model
const json = booster.toJSON()          // LightGBM-compatible dump_model() JSON

const loaded = lgbm.loadModel(text)    // or lgbm.loadModel(json)
console.log(booster.engine)            // which engine trained it: 'wasm' or 'js'

Regression — a number per row

objective: 'regression' fits L2 loss. y is the value to predict, and predict returns one number per row. Here: a house price in thousands, from its size, bedroom count and age.

import lgbm from '@knockdata/lightgbmjs'

const X = [
    [ 750, 1, 35], [ 900, 2, 28], [1100, 2, 15], [1250, 3, 22],
    [1400, 3,  8], [1600, 3, 12], [1750, 4,  5], [1900, 4, 18],
    [2100, 4,  3], [2300, 5, 10], [2500, 5,  2], [2750, 5,  7],
]
const y = [168, 205, 262, 291, 340, 372, 415, 398, 470, 495, 545, 578]

const booster = lgbm.train(
    { objective: 'regression', num_leaves: 4, num_iterations: 30, learning_rate: 0.1, min_data_in_leaf: 1 },
    { data: X, label: y, featureNames: ['sqft', 'bedrooms', 'age'] },
)

booster.predict([[1000, 2, 20], [2400, 5, 4]])
// Float64Array [ 268.5, 536.9 ]

booster.featureImportance()             // { sqft: 71, age: 19 }   — splits that chose each feature
booster.featureImportance('gain')       // { sqft: 484461, age: 9369 }

min_data_in_leaf: 1 is only because this example has twelve rows; leave it at its default of 20 on real data.

Binary classification — one probability per row

objective: 'binary' fits logloss. y is 0 or 1, and predict returns P(y = 1). Here: whether a loan defaults, from income, debt ratio and late payments.

const X = [
    [42, 0.18, 0], [55, 0.22, 0], [38, 0.31, 0], [71, 0.15, 0], [63, 0.27, 0], [49, 0.20, 0],
    [88, 0.12, 0], [52, 0.35, 1], [31, 0.55, 1], [27, 0.61, 2], [35, 0.48, 1], [44, 0.52, 2],
    [29, 0.66, 3], [33, 0.58, 2], [60, 0.24, 0], [25, 0.72, 4], [47, 0.41, 1], [39, 0.29, 0],
]
const y = [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0]

const booster = lgbm.train(
    { objective: 'binary', num_leaves: 4, num_iterations: 30, learning_rate: 0.1, min_data_in_leaf: 1 },
    { data: X, label: y, featureNames: ['income_k', 'debt_ratio', 'late_payments'] },
)

booster.predict([[68, 0.19, 0], [28, 0.63, 3]])
// Float64Array [ 0.019, 0.971 ]        — P(default), sigmoid applied

booster.predictRaw([[68, 0.19, 0], [28, 0.63, 3]])
// Float64Array [ -3.950, 3.511 ]       — the log-odds before the sigmoid

binary runs on the pure-JS trainer, which needs no init().

Multiclass classification — one probability per class

objective: 'multiclass' fits softmax over num_class classes. y is a class index 0 .. K-1, and predict returns an ARRAY per row — one probability per class, summing to 1. Here: iris species from the four flower measurements.

const X = [
    [5.1, 3.5, 1.4, 0.2], [4.9, 3.0, 1.4, 0.2], [4.7, 3.2, 1.3, 0.2], [5.0, 3.6, 1.4, 0.2], [5.4, 3.9, 1.7, 0.4],
    [7.0, 3.2, 4.7, 1.4], [6.4, 3.2, 4.5, 1.5], [6.9, 3.1, 4.9, 1.5], [5.5, 2.3, 4.0, 1.3], [6.5, 2.8, 4.6, 1.5],
    [6.3, 3.3, 6.0, 2.5], [5.8, 2.7, 5.1, 1.9], [7.1, 3.0, 5.9, 2.1], [6.3, 2.9, 5.6, 1.8], [6.5, 3.0, 5.8, 2.2],
]
const y = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2]   // 0 setosa, 1 versicolor, 2 virginica

const booster = lgbm.train(
    { objective: 'multiclass', num_class: 3, num_leaves: 4, num_iterations: 25, learning_rate: 0.2, min_data_in_leaf: 1 },
    { data: X, label: y, featureNames: ['sepal_len', 'sepal_wid', 'petal_len', 'petal_wid'] },
)

const probs = booster.predict([[5.0, 3.4, 1.5, 0.2], [6.1, 2.9, 4.7, 1.4], [6.7, 3.1, 5.6, 2.4]])
// [ [0.9991, 0.0005, 0.0005],
//   [0.0014, 0.9973, 0.0014],
//   [0.0005, 0.0005, 0.9991] ]

const predicted = probs.map((row) => row.indexOf(Math.max(...row)))   // [0, 1, 2]

booster.numTrees    // 75 — num_class trees per iteration, 25 iterations
booster.numClass    // 3

num_class may be left out, in which case it is inferred from the labels as max(y) + 1.

To choose the engine explicitly:

import lgbm from '@knockdata/lightgbmjs'          // the WebAssembly engine — the default
import lgbm from '@knockdata/lightgbmjs/wasm'     // the same thing, named
import lgbm from '@knockdata/lightgbmjs/js'       // the pure-JS engine on its own, no init()

In the browser

In Node, init() reads the module itself. A browser has no filesystem to read it from, so hand it the bytes — the wasm is a package export, so a bundler resolves it like any other asset:

import lgbm from '@knockdata/lightgbmjs'
import wasmUrl from '@knockdata/lightgbmjs/lightgbmc.wasm?url'   // vite, esbuild, webpack

await lgbm.init(await fetch(wasmUrl).then((response) => response.arrayBuffer()))
const booster = lgbm.train(params, { data: X, label: y })

Without a bundler, resolve it against the module URL:

const bytes = await fetch(new URL('@knockdata/lightgbmjs/lightgbmc.wasm', import.meta.url)).then((r) => r.arrayBuffer())
await lgbm.init(bytes)

lgbm.isAvailable() says whether the module is initialised. Training an objective the C core does not implement needs no init() at all — it runs on the pure-JS trainer either way. If init() could not load wasm — CSP, an old browser, a bad fetch — it resolves anyway, warns once on the console, and leaves isAvailable() false; train() then runs on the pure-JS engine without the caller having to check for it.

Without a bundler

The package also ships a minified, dependency-free bundle of the pure-JS engine at dist/lightgbmjs.min.js — for a plain <script> tag, or a CDN, with no build step at all:

<script src="https://unpkg.com/@knockdata/lightgbmjs/dist/lightgbmjs.min.js"></script>
<script>
    const booster = lgbm.train(
        { objective: 'regression', num_leaves: 4, num_iterations: 30, learning_rate: 0.1, min_data_in_leaf: 1 },
        { data: X, label: y },
    )
</script>

This is the JS engine only — no init(), no wasm fetch. dist/lightgbmjs.esm.js and dist/lightgbmjs.cjs are the same bundle as an ES module and a CommonJS module, for a bundler that would rather not resolve src/'s internal imports itself.

Input shapes

data is either an array of row arrays, or one flat Float64Array laid out row-major (data[row * numFeatures + feature]) with featureNames given so the width is known. The flat form avoids a copy on the WASM path, and avoids a pointer chase per row on the JS path.

API

train(params, dataset)dataset is { data, label, weight?, featureNames?, categoricalFeatures? }.

Parameters follow LightGBM's names: objective (regression | binary | multiclass), num_class, num_leaves, num_iterations, learning_rate, min_data_in_leaf, min_sum_hessian_in_leaf, max_bin, lambda_l1, lambda_l2, min_gain_to_split, max_depth, boosting (gbdt | goss), boost_from_average, seed.

loadModel(source) — a LightGBM .txt string, or a dump_model() JSON object.

Both return a booster:

| | | | --- | --- | | predict(rows, options?) | probabilities/values; { raw_score: true } for pre-transform scores. Multiclass returns one array of num_class probabilities per row, everything else one number per row | | predictRaw(rows) | the same as raw_score | | featureImportance(type?) | { featureName: number } for the features that were used. 'split' (the default) counts the splits that chose each feature, 'gain' totals the gain those splits claimed — the same two types as LightGBM's Booster.feature_importance | | toText() / toJSON() | LightGBM-compatible model files | | numTrees, numClass, featureNames, params | | | engine | which engine trained this model: 'wasm' or 'js' |

Scope

  • Objectives: regression (L2), binary (logloss), multiclass (softmax). The C core implements regression and multiclass; binary runs on the pure-JS trainer whichever engine is asked for
  • Model files: LightGBM text and JSON. LightGBM has no binary format for a trained model, so none is invented here
  • Single threaded
  • Not in scope: ranking objectives, DART, linear-tree leaves, Exclusive Feature Bundling

Benchmark

Two engines this package ships — wasm and pure JS — on the same data through the same timed loop, against Microsoft's LightGBM as the reference. All single threaded. Regression (L2), matched hyperparameters, each number the best of 4 seconds of repeated runs.

Training

| Rows | Features | Leaves/iters | WASM (ms) | JS (ms) | LightGBM (ms) | WASM vs LightGBM | JS vs LightGBM | | --- | --- | --- | --- | --- | --- | --- | --- | | 2,000 | 10 | 15/30 | 6.3 | 18.5 | 5.5 | 1.14x slower | 3.35x slower | | 2,000 | 10 | 63/100 | 54.9 | 149.0 | 44.9 | 1.22x slower | 3.32x slower | | 20,000 | 10 | 15/30 | 26.8 | 85.8 | 14.9 | 1.80x slower | 5.74x slower | | 20,000 | 10 | 63/100 | 130.7 | 334.5 | 105.8 | 1.24x slower | 3.16x slower | | 300,000 | 10 | 63/100 | 1,345.2 | 3,252.3 | 709.3 | 1.90x slower | 4.59x slower |

Prediction

| Rows | Features | Leaves/iters | WASM (ms) | JS (ms) | LightGBM (ms) | WASM vs LightGBM | JS vs LightGBM | | --- | --- | --- | --- | --- | --- | --- | --- | | 2,000 | 10 | 15/30 | 0.5 | 1.2 | 1.3 | 2.34x faster | 1.05x faster | | 2,000 | 10 | 63/100 | 9.1 | 8.2 | 16.4 | 1.80x faster | 2.01x faster | | 20,000 | 10 | 15/30 | 11.4 | 15.3 | 15.1 | 1.32x faster | 1.01x slower | | 20,000 | 10 | 63/100 | 81.0 | 67.6 | 136.7 | 1.69x faster | 2.02x faster | | 300,000 | 10 | 63/100 | 1,324.3 | 1,159.8 | 2,248.7 | 1.70x faster | 1.94x faster |

Training is the one side where these engines trail LightGBM, by 1.2-1.9x for wasm at the sizes above — mostly the cost of the wasm32 runtime, since it compiles the same unmodified C source LightGBM's own C++ core plays against. Prediction is the one where both beat LightGBM, by 1.3-2.3x.

How the benchmark is recorded and reproduced is in BENCHMARK.md.

License

MIT