DEV Community

Dominik
Dominik

Posted on

Building a 3D Product Configurator in Three.js — Lessons From 9 Client Deployments

Over the last year I shipped 9 production 3D configurators for polish manufacturers — pools, garage doors, saunas, pergolas, greenhouses, packaging, decorative lamps, terrace roofs, and light-boxes. Each one runs live on its own subdomain of my studio at grodev.pl.

Some of the lessons were obvious in hindsight. Some cost me a weekend of debugging. Sharing the non-obvious ones here.

1. Draco compression is not optional for CAD-heavy models

Manufacturers send you STEP or SolidWorks files exported to glTF. Raw output is 40–120 MB per variant. On 4G mobile that's a 20-second load with an empty white canvas.

Draco compression brings that to 2–5 MB with no visible quality loss on product shots:

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'

const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')  // self-hosted, don't use CDN

const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)

loader.load('/models/pool-3.5m.glb', (gltf) => {
  scene.add(gltf.scene)
})
Enter fullscreen mode Exit fullscreen mode

Self-host the decoder — Google's CDN version added ~600 ms to first paint in my measurements. Copy node_modules/three/examples/jsm/libs/draco/ to your public/ folder.

Tooling: gltf-pipeline -i model.glb -o model.draco.glb --draco.compressionLevel 10

2. Instancing beats individual meshes past ~200 objects

A pergola with 40 louvres × 3 tilt positions × user color picker = 120 meshes updating on every frame. Naive approach tanks FPS to 12 on mid-range phones.

InstancedMesh batches identical geometry into one draw call:

const geo = new THREE.BoxGeometry(1, 0.05, 3)
const mat = new THREE.MeshStandardMaterial()
const louvres = new THREE.InstancedMesh(geo, mat, 40)

const dummy = new THREE.Object3D()
for (let i = 0; i < 40; i++) {
  dummy.position.set(0, 0, i * 0.15)
  dummy.rotation.x = userTilt  // update per frame is fine
  dummy.updateMatrix()
  louvres.setMatrixAt(i, dummy.matrix)
}
louvres.instanceMatrix.needsUpdate = true
scene.add(louvres)
Enter fullscreen mode Exit fullscreen mode

Same for basen tiles, brama slats, sauna wall boards. One material change updates all instances.

3. Ship the price calculator to the server, not the client

Every configurator eventually needs a "Show price" button. Tempting to compute client-side — you already have all the state.

Don't. Manufacturers change prices monthly. Every hardcoded PHP-in-JS multiplication is a redeploy. Also: users open DevTools.

Instead:

// Only what changed goes over the wire
const config = {
  model: 'pool-3.5m',
  finish: 'granite-grey',
  extras: ['lighting', 'cover'],
}

const { price, currency, deliveryWeeks } = await fetch('/api/quote', {
  method: 'POST',
  body: JSON.stringify(config)
}).then(r => r.json())
Enter fullscreen mode Exit fullscreen mode

Server (Laravel in my case) hits a pricing_rules table with monthly-updated coefficients. Client only knows what a valid config looks like, never the pricing logic.

Bonus: the same endpoint powers the "email me a PDF quote" flow. Zero duplication.

4. Mobile is your target device, not a "nice to have"

70% of my configurator traffic is mobile. Sauna buyers browse on the couch, pool buyers browse at the site. Any deployment that assumes desktop-first will feel broken.

Concrete mobile survival kit:

  • renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) — clamping to 2 saves ~40% GPU on retina phones
  • Detect WEBGL_lose_context and reload cleanly when the browser tab is backgrounded on iOS
  • Preload one hero material, lazy-load the rest — first paint at 400 ms feels instant even if full swatch library is still coming

5. The integration is 60% of the project

Building the WebGL viewer takes 2–3 weeks. Making it actually feed the manufacturer's WooCommerce / CRM / ERP takes another 4–6 weeks — and the client only sees the shiny part.

Budget accordingly. I now quote 12–55k PLN depending on catalog depth and integration surface, not per screen.


Live examples if you want to see any of these techniques in production:

Full studio at grodev.pl — happy to chat if you're integrating similar for a manufacturer.

What tripped you up on your first Three.js production project? Reply — always curious about other people's disaster stories.

Top comments (0)