Self-Hosting a Browser-Based 3D Space: Where the Engineering Actually Goes
self-hosted 3D spaceon-premises 3D deploymentThree.js self-hostingremove CDN dependencyintranet 3D worldGLB optimizationLOD3D asset pipeline
Summary: Most proposals treat "on-premises deployment" as a single line item — install a service, point a domain at it, done. Running a browser-side 3D space inside a corporate network, offline, on servers you own is a different job. The work concentrates in four places: removing CDN dependencies, upgrading the render core, automating the asset pipeline, and turning loading and stuttering into resources you can budget. This article takes each of the four in turn, with reproducible acceptance numbers and a table for deciding which projects should self-host — and which should not.
On-premises deployment is not a switch. It is a migration that converts external dependencies into your own responsibility. This article is about where the time actually goes during that migration.
1. First, define what "on-premises" actually covers
Selection discussions usually reduce this to one question — "is the data on my own server?" In practice there are four gates to pass:
| Dimension | What it means to be self-hosted | The point most often underestimated |
|---|---|---|
| Data | Business data, assets and logs all sit on your own database/disk | Backup and archival pipelines must be built too — you cannot lean on a cloud service |
| Network | No requests to public CDNs; runs on a closed intranet | Removing CDN dependencies — the step most commonly missed |
| Runtime | No dependency on third-party SaaS APIs | Version drift: an upstream update can silently change your behaviour |
| Upgrades | You control the release cadence | You carry the regression cost, which is why acceptance has to be scripted |
Rows two and four are where the real work hides. The rest of this article follows the order in which they can be verified.
2. Removing CDN dependencies: the step everyone assumes is "a few import edits"
Browser 3D projects almost always start with an importmap pointing at a CDN, or <script src="unpkg...">. Inside an intranet, that layer stops working immediately. Worse is the double-instance problem: one copy loaded locally and one from the CDN means the same THREE is loaded twice, and runtime instanceof checks fail for reasons that look inexplicable.
The path we ended up taking had six steps:
- Build a UMD bundle with esbuild, reusing the old file name so reference paths stay unchanged and the blast radius stays small.
- Localise or stub the loaders one by one — anything unused gets an empty implementation rather than being dragged into the bundle.
- Add a shim layer (87 symbols in our build) that maps historical APIs onto the new core.
- Vendor the post-processing library locally.
- Repoint
importmapfrom the CDN to local — this is the step that actually removes the double-instance red line. - Add a compatibility layer that genuinely maps the old output/texture encoding APIs onto the new colour-space APIs, rather than deleting them.
One discipline is worth copying: keep an inventory of legacy APIs. We found 79 call sites and covered all of them with one compatibility layer instead of rewriting business code. In a self-hosted project, every line you touch in business code is regression risk.
Fallback handling is yours as well. On the public web you can assume WebGL2 support; on intranet terminals you cannot (old machines, remote desktop sessions, virtual GPUs). We probe capabilities before loading three, and if the check fails we show a full-screen message in plain language and call window.stop(). A clear verdict beats a white screen with hundreds of console errors.
3. Upgrading the render core: why the version bump deserved its own round of work
Going from r128 to r185 is a wide jump, with the r152 colour-management overhaul sitting in the middle. The old baseline was, in fact, wrong on its own terms: sRGB output combined with textures treated as linear means double gamma, and the image comes out bright and washed pink. Many teams conclude "the artist supplied textures in the wrong colour", when the pipeline is at fault.
The return on the upgrade is not in the version number, it is in the capability surface:
- Colour correctness — with the pipeline right, colours are right. This is a visible quality change.
- The WebGL2 capability surface — our Gaussian-splatting shader, BVH collision and instanced batching all draw on WebGL2.
- Offline operation — once the CDN is gone, both intranet and disconnected environments work.
How do you prove the upgrade did not break the picture? That question has to be answered in a self-hosted project. Our approach is baseline screenshots taken under one fixed protocol plus a homegrown PSNR / differing-pixel tool. The admin page matched at 43.4 dB, and every difference in the main world could be attributed to "the colour was corrected" — framing, buildings and characters were pixel-identical. "Differences are attributable" matters more than "differences are small", otherwise you cannot tell which one is a bug.
4. Assets have to be digested on your own servers
Once self-hosted, the bandwidth is yours, the disk is yours, and the models users upload are yours to carry. That means the asset pipeline cannot be manual — uploads must be processed automatically.
4.1 Model slimming: surgery that does not touch geometry
The approach is to rewrite the GLB via its JSON and BIN chunks without touching geometry, compressing by purpose: normal and occlusion maps get lossless re-encoding, base colour goes through palette quantisation. Measured:
| Sample | Before | After | Reduction |
|---|---|---|---|
| Desk model | 25.74 MB | 8.22 MB | −68.1% |
| 4K multi-buffer model | 20.7 MB | 4.38 MB | −92.5% |
Binary rewriting of this kind is exactly what blows up in production, so we put five gates in front of it: mesh-name match, compression-format check, multi-buffer layout check, geometry consistency check, and a read-back verification. If any gate fails, the file is skipped and never written. In a self-hosted environment there is no "find it in production and roll back".
4.2 LOD: turning "distant objects cost nothing" into a specification you can promise
The common LOD approach switches bands by anchor distance. Ours uses the distance from the player to the model surface. The difference matters: for a model spanning 155 metres, the anchor may sit far away, so anchor-based banding classifies it as distant and swaps in a low-poly version — and the player standing right next to it still sees a blurry model. Surface distance does not make that mistake.
Low-poly meshes are produced by the toolchain, with a homegrown greedy edge collapse as the fallback when the topology floor is not reached. The outcome is a hard number we can state: 118 low-poly groups in the library, 118/118 at or under 100 faces.
Triangle counts with LOD on and off, from three camera positions:
| Position | LOD off | LOD on | Reduction |
|---|---|---|---|
| Classroom hotspot | — | — | −85.8% |
| Spawn point | — | — | −71.2% |
| Instance-dense group | — | — | −86.7% |
One more detail worth copying: variants do not cost extra VRAM. The low-poly variant and the high-poly original share the same texture object rather than duplicating it, and we assert this (33/33 and 36/36 hits on the shared object). Variant textures are also stripped automatically, saving 1.3 GB on disk; the offline deployment package simply omits variants and generates them on first access (118 entries in 36.7 seconds).
5. Turning "stutter" into a resource you can budget
On your own servers, the loading experience is yours to carry. We split it into four separately measurable problems.
① Parsing blocking the main thread. GLB parsing moved into a Worker, serialised with transferable objects, textures pre-downscaled through OffscreenCanvas. Acceptance: 23/23 parse in the Worker and render completely.
② Shader-compilation spikes in the driver layer. This one is the most counter-intuitive: on D3D11, synchronous shader reflection takes 160–340 ms per call and cannot be interrupted. Change the number of lights and the whole scene recompiles, freezing the picture. Our answer is a point-light pool — twelve permanently resident lights that get rented and returned, so a change in light count no longer triggers whole-scene recompilation — combined with a warm-up budget that keeps un-warmed meshes off screen and spreads the reflection cost at 170 ms per frame. The freeze becomes "the model appears slightly later".
The result: freezes caused by light-count changes went from an 8989 ms spike to 0; draw calls in dense areas went 3064 → 837 (−73%).
③ Empty distance. Far geometry used to collapse into placeholder boxes, which looks bad. The fix is a full-image placeholder field: one draw call covering 1000+ objects, fading out over 250 ms. Measured: 1071/1071 covered, draw calls 2150 → 956 (−56%) — the distance looks populated and up close the real models are there.
④ Loading progress has to be real. The denominator of the progress bar is the interest set in the current view, weighted 0.7 for downloaded bytes and 0.3 for confirmed on-screen, with generational isolation for late-arriving load tasks so an old request cannot drag the bar backwards. This matters more when self-hosted: with no CDN in front, users wait longer, and progress feedback is the "I am not dead" signal.
6. After self-hosting, operations is your KPI
There is no clever engineering here, but it decides whether the thing survives.
Acceptance must be scripted and re-runnable. Our repository ships 54 accept_*.js scripts that report a real exit code, a JSON report and VERDICT ACCEPTED. LOD 23/23, AI access 75/75, Gaussian splatting 26/26, skeletal regression 67/67 — every change can be re-verified. A self-hosted project has no canary release as a safety net, so acceptance scripts are it.
Configuration should be hot-adjustable. Changing parameters from the admin panel (LOD band distances, push tiers, speed ceilings, concurrency limits) should take effect within 60 seconds, so a customer never waits for a release to change one threshold.
Constraints buy maintainability. We hold ourselves to hard rules: files under 500 lines (no appending once past 1000), a blacklist of large files that may not be appended to, new features as separate modules, temporary scripts deleted after use. It sounds fussy, but it is the institutional safeguard that keeps a long-lived project from rotting.
7. The current shortcomings, stated plainly
The worst thing a self-hosted proposal can do is read better than its code. Two honest entries:
- Character frame-rate decoupling is not finished.
player.jsstill advances by a fixed increment per frame, so on low-frame-rate devices movement and turning slow down — not just the picture, the feel. This is a real shortcoming in the current code, not a nice-to-have. - The character template library carries 79.3 MB of orphan data. The library totals 205.5 MB, of which 79.3 MB is legacy (animations removed and the JSON edited but the BIN never rebuilt, concentrated in four files, the worst of which is 93.4% garbage). The cleanup plan is settled and would take the cross-world first screen from tens of seconds to 1–2 seconds, but it has not been implemented yet.
Writing these two lines into external material builds more trust than ten advantage bullets, because it shows the thing is actually being maintained.
8. Which projects should self-host, and which should not
| Scenario | Verdict |
|---|---|
| Content is confidential, the network is closed, nothing may leave | Must self-host — CDN-based options simply stop working here |
| You already have a Three.js team and need deep customisation | Use a self-hosted foundation, do not buy a template — the template's limits become the team's ceiling |
| Data requirements are explicit (data must not leave the network or the country) | Self-host, and build the log-archival pipeline yourself as well |
| You only want to validate an idea and need to ship in two or three months | Do not self-host. A SaaS platform is faster; validate the demand first |
| Nobody on the team touches code and there is no budget to hire deployment | Do not self-host. This is a foundation, not a finished product, and the start-up cost will exceed expectations |
| The product does not actually need spatial presentation | Do not build 3D. This is the easiest money to waste |
9. Frequently asked questions
How long does it take to self-host a browser 3D space, and where does it hurt most?
Not the deployment itself — containerisation is not hard now. The hard parts are removing CDN dependencies and automating the asset pipeline. The first decides whether it runs at all inside the network; the second decides whether user-uploaded content can drag your servers down.
Without a CDN, is loading much slower?
Yes, which is why you compensate with engineering: model slimming (−68% to −92%), three LOD versions, Worker parsing, a placeholder field and a warm-up budget. Note that brotli or gzip transfer compression is standard, and the bytes actually sent are typically about a quarter of the source files.
Intranet machines have weak GPUs and cannot do WebGL2. Then what?
Probe capabilities before loading and show a clear message instead of a white screen. If the terminals really are old, treat that as a requirement to be assessed up front rather than a discovery after go-live.
What about upgrades once self-hosted?
Re-runnable acceptance scripts. We have 54 of them, each with an explicit verdict, so one run before an upgrade tells you whether anything regressed.
With data on my own servers, is there anything else to do?
Build the backup and archival pipelines too (chat logs, audit logs, uploaded assets), and be explicit about what happens when archiving fails. Our rule is "never delete locally until the upload has succeeded" — better to consume disk than to lose data.
Can it be extended further?
Self-hosting presumes the underlying code is in your hands. The thing to watch is leaving clean extension seams: new features as separate modules, nothing appended to large files — otherwise extending it turns into archaeology.
Plan Comparison: Self-Hosted Deployment vs. Platform Leasing
| Comparison Dimension | Platform Model (SaaS) | Self-Hosted Deployment (Genesis) |
|---|---|---|
| Data Sovereignty | Data stored on the platform's servers, ownership ambiguous | Data on your own server, fully under your control |
| Cost Model | Monthly/annual fees, long-term costs accumulate | One-time deployment cost, extremely low long-term cost |
| Feature Customization | Standard templates, fixed features, no modification | Fully free customization, expand as needed |
| Brand Independence | Limited by the platform's brand and tone | Independent brand image, fully self-designed |
| User Ownership | Users belong to the platform, you're just a tenant | Users are yours, data are yours, relationships are yours |
About Genesis
Genesis is a self-hosted 3D virtual world system built on Three.js + WebGL, helping individuals and businesses build their own 3D spaces. Accessible directly from a browser, compatible with both PC and mobile, it supports multiplayer online, federated teleportation, a shop system, and Agent integration—where an AI can enter your world as an embodied character. Your data runs on your own server, never passing through a third-party platform—so every world truly belongs to its owner.
If you are evaluating the self-hosted route, you can look at our implementation: Genesis is a browser-side Three.js 3D virtual world foundation that runs on your own servers. Every number above comes from acceptance scripts in the repository, and you are welcome to re-run them.
These notes come from our own development work. The numbers are produced by in-repo acceptance scripts and can be re-run. Items labelled as current shortcomings are genuinely unfinished and are not presented as delivered capability.
Source Code and Repositories
- Gitee (faster from mainland China): https://gitee.com/miduoxinxijeji/miduo.git
- GitCode (mirror for mainland China): https://gitcode.com/qq_35054471/virtual-world
- GitHub: https://github.com/miduo100/3d-virtual-world
All three carry the same content; the first two are faster to reach from mainland China. The repositories contain the deployment guide and a demo entry point.