threaded-three
v0.2.0
Published
Multithreaded ECS runtime for Three.js scenes with worker pools, background jobs, and benchmark tooling
Maintainers
Readme
threaded-three
Multithreaded ECS runtime for Three.js scenes with worker pools, background jobs, adaptive dispatch, and benchmark tooling.
threaded-three is designed for simulation-heavy Three.js apps that need more than a single main-thread update loop. It uses SharedArrayBuffer, double-buffered component storage, compute workers, and an optional render worker to keep ECS work off the main thread.
Install
npm install threaded-three threeRequirements
- Modern browser with
SharedArrayBuffersupport - Cross-origin isolation enabled in development and production:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp
Without those headers you can still run in single mode, but the multithreaded worker runtime will not be available.
Quick Start
import { World, System, Component } from 'threaded-three';
class MovementSystem extends System {
components = ['position', 'velocity'];
static config = {
pool: 'physics',
allowStealing: true,
dynamicSplit: true,
writeComponents: ['position']
};
execute(chunk, dt) {
const position = chunk.comp0;
const velocity = chunk.comp1;
for (let i = 0; i < chunk.count; i++) {
const base = i * 3;
position[base] += velocity[base] * dt;
position[base + 1] += velocity[base + 1] * dt;
position[base + 2] += velocity[base + 2] * dt;
}
}
}
const world = new World({
totalWorkers: 'auto',
reserveWorkers: 2,
backgroundWorkers: 'off',
componentLayout: {
position: Component.f32x3,
velocity: Component.f32x3
},
maxEntities: 50000
});
world.addSystem(MovementSystem);
const entities = world.createEntity(1000);
for (const entity of entities) {
world.writeInitialComponent('position', entity, [0, 0, 0]);
world.writeInitialComponent('velocity', entity, [1, 0, 0]);
}
world.onUpdate(({ stats }) => {
console.log(stats.frameNumber, stats.lastTickMs);
});
await world.start({ mode: 'multi' });Public API
Component
Use component descriptors when building a world layout.
Component.f32
Component.f32x3
Component.i32
Component.ofSize(n)System
Subclass System and implement execute(chunk, dt).
Useful static config keys:
pool: named pool, usuallyphysics,ai, orglobalallowStealing: lets other pools steal this system's jobsdynamicSplit: uses adaptive chunkingminThreads: lower bound for dispatch parallelismwriteComponents: explicit write set for phase planning
World
Main runtime entry point.
Common constructor options:
totalWorkers: number or'auto'reserveWorkers: CPU headroom to leave unusedpools: pool sizes such as{ physics: { size: 'auto' }, ai: { size: 'auto' }, global: { size: 'auto' } }backgroundWorkers: number,'auto', or'off'componentLayout: component descriptors keyed by namerender: optional render configuration for worker or main-thread renderermaxEntities: maximum entity capacitymetrics: metrics collection config
Useful instance methods:
addSystem(SystemClassOrInstance)createEntity(count = 1)writeComponent(name, entityId, bufferId, values)writeInitialComponent(name, entityId, values)getComponentView(name, bufferId?)readComponent(name, entityId, bufferId?)onUpdate(callback)start({ mode, canvas })stop()getStats(options?)resetPerformanceMetrics(options?)registerBackgroundJob(name, handler)runBackgroundJob(name, payload)createBenchmarkReport(options?)
Rendering Modes
Simulation Only
await world.start({ mode: 'multi' });Worker Render
const canvas = document.querySelector('canvas');
await world.start({ mode: 'multi', canvas });If OffscreenCanvas transfer is not available, the runtime falls back to a main-thread renderer.
Background Jobs
await world.registerBackgroundJob('scanGrid', ({ width, height }) => {
let total = 0;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
total += x + y;
}
}
return total;
});
const result = await world.runBackgroundJob('scanGrid', { width: 64, height: 64 });Benchmarks
threaded-three also exports benchmark helpers used in this repo:
import {
runBenchmark,
runBenchmarkSeries,
compareBenchmarkReports,
runBenchmarkMatrix,
compareBenchmarkMatrices
} from 'threaded-three';These are useful for validating runtime changes, but they are optional for application consumers.
Notes
threeis a peer dependency.- Worker URLs are bundled from the library build, so consumers should import the package from a modern ESM bundler environment.
- For Node-based tests you may need a
requestAnimationFrameshim when exercising render or benchmark paths.
Development
npm install
npm run build
npm test
npm run test:e2e