<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dominik</title>
    <description>The latest articles on DEV Community by Dominik (@gronskideveloper).</description>
    <link>https://dev.to/gronskideveloper</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4063459%2F22e7ac79-5161-44b2-b933-ff19ebcb5783.png</url>
      <title>DEV Community: Dominik</title>
      <link>https://dev.to/gronskideveloper</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gronskideveloper"/>
    <language>en</language>
    <item>
      <title>The color-change memory leak in Three.js configurators</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Sun, 01 Nov 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/the-color-change-memory-leak-in-threejs-configurators-126d</link>
      <guid>https://dev.to/gronskideveloper/the-color-change-memory-leak-in-threejs-configurators-126d</guid>
      <description>&lt;p&gt;Color pickers are the most-used control in a product configurator. They are also where I shipped my first real GPU memory leak, because the fix for "change the color" looks harmless and is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The leak
&lt;/h2&gt;

&lt;p&gt;The intuitive version builds a fresh material every time the user picks a color:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;colorPicker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;mesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MeshStandardMaterial&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works on screen. But the old material is still on the GPU. Three.js does not garbage-collect GPU resources for you. A customer who tries twenty colors has left nineteen materials, and their textures and shader programs, allocated on the graphics card. On a configurator people play with for a few minutes, memory climbs until the tab gets slow or the context is lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: reuse the material, set the color
&lt;/h2&gt;

&lt;p&gt;If only the color changes, never build a new material:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;colorPicker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;mesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One material for the lifetime of the mesh. The color is a cheap uniform update, not a new GPU allocation. This is also why &lt;a href="https://dev.to/gronskideveloper/instancedmesh-in-threejs-one-draw-call-for-the-parts-you-repeat-1g5g"&gt;per-instance color on an InstancedMesh&lt;/a&gt; uses &lt;code&gt;setColorAt&lt;/code&gt;, not a new material per instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you really do need a new material
&lt;/h2&gt;

&lt;p&gt;Sometimes the change is not just color: a different map, a switch from matte to glossy, transparent to solid. Then you do need a different material, and you must dispose of the old one yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;
&lt;span class="nx"&gt;mesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildMaterial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                 &lt;span class="c1"&gt;// frees the program and uniforms&lt;/span&gt;
&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;            &lt;span class="c1"&gt;// textures are separate, dispose them too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule: &lt;strong&gt;&lt;code&gt;.dispose()&lt;/code&gt; anything you replace, and do not replace what you can mutate.&lt;/strong&gt; Color, roughness, metalness, opacity are all live properties. Reach for a new material only when the shader itself has to change.&lt;/p&gt;

&lt;p&gt;The decorative lamp configurator at &lt;a href="https://lampy3d.grodev.pl" rel="noopener noreferrer"&gt;lampy3d.grodev.pl&lt;/a&gt; swaps finishes constantly and holds flat memory because of exactly this.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I am Dominik Groński / GroDev, a studio in Poznan, Poland building custom 3D configurators and Laravel back-ends for manufacturers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webgl</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>CAD to glTF - the manufacturer file pipeline nobody warns you about</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Fri, 30 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/cad-to-gltf-the-manufacturer-file-pipeline-nobody-warns-you-about-3ch9</link>
      <guid>https://dev.to/gronskideveloper/cad-to-gltf-the-manufacturer-file-pipeline-nobody-warns-you-about-3ch9</guid>
      <description>&lt;p&gt;The demo looks easy: load a glTF, orbit around it, swap a material. Then a real manufacturer sends you their actual product file and you learn that the hard part was never the Three.js. It was getting their CAD into something a browser can load in under three seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually arrives
&lt;/h2&gt;

&lt;p&gt;Manufacturers model in SolidWorks, Inventor, Rhino, or hand you a STEP file. Exported to glTF, a single product variant lands at &lt;strong&gt;40 to 120MB&lt;/strong&gt;. It is usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Z-up&lt;/strong&gt;, because that is the CAD convention. Three.js is Y-up, so your model is lying on its back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaled in millimeters or inches&lt;/strong&gt;, so it is either microscopic or the size of a building.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripped of materials&lt;/strong&gt;, or carrying CAD materials that mean nothing to a PBR renderer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thousands of separate parts&lt;/strong&gt;, every bolt its own mesh, which is thousands of draw calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is the manufacturer doing anything wrong. It is just that CAD and realtime web rendering want opposite things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;I run every incoming model through the same stages before it goes near the configurator:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decimate and merge in Blender.&lt;/strong&gt; Collapse the part tree, remove internal geometry the customer never sees, weld bolts and screws into the parent. A 4000-part assembly becomes a handful of meshes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix orientation and scale on import.&lt;/strong&gt; Rotate Z-up to Y-up, set 1 unit = 1 meter. Do it once, bake it, never think about it again in the app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reassign PBR materials by slot.&lt;/strong&gt; Not per part. Group surfaces into material slots (frame, glass, hardware) so a color picker touches one material, not two hundred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compress.&lt;/strong&gt; &lt;code&gt;gltf-transform&lt;/code&gt; or &lt;code&gt;gltf-pipeline&lt;/code&gt; with Draco for geometry. For textures, KTX2 so the GPU stores them compressed, not just the file.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gltf-transform optimize &lt;span class="k"&gt;in&lt;/span&gt;.glb out.glb &lt;span class="nt"&gt;--compress&lt;/span&gt; draco &lt;span class="nt"&gt;--texture-compress&lt;/span&gt; ktx2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;90MB in, 2 to 5MB out, with no visible loss on a product shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part the quote forgets
&lt;/h2&gt;

&lt;p&gt;Here is the thing the demo budget never includes: &lt;strong&gt;this pipeline is per product line, and it is ongoing.&lt;/strong&gt; New product, new file, run it again. New variant, new export. If the manufacturer updates a model, someone reprocesses it.&lt;/p&gt;

&lt;p&gt;When I scope a configurator I treat the asset pipeline as its own line item, sometimes with a small Blender script or a Laravel queue job so reprocessing is one command, not a day of clicking. The &lt;a href="https://grodev.pl" rel="noopener noreferrer"&gt;studio&lt;/a&gt; runs several of these in production, one per manufacturer, each fed by the same pipeline.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I am Dominik Groński / GroDev, a studio in Poznan, Poland building custom 3D configurators and Laravel back-ends for manufacturers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webgl</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Visual CPQ for machine builders: let the customer lay out the cell</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Fri, 30 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/visual-cpq-for-machine-builders-let-the-customer-lay-out-the-cell-13kf</link>
      <guid>https://dev.to/gronskideveloper/visual-cpq-for-machine-builders-let-the-customer-lay-out-the-cell-13kf</guid>
      <description>&lt;p&gt;A machine builder told me their quoting cycle for a production line was three to six weeks. Every enquiry went back to engineering for a drawing, an equipment list and a cost estimate. Meanwhile the customer asked a competitor. That gap is exactly what visual CPQ closes.&lt;/p&gt;

&lt;p&gt;I build configurators, including visual CPQ systems for machine builders and line integrators, so here is how it works and when it is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What visual CPQ actually is
&lt;/h2&gt;

&lt;p&gt;CPQ means Configure, Price, Quote. The visual part means the customer does the configure step themselves, in 3D, in the browser, inside guardrails you define. Instead of describing what they want in an email, they lay out a production cell from your real catalogue: conveyors, a robot, a case packer, whatever your line is built from.&lt;/p&gt;

&lt;p&gt;The system then does the work that used to go to engineering first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validates the layout&lt;/strong&gt; for collisions and safety-zone clearances as it is built, so an impossible or unsafe configuration cannot be submitted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulates throughput&lt;/strong&gt; and shows the bottleneck, so the customer sizes the line correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returns a priced bill of materials&lt;/strong&gt;, computed to your rules, plus an engineering-review request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is that engineering receives a complete, buildable specification instead of "how much does a line like this cost".&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a PDF and an email lose this race
&lt;/h2&gt;

&lt;p&gt;The old cycle fails on timing and on data quality at the same time.&lt;/p&gt;

&lt;p&gt;Timing: the customer who can self-serve a validated layout in an afternoon does not wait three weeks. In Maschinenbau, where the buyer is comparing two or three suppliers, the one who answers fastest with real numbers is in front.&lt;/p&gt;

&lt;p&gt;Data quality: a free-text enquiry is ambiguous. A configured cell is not. It carries dimensions, equipment, interfaces and constraints, so the quote that comes back is accurate and the project starts without a round of clarifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it connects to
&lt;/h2&gt;

&lt;p&gt;A visual CPQ system is only useful if the output lands where your business already runs. The priced configuration and the bill of materials flow into SAP, a custom ERP, a PLM or your CRM over an API, so the quote and the order carry a full, validated spec. The code runs on your own infrastructure in the EU, and it is yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it running
&lt;/h2&gt;

&lt;p&gt;I built a visual CPQ demonstrator you can click with no sign-up. Drop in conveyors, a robot and a packer, watch the collision checks fire, run the throughput and read the priced bill of materials: &lt;a href="https://factory3d.grodev.pl" rel="noopener noreferrer"&gt;factory3d.grodev.pl&lt;/a&gt;. The longer write-up for machine builders is on the &lt;a href="https://grodev.pl/en/visual-cpq-for-machine-builders" rel="noopener noreferrer"&gt;visual CPQ page&lt;/a&gt;, and the full set of configurators is on the &lt;a href="https://grodev.pl/en/custom-3d-configurator" rel="noopener noreferrer"&gt;main configurator page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To be straight: that is my own demonstrator, not a client rollout. I show it so the mechanics stop being a promise before anyone pays for a build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Ballpark, so you can judge fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From &lt;strong&gt;EUR 40,000&lt;/strong&gt; for a configurable cell with collision checks, a throughput simulation and a priced bill of materials, wired into your quoting flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EUR 60,000 to 120,000+&lt;/strong&gt; for a multi-line or multi-plant platform with deep ERP and PLM integration and an operator console.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One fixed price after a scoping call. It is a one-off build you own, which removes the recurring licence that enterprise CPQ platforms charge year after year.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Who is visual CPQ for?&lt;/strong&gt;&lt;br&gt;
Machine builders and line integrators whose quoting bottleneck is engineering time per enquiry, and who lose deals to slower-but-not-really competitors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it really validate collisions and safety zones?&lt;/strong&gt;&lt;br&gt;
Yes. The layout is checked as the customer places equipment, so unsafe or impossible configurations cannot be submitted. The throughput simulation shows the bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it replace engineering?&lt;/strong&gt;&lt;br&gt;
No. It moves the first pass to the customer so engineering starts from a validated specification instead of a blank enquiry. It gives your engineers a head start, not a replacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will it integrate with our PLM and ERP?&lt;/strong&gt;&lt;br&gt;
Yes, over an API: SAP, a custom ERP, a PLM or your CRM. The code is yours and runs on your infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does it take to build?&lt;/strong&gt;&lt;br&gt;
A configurable cell is a few months depending on the equipment library and the rules. A paid pilot on one cell type is a good way to prove it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cut your quoting cycle
&lt;/h2&gt;

&lt;p&gt;Tell me what you build and how you quote it today. I will send a fixed quote within 48h, or an honest "this is not for me". Email: &lt;a href="mailto:dominik.gronski@grodev.pl"&gt;dominik.gronski@grodev.pl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Written by Dominik Groński GroDev, a solo full-stack developer building custom 3D configurators and Laravel back-ends for manufacturers across the EU, UK and US.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>threejs</category>
      <category>manufacturing</category>
    </item>
    <item>
      <title>Individueller 3D-Konfigurator vs Zakeke, Roomle, Threekit: selbst bauen oder kaufen?</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Thu, 29 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/individueller-3d-konfigurator-vs-zakeke-roomle-threekit-selbst-bauen-oder-kaufen-5cei</link>
      <guid>https://dev.to/gronskideveloper/individueller-3d-konfigurator-vs-zakeke-roomle-threekit-selbst-bauen-oder-kaufen-5cei</guid>
      <description>&lt;p&gt;Ein Hersteller mit einem Produkt in vielen Varianten stellt mir alle paar Wochen dieselbe Frage: "Soll ich Zakeke, Roomle oder Threekit kaufen oder einen bauen lassen?"&lt;/p&gt;

&lt;p&gt;Meine ehrliche Antwort beginnt mit einer Gegenfrage: muss Ihr Konfigurator das Produkt nur &lt;strong&gt;zeigen&lt;/strong&gt; oder es &lt;strong&gt;verstehen&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Wenn er es nur zeigen muss (ein Modell drehen, eine Farbe wählen, gut aussehen), kaufen Sie eine SaaS und zahlen nicht zu viel. Wenn er es verstehen muss (nach Ihren Regeln bepreisen, unmögliche Variantenkombinationen blockieren, eine fertige Bestellung in Ihr ERP schieben), kämpft eine Vorlage ewig gegen Sie, und Sie bauen individuell.&lt;/p&gt;

&lt;p&gt;Hier die ganze Analyse, mit dem Teil, den die meisten Vergleiche auslassen: die Kosten über drei Jahre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die eine Regel, die entscheidet
&lt;/h2&gt;

&lt;p&gt;Fertige Konfiguratoren (Zakeke, Roomle, Threekit, ConfigOne und der Rest) sind sehr gut in der visuellen Ebene. Sie bekommen schnell 3D im Browser, ohne Entwickler. Für ein Standardprodukt, eine einfache Optionsliste und ein kleines Budget ist das das richtige Werkzeug. Ich sage das offen, statt jemandem einen Build zu verkaufen, den er nicht braucht.&lt;/p&gt;

&lt;p&gt;Es wird schwierig, sobald Ihre Preise oder Regeln nicht mehr in die Vorlage passen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ihr Preis hängt von Mengenstaffeln, Großhandelsstufen und Margen ab, die nicht öffentlich sein sollen.&lt;/li&gt;
&lt;li&gt;Option A verändert, was bei Option B möglich ist, und die Kombinationen müssen validiert werden.&lt;/li&gt;
&lt;li&gt;Die fertige Konfiguration muss in SAP, ein eigenes ERP oder einen bestimmten Warenkorb, nicht nur in eine E-Mail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Genau da schlägt "verstehen" das "zeigen", und da rechnet sich individuell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build vs Buy im direkten Vergleich
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kriterium&lt;/th&gt;
&lt;th&gt;Individueller Build&lt;/th&gt;
&lt;th&gt;Fertige SaaS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Zahlungsmodell&lt;/td&gt;
&lt;td&gt;Einmaliges Projekt&lt;/td&gt;
&lt;td&gt;Abo, dauerhaft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typischer Preis&lt;/td&gt;
&lt;td&gt;Ab ~15.000 EUR einmalig&lt;/td&gt;
&lt;td&gt;~8.000 bis 100.000+ EUR pro Jahr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quellcode&lt;/td&gt;
&lt;td&gt;Ihrer, auf Ihrem Repo und Hosting&lt;/td&gt;
&lt;td&gt;Keiner, Sie mieten Zugang&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wo der Preis berechnet wird&lt;/td&gt;
&lt;td&gt;Serverseitig, nicht in den Devtools sichtbar&lt;/td&gt;
&lt;td&gt;Oft clientseitig&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preis- und Variantenregeln&lt;/td&gt;
&lt;td&gt;Jede Logik, die Sie beschreiben können&lt;/td&gt;
&lt;td&gt;Innerhalb der Vorlage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ERP- / Shop-Anbindung&lt;/td&gt;
&lt;td&gt;Beliebig, über eine API&lt;/td&gt;
&lt;td&gt;Innerhalb der Konnektoren des Anbieters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anbieter-Lock-in&lt;/td&gt;
&lt;td&gt;Keiner, Daten und Engine sind Ihre&lt;/td&gt;
&lt;td&gt;Ja, beides bleibt beim Anbieter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Die Drei-Jahres-Kosten, die auf keiner Preisseite stehen
&lt;/h2&gt;

&lt;p&gt;Das ist die Zahl, die für die meisten mittelständischen Hersteller die Entscheidung dreht.&lt;/p&gt;

&lt;p&gt;Eine Enterprise-Konfigurator-SaaS kostet nicht 50 EUR im Monat. Veröffentlichte und geschätzte Spannen liegen zwischen 8.000 und 100.000+ EUR &lt;strong&gt;pro Jahr&lt;/strong&gt;, sobald Sie echte Varianten, Integrationen und Nutzer brauchen. Rechnen Sie das weiter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jahr 1: Sie zahlen und haben ein gemietetes Werkzeug.&lt;/li&gt;
&lt;li&gt;Jahr 2: Sie zahlen erneut, dasselbe Werkzeug.&lt;/li&gt;
&lt;li&gt;Jahr 3: Sie zahlen erneut und besitzen weiterhin nichts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ein einmaliger Build ist eine einzige Zahlung plus Hosting (oft ein paar Dutzend Euro im Monat). Der Break-even gegenüber einem Enterprise-Abo liegt meist innerhalb der ersten 12 bis 24 Monate. Danach besitzen Sie einen Vermögenswert statt einer Miete, und eine Preisänderung ist eine Datenänderung auf Ihrem Server, keine neue Lizenzstufe.&lt;/p&gt;

&lt;p&gt;Das ist das ganze Argument in einem Satz: mit SaaS zahlen Sie weiter, um Ihren eigenen Konfigurator nutzen zu dürfen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Die stille technische Falle: clientseitige Preise
&lt;/h2&gt;

&lt;p&gt;Ein Detail entscheidet mehr als das Preisschild. In vielen Plugin-Konfiguratoren wird der Preis &lt;strong&gt;im Browser&lt;/strong&gt; berechnet. Das heißt, Ihre Mengenschwellen, Großhandelspreise und Margen stecken in JavaScript, das jeder in den Devtools öffnen und lesen oder ändern kann.&lt;/p&gt;

&lt;p&gt;Ein individueller Build berechnet den Preis &lt;strong&gt;serverseitig&lt;/strong&gt;. Der Browser erhält ein Ergebnis, nie die Preisliste. Für B2B, wo Ihre Nettopreise das sind, was Sie am meisten schützen wollen, ist das kein Nice-to-have. Es ist der Grund zu bauen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wann ich zur SaaS rate
&lt;/h2&gt;

&lt;p&gt;Um ehrlich zu bleiben, die Fälle, in denen ich zu einem fertigen Werkzeug rate und keinen Build anbiete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Das Produkt ist Standard und die Optionen sind einfach.&lt;/li&gt;
&lt;li&gt;Sie brauchen es in Tagen, nicht Wochen.&lt;/li&gt;
&lt;li&gt;Es gibt kein ERP und keine Preislogik zu integrieren.&lt;/li&gt;
&lt;li&gt;Das Budget ist klein und ein Abo ist über Ihren realen Horizont wirklich günstiger.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wenn das auf Sie zutrifft, erledigen Zakeke oder Roomle den Job. Kein Build nötig.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sehen Sie die Mechanik, bevor Sie dafür zahlen
&lt;/h2&gt;

&lt;p&gt;Ich baue individuelle 3D-Konfiguratoren und habe zehn davon als Live-Demonstratoren online gestellt, damit die Mechanik kein Versprechen bleibt. Es sind meine eigenen Demos, keine Kundenprojekte, und Sie können jede ohne Anmeldung anklicken:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ein &lt;a href="https://pergola3d.grodev.pl" rel="noopener noreferrer"&gt;Pergola-Konfigurator&lt;/a&gt; mit Sonnensimulator und Regentest.&lt;/li&gt;
&lt;li&gt;Ein &lt;a href="https://sauna3d.grodev.pl" rel="noopener noreferrer"&gt;Sauna- und Spa-Konfigurator&lt;/a&gt; mit kinoreifem nordischem Render.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://grodev.pl/de/visual-cpq-maschinenbau" rel="noopener noreferrer"&gt;Visual CPQ für Maschinenbauer&lt;/a&gt;: eine Fertigungszelle planen, mit Kollisionsprüfung und bepreister Stückliste.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Das volle Set und der längere Make-or-Buy-Vergleich stehen auf der &lt;a href="https://grodev.pl/de/3d-produktkonfigurator" rel="noopener noreferrer"&gt;Hauptseite zum 3D-Produktkonfigurator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was ein individueller Build kostet
&lt;/h2&gt;

&lt;p&gt;Richtwerte, damit Sie die Passung vor jedem Gespräch einschätzen können:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ab &lt;strong&gt;15.000 EUR&lt;/strong&gt; für eine Produktlinie mit Echtzeit-3D und serverseitiger Live-Kalkulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;25.000 bis 45.000 EUR&lt;/strong&gt; mit Variantenkatalog, Abhängigkeitsregeln und einem Admin-Panel, das Ihr Team bedient.&lt;/li&gt;
&lt;li&gt;Ab &lt;strong&gt;60.000 EUR&lt;/strong&gt; für ein Multi-Brand- oder Betreiberportal mit tiefer ERP-Integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ein Festpreis nach einem kurzen Gespräch, keine Spanne mit verstecktem "ab".&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Ist individuell immer besser als SaaS?&lt;/strong&gt;&lt;br&gt;
Nein. Für ein Standardprodukt mit einfachen Optionen und ohne Integration ist eine SaaS schneller und günstiger. Individuell gewinnt bei Preisregeln, ERP-Integration, Code-Eigentum und einem mehrjährigen Horizont.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wie lange dauert ein individueller Build?&lt;/strong&gt;&lt;br&gt;
Von etwa drei Wochen für eine Produktlinie bis zu einigen Monaten für ein Multi-Brand-Portal. Ein bezahlter Pilot an einem echten Produkt ist ein guter Start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wem gehört der Code?&lt;/strong&gt;&lt;br&gt;
Ihnen, ab dem ersten Commit, auf Ihrem Repository und Hosting. Keine Lizenz, um Ihr eigenes Werkzeug zu nutzen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lässt es sich an mein ERP anbinden?&lt;/strong&gt;&lt;br&gt;
Ja, über eine API: SAP, ein eigenes ERP, PIM, WooCommerce, Shopify oder eine schlanke Seite, die die Bestellung an Ihr CRM übergibt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warum serverseitige Preise?&lt;/strong&gt;&lt;br&gt;
Damit Ihre Staffeln und Margen nie im Browser sichtbar oder editierbar sind. Konfiguratoren, die rein in JavaScript rechnen, haben dieses Problem per Definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build or Buy, für Ihren Fall entschieden
&lt;/h2&gt;

&lt;p&gt;Schicken Sie mir, was Sie verkaufen und wie Sie heute kalkulieren. Ich sage Ihnen ehrlich, ob Sie eine SaaS kaufen oder bauen sollten, und wenn es ein Build ist, einen Festpreis innerhalb von 48 Stunden. E-Mail: &lt;a href="mailto:dominik.gronski@grodev.pl"&gt;dominik.gronski@grodev.pl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Geschrieben von Dominik Groński GroDev, einem Solo-Full-Stack-Entwickler, der individuelle 3D-Konfiguratoren und Laravel-Backends für Hersteller in der EU, UK und den USA baut.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>threejs</category>
      <category>saas</category>
    </item>
    <item>
      <title>How much does a custom 3D product configurator really cost?</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Tue, 27 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/how-much-does-a-custom-3d-product-configurator-really-cost-2n0c</link>
      <guid>https://dev.to/gronskideveloper/how-much-does-a-custom-3d-product-configurator-really-cost-2n0c</guid>
      <description>&lt;p&gt;"From $300." I see that number on plugin pages and it is the most expensive sentence in the category, because it is the start of a bill, not the end of one.&lt;/p&gt;

&lt;p&gt;I build custom 3D product configurators, so people ask me the real number constantly. Here it is, with the parts that actually move it, so you can budget before any call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;p&gt;A custom 3D product configurator, built from scratch and owned by you, lands in these ranges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From &lt;strong&gt;EUR 15,000&lt;/strong&gt; for one product line: real-time 3D, server-side live pricing, mobile-tuned, lead or cart output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EUR 25,000 to 45,000&lt;/strong&gt; for a variant catalogue, dependency rules and an admin panel your team runs.&lt;/li&gt;
&lt;li&gt;From &lt;strong&gt;EUR 60,000&lt;/strong&gt; for a multi-brand or operator portal with deep ERP integration and custom logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One fixed price after a short call. Not a range with a hidden "from" that grows mid-project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moves the price
&lt;/h2&gt;

&lt;p&gt;The headline number is set by five things, in roughly this order of impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Do you have 3D models?
&lt;/h3&gt;

&lt;p&gt;If you already have CAD or 3D files, we start from them and you save real money. If not, there are cheaper paths than a photorealistic render per SKU: a &lt;strong&gt;parametric model&lt;/strong&gt; that resizes, recolours and reconfigures itself (ideal for furniture, joinery, packaging, profile systems), or photos combined with a 2D configuration where full 3D is not worth it. The wrong assumption here, "we need a perfect render of all 800 products", is what blows budgets.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. How complex is the pricing logic?
&lt;/h3&gt;

&lt;p&gt;A single rate card is cheap. Quantity tiers, wholesale levels, margins, surcharges and dependency rules (option A changes what is possible for option B) are where the engineering sits. This is also where a custom build earns its keep, because a template cannot express rules it was not designed for.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. What does it integrate with?
&lt;/h3&gt;

&lt;p&gt;A clean page that emails the order is the low end. Pushing a validated order with a full spec into WooCommerce, Shopify, SAP or a custom ERP is more, and worth it, because it removes manual re-entry and the errors that come with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. How many variants and languages?
&lt;/h3&gt;

&lt;p&gt;An unlimited SKU catalogue with an admin panel your staff edit, plus three or four languages for export markets, moves the number up. But it moves it once. A SaaS moves it every year.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Server-side pricing
&lt;/h3&gt;

&lt;p&gt;Computing the price on the server (so your margins are never visible in devtools) is a small line item and a large protection. I treat it as default for anything B2B.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "from $300" is a trap
&lt;/h2&gt;

&lt;p&gt;A cheap plugin gives you a dropdown and a photo that often does not match the selection. The customer still emails "is it possible to..." and you still answer by hand. Worse, the price usually computes in the browser, so your wholesale tiers sit in JavaScript that anyone can read. The $300 is real. The cost of everything it does not do is the actual bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that decides it: three-year cost
&lt;/h2&gt;

&lt;p&gt;Compare like for like over a realistic horizon.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Custom build&lt;/th&gt;
&lt;th&gt;Enterprise configurator SaaS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Up front&lt;/td&gt;
&lt;td&gt;From EUR 15,000 once&lt;/td&gt;
&lt;td&gt;Setup + first year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year 2&lt;/td&gt;
&lt;td&gt;Hosting only (~tens of EUR/mo)&lt;/td&gt;
&lt;td&gt;Full subscription again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year 3&lt;/td&gt;
&lt;td&gt;Hosting only&lt;/td&gt;
&lt;td&gt;Full subscription again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After 3 years&lt;/td&gt;
&lt;td&gt;You own the code&lt;/td&gt;
&lt;td&gt;You own nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Enterprise configurator subscriptions commonly run EUR 8,000 to 100,000+ per year. The break-even against them usually lands inside the first 12 to 24 months. After that, a custom build is an asset and the SaaS is still a rental.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get a real fixed price
&lt;/h2&gt;

&lt;p&gt;Two steps, no guesswork.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A short call: you show the product and its variants, we agree what the customer can change, how the price is calculated and where orders go.&lt;/li&gt;
&lt;li&gt;A fixed quote within 48h. If it is a build, I take one product as a &lt;strong&gt;paid pilot&lt;/strong&gt;, with real SKUs and real pricing rules, and ship a working configurator in the browser in about three weeks. The pilot fee counts toward the full build, and the pilot code is yours either way.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can see what the finished thing looks like first: ten live configurators you can click with no sign-up are on the &lt;a href="https://grodev.pl/en/custom-3d-configurator" rel="noopener noreferrer"&gt;custom 3D configurator page&lt;/a&gt;, including a &lt;a href="https://sauna3d.grodev.pl" rel="noopener noreferrer"&gt;sauna configurator&lt;/a&gt; and a &lt;a href="https://opakowania3d.grodev.pl" rel="noopener noreferrer"&gt;packaging configurator&lt;/a&gt; with volume pricing.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the cheapest a real custom configurator can be?&lt;/strong&gt;&lt;br&gt;
Around EUR 15,000 for one product line with real-time 3D and live pricing. Below that you are usually buying a plugin, which is fine for a simple, show-only product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just buy a SaaS?&lt;/strong&gt;&lt;br&gt;
For a standard product with simple options and no integration, you should. Custom wins on pricing rules, ERP integration, code ownership and a multi-year horizon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the price include the 3D models?&lt;/strong&gt;&lt;br&gt;
Models are scoped separately and transparently, because the cost depends entirely on what you already have. CAD files make it cheap. Nothing at all means parametric modelling, which I price up front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there any subscription?&lt;/strong&gt;&lt;br&gt;
No. It is a one-off project plus your own hosting. No per-order commission, no per-view fee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who owns the code?&lt;/strong&gt;&lt;br&gt;
You do, from the first commit, on your repository and hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get your number
&lt;/h2&gt;

&lt;p&gt;Send me what you sell and how you price it today, and I will send a fixed quote within 48h, with an honest "buy a SaaS instead" if that is the right call. Email: &lt;a href="mailto:dominik.gronski@grodev.pl"&gt;dominik.gronski@grodev.pl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Written by Dominik Groński GroDev, a solo full-stack developer building custom 3D configurators and Laravel back-ends for manufacturers across the EU, UK and US.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>threejs</category>
      <category>saas</category>
    </item>
    <item>
      <title>Self-host your Draco decoder - the 600ms CDN tax on Three.js configurators</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Tue, 27 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/self-host-your-draco-decoder-the-600ms-cdn-tax-on-threejs-configurators-1f0f</link>
      <guid>https://dev.to/gronskideveloper/self-host-your-draco-decoder-the-600ms-cdn-tax-on-threejs-configurators-1f0f</guid>
      <description>&lt;p&gt;Draco compression is the difference between a 90MB glTF export and a 3MB one. For CAD-heavy product models it is not optional. But there is a trap in the standard setup that quietly costs you half a second on first paint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default that looks fine
&lt;/h2&gt;

&lt;p&gt;Most examples point the decoder at Google's CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dracoLoader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DRACOLoader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;dracoLoader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDecoderPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.gstatic.com/draco/v1/decoders/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works. It also means every first-time visitor makes a cross-origin trip to fetch the WASM decoder before a single triangle appears. In my measurements on real configurators that added around &lt;strong&gt;600ms to first paint&lt;/strong&gt; on a cold cache, right at the moment the user is staring at an empty canvas deciding whether to wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: self-host the decoder
&lt;/h2&gt;

&lt;p&gt;Copy the decoder out of the Three.js package you already have installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules/three/examples/jsm/libs/draco/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it in your &lt;code&gt;public/&lt;/code&gt; folder and point at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dracoLoader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DRACOLoader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;dracoLoader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDecoderPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/draco/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// served from your own origin&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GLTFLoader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDRACOLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dracoLoader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the decoder loads from the same origin, in parallel with the rest of your assets, cached by the same CDN or server rules as everything else. No extra cross-origin handshake on the critical path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things people miss
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The decoder is WASM plus a JS wrapper.&lt;/strong&gt; Copy the whole &lt;code&gt;draco/&lt;/code&gt; folder, not just one file, or you get a confusing "failed to load" with no stack trace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the right cache headers.&lt;/strong&gt; The decoder never changes between builds. Give it a long &lt;code&gt;Cache-Control: max-age&lt;/code&gt; so returning visitors pay nothing. First visit is the only one that touches it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Small change, measurable win, and it lives on the critical path so it punches above its weight. The pool configurator at &lt;a href="https://basen3d.grodev.pl" rel="noopener noreferrer"&gt;basen3d.grodev.pl&lt;/a&gt; serves its decoder this way.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I am Dominik Groński / GroDev, a studio in Poznan, Poland building custom 3D configurators and Laravel back-ends for manufacturers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webgl</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Custom 3D configurator vs Zakeke, Roomle, Threekit: build or buy?</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Sat, 24 Oct 2026 18:31:27 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/custom-3d-configurator-vs-zakeke-roomle-threekit-build-or-buy-3ng2</link>
      <guid>https://dev.to/gronskideveloper/custom-3d-configurator-vs-zakeke-roomle-threekit-build-or-buy-3ng2</guid>
      <description>&lt;p&gt;A manufacturer with a product that sells in many variants asks me the same question every few weeks: "Should I buy Zakeke, Roomle or Threekit, or have one built?"&lt;/p&gt;

&lt;p&gt;My honest answer starts with a single question back: does your configurator have to &lt;strong&gt;show&lt;/strong&gt; the product, or &lt;strong&gt;understand&lt;/strong&gt; it?&lt;/p&gt;

&lt;p&gt;If it only has to show (rotate a model, pick a colour, look nice), buy a SaaS and do not overpay. If it has to understand (price it by your own rules, block impossible variant combinations, push a finished order into your ERP), a template will fight you forever and you build custom.&lt;/p&gt;

&lt;p&gt;Here is the full breakdown, with the part most comparison posts skip: the three-year cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one rule that decides it
&lt;/h2&gt;

&lt;p&gt;Off-the-shelf configurators (Zakeke, Roomle, Threekit, ConfigOne and the rest) are very good at the visual layer. You get 3D in the browser fast, with no developer. For a standard product, a simple option list and a small budget, that is the right tool. I tell people this out loud instead of selling them a build they do not need.&lt;/p&gt;

&lt;p&gt;They start to hurt the moment your pricing or your rules stop fitting the template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your price depends on quantity tiers, wholesale levels and margins you do not want public.&lt;/li&gt;
&lt;li&gt;Option A changes what is possible for option B, and the combinations have to be validated.&lt;/li&gt;
&lt;li&gt;The finished configuration has to land in SAP, a custom ERP or a specific cart, not just an email.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where "understand" beats "show", and where custom pays off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build vs buy, side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Custom build&lt;/th&gt;
&lt;th&gt;Off-the-shelf SaaS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Payment model&lt;/td&gt;
&lt;td&gt;One-off project&lt;/td&gt;
&lt;td&gt;Subscription, forever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical price&lt;/td&gt;
&lt;td&gt;From ~EUR 15,000 once&lt;/td&gt;
&lt;td&gt;~EUR 8,000 to 100,000+ per year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source code&lt;/td&gt;
&lt;td&gt;Yours, on your repo and hosting&lt;/td&gt;
&lt;td&gt;None, you rent access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where price is computed&lt;/td&gt;
&lt;td&gt;Server-side, not visible in devtools&lt;/td&gt;
&lt;td&gt;Often client-side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing and variant rules&lt;/td&gt;
&lt;td&gt;Any logic you can describe&lt;/td&gt;
&lt;td&gt;Within the template limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ERP / shop integration&lt;/td&gt;
&lt;td&gt;Any, over an API&lt;/td&gt;
&lt;td&gt;Within the vendor's connectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor lock-in&lt;/td&gt;
&lt;td&gt;None, data and engine are yours&lt;/td&gt;
&lt;td&gt;Yes, both stay with the vendor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The three-year cost nobody prints on the pricing page
&lt;/h2&gt;

&lt;p&gt;This is the number that flips the decision for most mid-size manufacturers.&lt;/p&gt;

&lt;p&gt;An enterprise configurator SaaS is not EUR 50 a month. Published and estimated ranges land somewhere between EUR 8,000 and 100,000+ &lt;strong&gt;per year&lt;/strong&gt; once you need real variants, integrations and seats. Run that forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Year 1: you pay, and you have a rented tool.&lt;/li&gt;
&lt;li&gt;Year 2: you pay again, same tool.&lt;/li&gt;
&lt;li&gt;Year 3: you pay again, and you still own nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A one-off custom build is a single payment plus hosting (often tens of euros a month). The break-even against an enterprise subscription usually lands inside the first 12 to 24 months. After that, you own an asset instead of renting one, and the price list is a data change on your server, not a new licence tier.&lt;/p&gt;

&lt;p&gt;That is the whole argument in one line: with SaaS you keep paying to keep using your own configurator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quiet technical trap: client-side pricing
&lt;/h2&gt;

&lt;p&gt;One detail decides more than the price tag. In a lot of plug-in configurators, the price is computed &lt;strong&gt;in the browser&lt;/strong&gt;. That means your quantity thresholds, wholesale prices and margins sit in JavaScript that anyone can open in devtools and read, or edit.&lt;/p&gt;

&lt;p&gt;A custom build computes the price &lt;strong&gt;server-side&lt;/strong&gt;. The browser receives a result, never the price list. For B2B, where your net prices are the thing you most want to protect, this is not a nice-to-have. It is the reason to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I tell people to buy the SaaS
&lt;/h2&gt;

&lt;p&gt;To keep this honest, the cases where I send people to an off-the-shelf tool and do not pitch a build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The product is standard and the options are simple.&lt;/li&gt;
&lt;li&gt;You need it live in days, not weeks.&lt;/li&gt;
&lt;li&gt;There is no ERP or pricing logic to integrate.&lt;/li&gt;
&lt;li&gt;The budget is small and a subscription is genuinely cheaper over your real horizon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If that is you, Zakeke or Roomle will do the job. No build needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  See the mechanics before you pay for them
&lt;/h2&gt;

&lt;p&gt;I build custom 3D configurators, and I put ten of them online as live demonstrators so the mechanics stop being a promise. They are my own demos, not client work, and you can click any of them with no sign-up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://pergola3d.grodev.pl" rel="noopener noreferrer"&gt;bioclimatic pergola configurator&lt;/a&gt; with a sun simulator and a rain test.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://sauna3d.grodev.pl" rel="noopener noreferrer"&gt;sauna and spa configurator&lt;/a&gt; with a cinematic Nordic render.&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://opakowania3d.grodev.pl" rel="noopener noreferrer"&gt;packaging configurator&lt;/a&gt; where the unit price drops with volume and you download a print-ready die-line.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://grodev.pl/en/visual-cpq-for-machine-builders" rel="noopener noreferrer"&gt;Visual CPQ for machine builders&lt;/a&gt;: lay out a production cell, with collision checks and a priced bill of materials.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full set and the longer build-vs-buy comparison live on the &lt;a href="https://grodev.pl/en/custom-3d-configurator" rel="noopener noreferrer"&gt;custom 3D configurator page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a custom build costs
&lt;/h2&gt;

&lt;p&gt;Ballpark, so you can judge fit before any call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From &lt;strong&gt;EUR 15,000&lt;/strong&gt; for one product line with real-time 3D and server-side live pricing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EUR 25,000 to 45,000&lt;/strong&gt; with a variant catalogue, dependency rules and an admin panel your team runs.&lt;/li&gt;
&lt;li&gt;From &lt;strong&gt;EUR 60,000&lt;/strong&gt; for a multi-brand or operator portal with deep ERP integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One fixed price after a short call, not a range with a hidden "from".&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is a custom configurator always better than SaaS?&lt;/strong&gt;&lt;br&gt;
No. For a standard product with simple options and no integration, a SaaS is faster and cheaper. Custom wins when pricing rules, ERP integration, code ownership or a multi-year horizon are in play.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long does a custom build take?&lt;/strong&gt;&lt;br&gt;
From about three weeks for a single product line to a few months for a multi-brand portal. A paid pilot on one real product is a good way to start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who owns the code?&lt;/strong&gt;&lt;br&gt;
You do. It goes to your repository and your hosting from the first commit. No licence to use your own tool, and you can hand it to another developer any time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it integrate with my ERP?&lt;/strong&gt;&lt;br&gt;
Yes, over an API: SAP, a custom ERP, PIM, WooCommerce, Shopify or a clean page that pushes the order to your CRM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why server-side pricing?&lt;/strong&gt;&lt;br&gt;
So your wholesale tiers and margins are never visible or editable in the browser. Configurators that price purely in JavaScript have that problem by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build or buy, decided for your case
&lt;/h2&gt;

&lt;p&gt;Send me what you sell and how you price it today. I will tell you honestly whether to buy a SaaS or build custom, and if it is a build, a fixed quote within 48h. Email: &lt;a href="mailto:dominik.gronski@grodev.pl"&gt;dominik.gronski@grodev.pl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Written by Dominik Groński GroDev, a solo full-stack developer building custom 3D configurators and Laravel back-ends for manufacturers across the EU, UK and US.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>threejs</category>
      <category>saas</category>
    </item>
    <item>
      <title>Custom 3D configurator vs Zakeke or Roomle - when to actually build your own</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Sat, 24 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/custom-3d-configurator-vs-zakeke-or-roomle-when-to-actually-build-your-own-2cd3</link>
      <guid>https://dev.to/gronskideveloper/custom-3d-configurator-vs-zakeke-or-roomle-when-to-actually-build-your-own-2cd3</guid>
      <description>&lt;p&gt;I build custom 3D product configurators for a living, so take this with the obvious grain of salt. But I talk a lot of prospects out of a custom build, because for many of them an off-the-shelf tool is the right answer. Here is how I decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  When off-the-shelf wins (Zakeke, Roomle, ThreeKit and friends)
&lt;/h2&gt;

&lt;p&gt;Use a SaaS configurator when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your product fits a &lt;strong&gt;standard parametric model&lt;/strong&gt;: apparel, furniture with clean options, signage, packaging with print areas.&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;launch in days&lt;/strong&gt;, not a quarter.&lt;/li&gt;
&lt;li&gt;Your pricing is &lt;strong&gt;option-based and simple&lt;/strong&gt;: base price plus per-option surcharges.&lt;/li&gt;
&lt;li&gt;You are fine living inside &lt;strong&gt;their data model and their UI&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Your volume does not make the &lt;strong&gt;per-seat or per-view fee&lt;/strong&gt; hurt yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If that is you, pay the subscription. You get a maintained renderer, a dashboard, and someone else on call when WebGL breaks on a new iOS version. Rebuilding that yourself is not a flex, it is a cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  When custom actually pays off
&lt;/h2&gt;

&lt;p&gt;I tell people to build custom when at least one of these is true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Geometry or rules the SaaS cannot express.&lt;/strong&gt; Operable wall panels with track-and-stacking logic, cables that need real catenary curves, assemblies where option A changes what is valid for option B. The moment the configurator needs to understand your product, templates run out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing and lead time live in your systems.&lt;/strong&gt; If the real price comes from your ERP and the real lead time comes from your production queue, you are going to integrate anyway. At that point the SaaS is a skin you are fighting, not a shortcut.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep integration with your stack.&lt;/strong&gt; WooCommerce, a custom CRM, a Laravel back-office, an ERP. Custom means the configuration flows straight into an order and a work ticket, not a CSV you re-key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to own the data and the UX.&lt;/strong&gt; No per-view pricing surprise, no vendor roadmap deciding your features, your brand end to end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The SaaS fee crosses over.&lt;/strong&gt; Run the math. A four-figure monthly fee for a few years funds a custom build that then costs you only hosting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest crossover
&lt;/h2&gt;

&lt;p&gt;A SaaS configurator is cheaper and faster right up until your product or your process stops fitting the template. Then every workaround costs more than the subscription saved. Custom is the opposite: more upfront, then it bends to whatever you actually sell.&lt;/p&gt;

&lt;p&gt;My rule of thumb: &lt;strong&gt;if the configurator only has to show the product, buy it. If it has to understand the product, build it.&lt;/strong&gt; The understanding part - rules, pricing, lead time, integration - is the &lt;a href="https://grodev.pl/konfigurator-produktowy-3d" rel="noopener noreferrer"&gt;part I work on&lt;/a&gt;, and it is also the part no template ships with.&lt;/p&gt;

&lt;p&gt;If you want to see what custom looks like running in production, there are a few live on subdomains of &lt;a href="https://grodev.pl" rel="noopener noreferrer"&gt;grodev.pl&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I am Dominik Groński / GroDev, a studio in Poznan, Poland building custom 3D configurators and Laravel back-ends for manufacturers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webgl</category>
      <category>ecommerce</category>
      <category>webdev</category>
    </item>
    <item>
      <title>InstancedMesh in Three.js - one draw call for the parts you repeat</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Wed, 21 Oct 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/instancedmesh-in-threejs-one-draw-call-for-the-parts-you-repeat-1g5g</link>
      <guid>https://dev.to/gronskideveloper/instancedmesh-in-threejs-one-draw-call-for-the-parts-you-repeat-1g5g</guid>
      <description>&lt;p&gt;A product configurator is full of repeated geometry. A pergola has 40 louvres. A fence has 60 slats. A pool deck has a grid of tiles. The straightforward way to build that, one &lt;code&gt;Mesh&lt;/code&gt; per part, is also how your frame rate dies on a mid-range phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;louvre&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Mesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;geometry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;louvre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;
  &lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;louvre&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;40 meshes is 40 draw calls, every single frame. Add a tilt slider the user drags and now you are touching 40 objects per frame on top of that. My first pergola configurator sat at 12 FPS on a mid-range Android doing exactly this.&lt;/p&gt;

&lt;h2&gt;
  
  
  InstancedMesh: one geometry, N transforms, one draw call
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;geo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BoxGeometry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MeshStandardMaterial&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;louvres&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InstancedMesh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dummy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object3D&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userTilt&lt;/span&gt;
  &lt;span class="nx"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMatrix&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;louvres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMatrixAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;louvres&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instanceMatrix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;needsUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;louvres&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same 40 louvres, one draw call. The pergola went from 12 FPS to a locked 60.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three gotchas that cost me time
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dummy.updateMatrix()&lt;/code&gt; before &lt;code&gt;setMatrixAt&lt;/code&gt;.&lt;/strong&gt; Forget it and every instance renders at the origin, stacked on top of each other. There is no error, just a mess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;instanceMatrix.needsUpdate = true&lt;/code&gt; after you change transforms.&lt;/strong&gt; Without it the GPU never sees the update. This is the one that makes people decide InstancedMesh is broken and go back to loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-instance color is &lt;code&gt;setColorAt(i, color)&lt;/code&gt; plus &lt;code&gt;instanceColor.needsUpdate = true&lt;/code&gt;.&lt;/strong&gt; Not &lt;code&gt;material.color&lt;/code&gt;. If your user picks a color per slat, this is how you do it: one material, N colors, still one draw call.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When it is worth it
&lt;/h2&gt;

&lt;p&gt;Past roughly 100 to 200 repeated objects, or for any repeated object you update every frame. Below that, plain meshes are fine and the code is simpler. Do not instance three cubes to feel clever.&lt;/p&gt;

&lt;p&gt;The louvres on &lt;a href="https://pergola3d.grodev.pl" rel="noopener noreferrer"&gt;pergola3d.grodev.pl&lt;/a&gt; run exactly like this. It is one of several things that only show up once a Three.js demo meets a real product catalog, which I wrote more about in &lt;a href="https://dev.to/gronskideveloper/building-a-3d-product-configurator-in-threejs-lessons-from-9-client-deployments-3c90"&gt;lessons from 9 configurator deployments&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I am Dominik Groński / &lt;a href="https://grodev.pl" rel="noopener noreferrer"&gt;GroDev&lt;/a&gt;, a studio in Poznan, Poland building custom 3D configurators and Laravel back-ends for manufacturers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webgl</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Turning a 3D configurator into a real quote - price and lead time, not just a pretty model</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Sat, 17 Oct 2026 19:53:31 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/turning-a-3d-configurator-into-a-real-quote-price-and-lead-time-not-just-a-pretty-model-3pe0</link>
      <guid>https://dev.to/gronskideveloper/turning-a-3d-configurator-into-a-real-quote-price-and-lead-time-not-just-a-pretty-model-3pe0</guid>
      <description>&lt;p&gt;Most Three.js configurator demos stop at the visual. The customer rotates a nice model, picks a color, and that is the whole show. But the manufacturers I build for do not sell models. They sell a product with a price and a delivery date.&lt;/p&gt;

&lt;p&gt;The question that closes a sale is not "does it look good". It is "what does THIS configuration cost, and when do I get it". That second half is the part nobody puts in a demo. Here is how I build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The configuration is the only thing that matters
&lt;/h2&gt;

&lt;p&gt;The whole system hangs off one structured object: the config. Not the price, not the Three.js scene state. Just the choices the customer made.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;horizon&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width_mm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height_mm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;panels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;glass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;frosted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anodized-black&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;soundproofing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;acoustic-42db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;track&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top-hung&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else is derived from this, server-side. The browser owns the preview. The server owns the truth. I wrote about why a client-side price is a 5-second DevTools hack in &lt;a href="https://dev.to/gronskideveloper/server-side-pricing-in-threejs-configurators-why-client-side-gets-devtools-hacked-3jjh"&gt;a separate post&lt;/a&gt;. The exact same principle applies to lead time: never let the page be the source of a number the customer acts on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Price is a server-side function of config
&lt;/h2&gt;

&lt;p&gt;Price is the easy half. Given a raw config, the backend recalculates from scratch and ignores whatever the frontend thinks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;calculateFinalPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;$price&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BASE_PRICE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'product'&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
  &lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;GLASS_PRICING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'glass'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'width_mm'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'height_mm'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PER_SQ_METER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SOUNDPROOFING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'soundproofing'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend still shows a live estimate as the customer drags sliders, because that is good UX. It is just labeled an estimate, and it is never what gets charged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lead time is pricing's harder cousin
&lt;/h2&gt;

&lt;p&gt;Price is mostly a lookup plus arithmetic. Lead time depends on moving parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;base production time per product line&lt;/li&gt;
&lt;li&gt;option surcharges (custom glass adds days, acoustic seals add more)&lt;/li&gt;
&lt;li&gt;stock components vs made-to-order&lt;/li&gt;
&lt;li&gt;the factory's current backlog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Model it as rules, server-side, the same way as price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;leadTimeDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Capacity&lt;/span&gt; &lt;span class="nv"&gt;$cap&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;$days&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BASE_LEAD&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'product'&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'glass'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'custom'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="nv"&gt;$days&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str_starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'soundproofing'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'acoustic'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nv"&gt;$days&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// queue pressure from live capacity, not a hardcoded guess&lt;/span&gt;
  &lt;span class="nv"&gt;$days&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$cap&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;currentBacklogDays&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$days&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;currentBacklogDays()&lt;/code&gt; is what makes the number real. A hardcoded "4 to 6 weeks" becomes a lie the moment the factory is busy. If the manufacturer can feed you a backlog figure, even a weekly spreadsheet export or a single field in an admin panel, the lead time stops being marketing and becomes a commitment the customer can plan around.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output is a quote, not a number
&lt;/h2&gt;

&lt;p&gt;The customer configured something real. Give them something to act on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the price (server-calculated)&lt;/li&gt;
&lt;li&gt;the lead time (server-calculated)&lt;/li&gt;
&lt;li&gt;a snapshot of their exact configuration: the 3D view rendered to an image, plus the spec list&lt;/li&gt;
&lt;li&gt;a reference id, so sales and the customer talk about the same thing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a quote. Generate it as a PDF or an order record, email it, and the configurator just did a salesperson's first hour of work before anyone picked up the phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is the 80% nobody demos
&lt;/h2&gt;

&lt;p&gt;The visual is 2 to 3 weeks of work. The quote engine (pricing rules, lead-time rules, a capacity input, PDF or order generation, and an admin UI for the manufacturer to maintain all of it) is the larger half. It is also the half that turns a toy into a tool. When I quote a configurator project, I price the engine, not the screens.&lt;/p&gt;

&lt;p&gt;Live configurators if you want to see the visual foundation this sits on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pergola3d.grodev.pl" rel="noopener noreferrer"&gt;pergola3d.grodev.pl&lt;/a&gt; - bioclimatic pergola, instanced louvres&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://basen3d.grodev.pl" rel="noopener noreferrer"&gt;basen3d.grodev.pl&lt;/a&gt; - pool configurator with Draco and PWA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I am Dominik Groński / &lt;a href="https://grodev.pl" rel="noopener noreferrer"&gt;GroDev&lt;/a&gt;, a studio in Poznan, Poland building custom 3D configurators and Laravel back-ends for manufacturers. If you are putting a price and a lead time behind a configurator, that back half is the part I work on.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webgl</category>
      <category>javascript</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Why your three.js scene gets a bad PageSpeed score (and it is not JavaScript)</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Mon, 28 Sep 2026 07:51:18 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/why-your-threejs-scene-gets-a-bad-pagespeed-score-and-it-is-not-javascript-gpj</link>
      <guid>https://dev.to/gronskideveloper/why-your-threejs-scene-gets-a-bad-pagespeed-score-and-it-is-not-javascript-gpj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://grodev.pl/blog/three-js-scena-niski-wynik-pagespeed" rel="noopener noreferrer"&gt;grodev.pl&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your 3D scene runs smoothly on your machine. Sixty frames, no jank. You drop the page into PageSpeed Insights and get a 48. Total Blocking Time is red, and the main-thread breakdown shows one long task blamed on &lt;code&gt;gsap.min.js&lt;/code&gt;. The first instinct is "too much JavaScript" or "too many particles". Both are almost always wrong, and chasing them can eat a whole day.&lt;/p&gt;

&lt;p&gt;This post is the diagnosis, not the recipe. I will show you why a heavy three.js scene scores badly in PageSpeed and where the cost really sits.&lt;/p&gt;

&lt;h2&gt;
  
  
  PageSpeed has no GPU
&lt;/h2&gt;

&lt;p&gt;This is the key that changes everything else, so start here. Google does not measure your page on a graphics card. Lighthouse and PageSpeed run headless Chrome where WebGL goes through software rendering on the CPU (SwiftShader). Your scene, which takes a fraction of a millisecond on your GPU, runs on the CPU on Google's machine.&lt;/p&gt;

&lt;p&gt;You can confirm it with one read of the renderer name. If &lt;code&gt;WEBGL_debug_renderer_info&lt;/code&gt; returns &lt;code&gt;swiftshader&lt;/code&gt;, &lt;code&gt;llvmpipe&lt;/code&gt;, &lt;code&gt;softpipe&lt;/code&gt; or &lt;code&gt;software&lt;/code&gt;, you are in that mode. Real users on virtual machines and weak hardware get the same. As long as you measure on your own card, you are not seeing what Google sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is not JavaScript
&lt;/h2&gt;

&lt;p&gt;In the main-thread breakdown you look at categories: "Script Evaluation", "Style &amp;amp; Layout", "Other". With a heavy WebGL scene, &lt;strong&gt;"Other" can be bigger than "Script Evaluation"&lt;/strong&gt;. That is not a bucket to ignore. Other is usually the GPU work and pixel scans, which a script profile does not show at all.&lt;/p&gt;

&lt;p&gt;Then there is the misleading label. A long task tagged &lt;code&gt;gsap.min.js&lt;/code&gt; or &lt;code&gt;requestAnimationFrame&lt;/code&gt; is usually not GSAP. In one frame, every &lt;code&gt;requestAnimationFrame&lt;/code&gt; call lands in a single task that takes the label of whoever started it. The GSAP ticker registers first, so it takes the blame for someone else's work. Optimizing GSAP here is aiming at the wrong target.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first frame is the most expensive frame
&lt;/h2&gt;

&lt;p&gt;A scene usually draws one frame on start and then waits for the user to move. Google's measurement does not move the mouse, so it sees exactly that one frame. And that one frame compiles every shader program at once.&lt;/p&gt;

&lt;p&gt;On one of my production builds the scene had fourteen programs: particles, arcs, several bloom layers, composite, output. Each compiled in the first frame, and after every compile the library asked the card for the result. Every such query waits until the card has worked through its whole command queue. The result: &lt;strong&gt;one 656 ms task on Google's machine and a score of 73&lt;/strong&gt; on desktop. Total Blocking Time sat around 700 ms. None of it was JavaScript in the sense most guides mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cutting particles does not help, and it ruins mobile
&lt;/h2&gt;

&lt;p&gt;The most common reflex is to lower the particle count "for performance". That misreads the problem on two levels.&lt;/p&gt;

&lt;p&gt;First, particles are cheap. A point is one vertex. What is expensive is fill: bloom, transparency, overdraw, not the number of points. Cut the particles and you cut the cheap resource while leaving the expensive one untouched.&lt;/p&gt;

&lt;p&gt;Second, an image made of particles has a resolution equal to their count. Text built from ten thousand particles looks different from the same text built from seventy thousand. When you give the phone ten times fewer particles "to make it lighter", the text goes blurry or sparse, and you still have not touched the real cost. On one build I raised the phone particle count from seven thousand to seventy-two thousand and the text finally looked sharp, while the score did not drop, because the problem was never the point count.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to measure without chasing noise
&lt;/h2&gt;

&lt;p&gt;Before you fix anything, you need a measurement that does not lie. Here are the three traps almost everyone falls into.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you do&lt;/th&gt;
&lt;th&gt;Why it lies&lt;/th&gt;
&lt;th&gt;What to do instead&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Profile in DevTools on your own GPU&lt;/td&gt;
&lt;td&gt;The second load has the programs cached on disk, the compilation disappears&lt;/td&gt;
&lt;td&gt;A fresh profile with no cache, software rendering (&lt;code&gt;--disable-gpu&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A single PageSpeed run&lt;/td&gt;
&lt;td&gt;Noise of 7 to 10 points, the same file gives 80 and 95&lt;/td&gt;
&lt;td&gt;Median of three, a 7-point significance threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Looking at categories instead of tasks&lt;/td&gt;
&lt;td&gt;"Other" tells you nothing about the cause&lt;/td&gt;
&lt;td&gt;Read the list of long tasks with their start times&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule is simple. Measure in Google's conditions, not on your card. Read specific tasks, not categories. Treat a change smaller than seven points as chance, not success. Only with a measurement like that does it make sense to touch the scene.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about it
&lt;/h2&gt;

&lt;p&gt;The fix exists and it is repeatable. In short: the first frame must not compile programs, so you compile them ahead of time and warm them up in the background, in the right render conditions. Pixel loops and scene building cannot sit in one task at &lt;code&gt;load&lt;/code&gt;. The phone gets the same particle count, only the shots without text are lighter. On the same build where I started at 73, I ended at &lt;strong&gt;97 on desktop, with Total Blocking Time cut from 700 to 140 ms and the first frame from 656 ms to a few milliseconds&lt;/strong&gt;, with no change to how the scene looks.&lt;/p&gt;

&lt;p&gt;I packaged the full method, with copy-ready code, a measurement protocol and a PageSpeed script, as a &lt;a href="https://grodev.pl/skills" rel="noopener noreferrer"&gt;ready-made skill for developers and coding agents&lt;/a&gt;. If you would rather hand the work to me, &lt;a href="https://grodev.pl/skills" rel="noopener noreferrer"&gt;send the scene URL&lt;/a&gt; and I will tell you plainly where the cost sits and how much I will take off.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does a bad PageSpeed score on a WebGL scene hurt my Google ranking?&lt;/strong&gt;&lt;br&gt;
Google ranks mostly on field data from real users (Core Web Vitals: LCP, CLS, INP), not on the lab score. A &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; is not an LCP candidate, so a WebGL scene does not hurt LCP. It does hurt Total Blocking Time, a lab metric, and real INP if the scene blocks the thread during interaction. Worth fixing for the user and the score, without panic that one red lab number sinks the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is the long task labelled GSAP when I barely use GSAP?&lt;/strong&gt;&lt;br&gt;
Because a long task label points to the script that started it, not the one that did the work. All &lt;code&gt;requestAnimationFrame&lt;/code&gt; calls in a frame go in one task and take the name of the first. The GSAP ticker registers early, so it takes the blame. The real weight is usually shader compilation in the first frame. Do not optimize GSAP on that basis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I just move the scene to a Web Worker or OffscreenCanvas?&lt;/strong&gt;&lt;br&gt;
Usually not, and usually not as the first move. A Web Worker is a day of work, while the problem is most often solved by task ordering and compiling programs ahead of time, a change of a few hours. Reach for a Worker when you genuinely have heavy CPU work in the render loop, not to hide shader compilation from the measurement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it be fixed without changing how the scene looks?&lt;/strong&gt;&lt;br&gt;
Yes, and that is the whole point. The method keeps the desktop image untouched to the percent, and the phone just as sharp, only smaller. It is about order and the way things are drawn, when programs compile and what sits in the first frame, not about cutting graphics.&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>webgl</category>
      <category>performance</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Server-side pricing in Three.js configurators - why client-side gets DevTools-hacked</title>
      <dc:creator>Dominik</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:52:51 +0000</pubDate>
      <link>https://dev.to/gronskideveloper/server-side-pricing-in-threejs-configurators-why-client-side-gets-devtools-hacked-3jjh</link>
      <guid>https://dev.to/gronskideveloper/server-side-pricing-in-threejs-configurators-why-client-side-gets-devtools-hacked-3jjh</guid>
      <description>&lt;h2&gt;
  
  
  The setup that looks fine
&lt;/h2&gt;

&lt;p&gt;You build a 3D product configurator in Three.js. User picks material, size, options - and next to the model, a live price updates as they configure. The frontend calculates it because that is fast, feels responsive, and does not need a network trip.&lt;/p&gt;

&lt;p&gt;The code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculatePrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;BASE_PRICE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;MATERIAL_PRICING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;PER_SQ_METER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Live update on any config change&lt;/span&gt;
&lt;span class="nx"&gt;someSlider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculatePrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;updatePriceDisplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// On order submission, send the whole state&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what most tutorials show. It is also what I shipped in my first configurator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious problem I missed
&lt;/h2&gt;

&lt;p&gt;A user opens DevTools. &lt;code&gt;window.state.price = 1&lt;/code&gt;. Clicks 'Buy'. Order goes to my backend with price = 1. Backend has no independent pricing logic - it trusts what the frontend sends.&lt;/p&gt;

&lt;p&gt;This is not paranoia. This is a 5-second exploit that anyone with basic browser knowledge can do. I caught it because a friend I asked to test the configurator did exactly this within 20 seconds of opening the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that is actually secure
&lt;/h2&gt;

&lt;p&gt;Separate two responsibilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: preview the price. This is UX, not truth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: calculate the final price from raw config, independently. This is truth.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Frontend - preview only&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;previewPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Same math as before, but purely for display&lt;/span&gt;
  &lt;span class="c1"&gt;// Explicitly labeled 'estimate' in UI&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// No price sent&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Backend - authoritative&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;calculateFinalPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BASE_PRICE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MATERIAL_PRICING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'material'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\InvalidArgumentException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nv"&gt;$price&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'width'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'height'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PER_SQ_METER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frontend never sends price. Backend never trusts frontend price. If someone hacks the DevTools price display, they only change what they see - the order still calculates correctly server-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 'but performance' objection
&lt;/h2&gt;

&lt;p&gt;One network trip per configuration change would kill UX. So do not do that. Frontend preview stays instant (client-side math), but is labeled 'preview' or 'estimate'. Backend calculation happens ONCE, at order submission. User never waits during configuration.&lt;/p&gt;

&lt;p&gt;Add a 'Confirm order' step where the real price is fetched from backend and displayed for confirmation - so there is no surprise between what they saw and what they pay. This is the pattern Bagisto, Shopify custom apps and Zakeke all use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha with variant validation
&lt;/h2&gt;

&lt;p&gt;Same principle applies to configuration validity. Frontend might allow user to set impossible combinations (glass thickness 20mm with hinge type 'lightweight'). Backend must validate independently and reject.&lt;/p&gt;

&lt;p&gt;I keep validation logic in a shared JSON schema (JSON Schema draft-07) that both frontend and backend read. Frontend uses it for real-time feedback. Backend uses it as the final gate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"if"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"thickness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"then"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hinge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"heavy-duty"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One source of truth. Both sides read it. Backend is authoritative on submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference implementation
&lt;/h2&gt;

&lt;p&gt;My starter &lt;a href="https://github.com/GronskiDeveloper/threejs-product-configurator-starter" rel="noopener noreferrer"&gt;threejs-product-configurator-starter&lt;/a&gt; has the frontend preview pattern wired in. Backend calculator is not in the starter (it is deliberately frontend-only for education), but the client-side code is structured so that adding server-side pricing later is a one-file change.&lt;/p&gt;

&lt;p&gt;The pricing question is one of five I hit building configurators. The full list is in &lt;a href="https://4programmers.net/Forum/JavaScript/378090-konfigurator_3d_od_zera_w_threejs_5_pulapek_ktore_kosztowaly_mnie_tydzien" rel="noopener noreferrer"&gt;my 4programmers.net Polish-language thread&lt;/a&gt; - WebGL context leaks, material vs new material for color changes, pixel ratio caps for 4K, OrbitControls vs TrackballControls for touch. Different post, same theme: things Three.js tutorials skip.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I am Dominik Groński / &lt;a href="https://grodev.pl" rel="noopener noreferrer"&gt;GroDev&lt;/a&gt; - a studio in Poznań, Poland, building custom 3D configurators and Laravel panels for manufacturers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>threejs</category>
      <category>javascript</category>
      <category>security</category>
      <category>ecommerce</category>
    </item>
  </channel>
</rss>
