Run Three js on Node JS?

Hello there,

I’m creating a multiplayer game, more precisely a FPS.

I use ViteJS - threeJS for the frontend, and node js - express - for the backend.

My problem here is i need to run three js in the backend

Indeed, i can’t only handle players positions in an array inside my backend, i need to check if every shoot hits someone, and to do that, i need to detect collisions with objects or players in my scene.

So i don’t see how to do that without three js.

Do you have any ideas, best practices to do that ?

I tried to install everything but it’s hard to make it works, i have a lot of problems with ES6 Module and stuff like this…

So i need to be oriented to a full solution to do this.

I hope someone encountered the same issue…

Thank you

Welcome to the life-long lesson of “Decouple Your Rendering From Game Logic Before It Becomes a Problem”.

But since you’d likely be more happy with a dirty-but-working solution than a polite yet firm suggestion to "go back and rewrite the thing from scratch, so that the server-side doesn’t ever need to use model or texture loaders¹ - you can save yourself by using jsdom. JSDOM is quite limited, and will not work right away with three.js, but you are free to append necessary polyfills so that loaders stop complaining (this code is taken from a 2022 project, so keep in mind part of it may not be needed anymore):

// NOTE This code was using jsdom@21.1.0, may or may not work with more recent versions
import jsdom from 'jsdom';
import fs from 'fs';
import { TextDecoder, TextEncoder } from 'util';

// NOTE This should fetch index.html of your bundled game
let serverIndex = fs.readFileSync('./build/index.html').toString();

// NOTE Resolve all scripts of your serverIndex file
// (just load the JS files and inline the scripts from correct paths.
//      This is generally easier and faster with JSDOM than setting up alternative resolving paths in express.js)
serverIndex = serverIndex.split('<script src="').map((fragment, index) => {
  if (index === 0) {
    return fragment;

  const [ url ] = fragment.split('"');

  return [

// NOTE Polyfill APIs used by Loaders that are not polyfilled by JSDOM
const polyfills = `
    window.DOMRect = window.DOMRect || function (x, y, width, height) { 
      this.x = this.left = x;
      this.y = = y;
      this.width = width;
      this.height = height;
      this.bottom = y + height;
      this.right = x + width;

    window.URL = {
      createObjectURL: (blob) => {
        return "data:" + blob.type + ";base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==";
      revokeObjectURL: () => {},

    window.document.createElementNS = (() => {
      const originalCreateFn = window.document.createElementNS.bind(window.document);
      return (spec, type) => {
        if (type === 'img') {
          const mockImage = originalCreateFn(spec, type);
          setTimeout(() => {
            mockImage.dispatchEvent(new window.Event('load'));
          }, 1);
          return mockImage;
        } else {
          return originalCreateFn(spec, type);
    window.AudioBuffer = class AudioBuffer {};

    // NOTE Needed only if you use troika-three-text
    window.TroikaText = class TroikaText {};

    window.console.log = () => {}; = () => {};
    window.console.warn = () => {};
    window.console.error = () => {};

// NOTE Combine polyfills and the index file into a single "executable" JSDOM page
serverIndex = `${polyfills}${serverIndex}`;

const initServerWorld = () => {'initServerWorld', 'start');

  const resourceLoader = new jsdom.ResourceLoader({
    userAgent: 'my-game-agent',

  const emulation = new jsdom.JSDOM(serverIndex, {
    url: 'http://localhost:2567',
    runScripts: 'dangerously',
    resources: resourceLoader,
    pretendToBeVisual: true

  class RequestMock {

    constructor(url) {
      this.url = `./build${url}`;

  class ResponseMock {
    status = 200;
    statusText = 'ok';

    constructor(body, options) {
      this._body = body;
    arrayBuffer() {
      const buffer = new emulation.window.ArrayBuffer(this._body.length);
      const view = new emulation.window.Uint8Array(buffer);

      for (let i = 0, total = this._body.length; i < total; i++) {
        view[i] = this._body.readUInt8(i);

      return buffer;

    text() {
      return this._body.toString();

    json() {
      return JSON.parse(this._body.toString());

  const fetchMock = (request) => {
    return new Promise(resolve => {
      resolve(new ResponseMock(fs.readFileSync(request.url)));
  }'initServerWorld', 'mock emulation');

  // NOTE Server-side version of the same polyfills (may not be necessary in the newer JSDOM versions)
  emulation.window.Request = RequestMock;
  emulation.window.Response = ResponseMock;
  emulation.window.fetch = fetchMock;
  emulation.window.URL.createObjectURL = (blob) => {
    return `data:${blob.type};base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAGgwJ/lK3Q6wAAAABJRU5ErkJggg==`;
  emulation.window.URL.revokeObjectURL = (url) => {};
  emulation.window.document.createElementNS = (() => {
    const originalCreateFn = emulation.window.document.createElementNS.bind(emulation.window.document);

    return (spec, type) => {
      if (type === 'img') {
        const mockImage = originalCreateFn(spec, type);

        setTimeout(() => {
          mockImage.dispatchEvent(new emulation.window.Event('load'));
        }, 1);

        return mockImage;
      } else {
        return originalCreateFn(spec, type);
  emulation.window.AudioBuffer = class AudioBuffer {};
  emulation.window.TroikaText = class TroikaText {};
  emulation.window.TextDecoder = TextDecoder;
  emulation.window.TextEncoder = TextEncoder;'initServerWorld', 'done');

  return emulation.window;

// NOTE Create server-side instance of the game, this is an instance of Window, you can append global-scope methods and helpers to it that'd let you communicate with the JSDOM instance
world = initServerWorld();
world.sendToServer = (action, payload) => { /* ... */ };

¹ In terms of how - each entity should have colliders defined separately from the model itself, you can see how Unreal does it, also back in the day if you tried running a World Of Warcraft private server locally, the “baking navmaps & collisions” part of the initialization is the part the decoupled frontend from the backend.

1 Like

I’m definitely saving this post.

Hello, thanks for the reply.

Can you explain more precisely what are the best practices ? What is the correct way to achieve this without choosing the dirty solution ?

In the proper solution, you’d need to make sure your game / app can run in a “headless mode” - ie. you should be able to run it without any canvas / browser / audio etc. just pure javascript.

While you can import most of three.js in your code, what you cannot import is any of the loaders (they use browser APIs not available in node, hence the JSDOM polyfills above.) So for example:

  • Create configuration service / helper / class / component, however you’d call it:
class ConfigurationServiceClass {
  config = {};

  setConfig(config) {
    this.config = config;

  getConfig() {
    return this.config || {};

export const ConfigurationService = new ConfigurationService();
  • Create index.browser.js and index.server.js, in which you’d differentiate the dependencies:
// NOTE index.browser.js (This one use as the entry file when bundling the game for browser)

import axios from 'axios';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { ConfigurationService } from './ConfigurationService.js';
import { Game } from './Game.js';

  isHeadless: false,
  loaders: {
    gltf: GLTFLoader,
    url: axios


// NOTE index.server.js (Use this as an entry point in node.js execution)

import axios from 'axios';
import { ConfigurationService } from './ConfigurationService.js';
import { Game } from './Game.js';

  isHeadless: true,
  loaders: {
    gltf: null,
    url: axios
  • Then in any of your game components:
import { ConfigurationService } from './ConfigurationService.js';

export const ActorComponent {
  data = {
    modelUrl: null,
    colliderUrl: null,

  constructor(data = {}) {
    const { isHeadless } = ConfigurationService.getConfig(); = { };

    if (isHeadless) {

  loadModels() {
    const { loaders } = ConfigurationService.getConfig();

    // NOTE Do the usual client-side loading of models, materials, etc.
    loaders.gltf.load(, /* ... */);

  loadColliders() {
    // NOTE Load only the collider data - serialised geometry + normals representing your object collisions. How these are shaped and saved depends on the physics / collision library you'd use. The less complex the better.

    const { loaders } = ConfigurationService.getConfig();

    loaders.url.get(, /* ... */);

I’m 99% certain three-mesh-bvh can help you with generation and serialization of colliders - but otherwise you can just fallback to bounding boxes and capsules.

The rest of the Game code should run as usual. If there are any mouse / keyboard interactions in the code - you can put them within a Controller class and hide behind the isHeadless flag, similarly to the loaders.

Ok thank you

So basically, i need to find a way to extract only the colliders from my gltf object,

For the structure of my code, i started to build a “bridge” where i created some scripts which can be used either on the server and the client.

I can add them some functions by using _prototype in my client, or in my server, so they are dedicated to a specific environnement.

For now i’m going to dig deeper to understand what’s inside a gltf object, and how i can extract the data needed in my server