WebGL/Three.js CAD Rendering Optimization

The engineering requirements for rendering high-fidelity Comere puter-Aided Design (CAD) models on integrated Graphics Processing Unit (iGPU) hardware—specifically Intel UHD and AMD Radeon Vega series—represent one of the most significant challenges in modern browser-based visualization. Unlike discrete graphics cards with dedicated high-speed VRAM, iGPUs operate under a Unified Memory Architecture (UMA), where the graphics processor shares system RAM and memory bandwidth with the Central Processing Unit (CPU).1 On standard professional laptops equipped with 8GB of RAM, this contention for resources creates a performance ceiling that traditional rendering techniques frequently fail to navigate. Achieving a consistent 60 frames per second (FPS) with assemblies exceeding one million triangles requires a meticulously orchestrated pipeline that addresses geometry consolidation, visibility determination, state management, and memory stability within the Electron runtime environment.1

Architectural Foundations of Integrated GPU Rendering

The performance of integrated GPUs like the Intel UHD 630 or AMD Radeon Vega 8 is primarily constrained by memory bandwidth rather than raw compute throughput. In a UMA environment, the CPU and GPU compete for access to the DDR4 or LPDDR4x memory bus. Large CAD models, which can easily reach hundreds of megabytes of vertex data, can saturate this bus, leading to “stuttering” as the GPU waits for data to be streamed from the shared system memory.1 For a 1M+ triangle mesh, the raw vertex data, including positions and normals, can exceed 120MB.

Assuming 32-bit floats for positions (12 bytes) and normals (12 bytes), a 1M triangle assembly requires approximately 72MB just for vertex attributes, not accounting for indices or auxiliary data.4 When this data is transferred between the CPU and GPU through WebGL draw calls, the overhead of the browser’s abstraction layer and the underlying graphics driver becomes the primary bottleneck.3

Hardware Profile Memory Architecture Typical Bottleneck Target CAD Complexity
Intel UHD 630 UMA (Shared System RAM) Memory Bandwidth / ROPs 1M – 2M Triangles
AMD Radeon Vega 8 UMA (Shared System RAM) Memory Bandwidth / Compute 1.5M – 3M Triangles
Discrete (RTX 3060) Dedicated VRAM (GDDR6) Geometry Throughput 10M+ Triangles

Geometry Architectures for Multi-Body CAD Assemblies

CAD models are typically authored as multi-body assemblies, where a single file (such as a STEP or STL export) contains thousands of discrete mechanical components. In Three.js, representing each component as a unique THREE.Mesh object is the most common architectural error. Every individual mesh object results in a separate draw call, which forces the CPU to prepare and send a command to the GPU. For 1,000 components, this results in 1,000 draw calls per frame, which at 60 FPS equates to 60,000 calls per second—a volume that overwhelms the CPU’s command preparation capacity long before the GPU’s rendering limit is reached.3

Instancing and Batched Rendering Strategies

For assemblies containing repetitive parts (e.g., bolts, rivets, standard brackets), THREE.InstancedMesh is the optimal primitive. Instancing allows the GPU to render N copies of the same geometry in a single draw call, where N is limited only by the maximum size of the attribute buffers.5 However, CAD assemblies are often heterogeneous, consisting of thousands of unique geometries. For these cases, THREE.BatchedMesh (available in r156+) is the superior solution.8

BatchedMesh allocates a single, large set of vertex and index buffers on the GPU and packs multiple different geometries into them. This allows the renderer to draw an entire assembly—or large sections of it—with a single render call, while still allowing individual control over the transformation matrix, visibility, and color of each constituent body.9 This approach minimizes the “draw call overhead” by grouping instructions, which is critical for iGPUs where driver-level overhead for many small calls is disproportionately high.10

Buffer Merging and Transformation Precision

When dealing with static sections of an assembly that do not require independent movement, BufferGeometryUtils.mergeBufferGeometries can be used to fuse multiple geometries into a single BufferGeometry object.5 While this provides the ultimate reduction in draw calls, it complicates frustum culling, as the entire merged object is treated as a single bounding volume. A more nuanced “tiled merging” strategy—where geometries are merged based on their spatial proximity—balances draw call reduction with the ability of the engine to cull sections that are not currently visible.5

Precision management is another architectural pillar for CAD visualization. CAD data is often authored in large coordinate systems ( to units). Because WebGL and GPU shaders typically operate on 32-bit floating-point numbers, these large values suffer from “precision collapse”.13 A 32-bit float has a 23-bit mantissa, meaning that at a coordinate of , the smallest representable increment is 64 units.13

The result is “vertex snapping” or “jitter” where triangles appear to vibrate as the camera moves. To mitigate this without the overhead of 64-bit emulation, a re-centering shift must be performed before the geometry is uploaded to the GPU buffers.13 The bounding box of the entire assembly is calculated in 64-bit space (JavaScript’s Number type), a basePoint is established (usually the center of the box), and all vertex coordinates are offset by this basePoint.13

Feature InstancedMesh BatchedMesh mergeBufferGeometries
Geometry Type Single Shared Multiple Different Monolithic Single
Transformation Per-instance Matrix Per-instance Matrix Baked at Merge
Draw Calls 1 per Mesh Type 1 per Batch 1 per Merged Mesh
Best Use Case Repeated fasteners Heterogeneous parts Static sub-assemblies

Advanced Visibility Determination and Culling

For dense CAD assemblies, rendering everything the camera is pointing at is often wasteful, as many parts are hidden behind other opaque surfaces. Visibility determination aims to remove these unnecessary calculations from the GPU’s pipeline.

Frustum and Spatial Partitioning

Three.js performs frustum culling by default, which skips the rendering of objects whose bounding spheres are entirely outside the camera’s field of view.5 However, the efficiency of this process is tied to the granularity of the scene graph. If an assembly is merged into a single mesh, the entire mesh is rendered if any part of its bounding box touches the frustum.6

For massive assemblies, a hierarchical spatial index, such as an Octree or a Bounding Volume Hierarchy (BVH), should be implemented on the CPU. The scene is partitioned into spatial nodes; the renderer first tests the frustum against these high-level nodes and only traverses down to the individual components if the node is visible.14 This dramatically reduces the number of intersection tests and bounding sphere calculations performed by the renderer every frame.

Occlusion Culling and the Depth Pre-Pass

Occlusion culling—the removal of objects hidden by other objects—is difficult to perform on the CPU for complex geometry and expensive to perform on the GPU due to latency.15 On iGPUs, where fill-rate and fragment processing are limited, a “Depth Pre-Pass” strategy is often the most effective workaround.17

The Depth Pre-Pass involves two rendering stages:

  1. Stage 1 (Depth-Only): The entire scene is rendered with a very simple material (such as MeshBasicMaterial) and color writing disabled (colorWrite: false). This pass only populates the Z-buffer with the closest fragment depths.17
  2. Stage 2 (Main Render): The scene is rendered again with full shading. The depth test function is set to EQUAL. Because the Z-buffer already contains the depths of only the visible surfaces, the GPU automatically discards occluded fragments before they enter the expensive main shader logic.17

Implementing this manual Z-prepass can reduce the fragment shader load by up to 30%, which is particularly valuable for CAD models that often feature significant “overdraw” (many layers of hidden internal geometry).17

Level of Detail (LOD) for Non-Textured Meshes

To maintain 60 FPS with 1M+ triangles on low-end hardware, the triangle count must be reduced as objects recede from the view. Standard LOD systems in Three.js use discrete swaps between high-, medium-, and low-poly versions of a mesh.18 However, in a CAD context where users often perform slow, precise inspections, the “popping” effect of discrete LOD swaps is unacceptable.19

Continuous LOD and Mesh Simplification

For triangle meshes without UV or texture data, smooth LOD transitions can be achieved through “Virtual Geometry” or shader-based dithering.19 A continuous LOD system dynamically simplifies the mesh on the fly, dropping triangles that are smaller than a single pixel at the current distance.20

If building a custom virtual geometry stack is not feasible, a “Dithered LOD Blend” is a high-performance alternative.21 In this pattern, the renderer draws both the current LOD level and the next-lower LOD level during the transition window. A fragment shader uses a screen-space dither pattern (e.g., a Bayer matrix) to interleave the pixels of the two versions. As the distance increases, the dither pattern gradually favors the lower-poly version until the high-poly version is no longer drawn.21 This eliminates the sudden “pop” without requiring alpha transparency, which would break the Z-buffer and increase fragment costs on the iGPU.22

Geometry Attributes for CAD Edge Rendering

CAD models typically require “Edge” or “Wireframe” rendering to distinguish between parts and highlight sharp transitions. THREE.EdgesGeometry creates a secondary set of line segments, which effectively doubles the draw calls and the vertex count.23 On iGPUs, this is often the breaking point for performance.

A more efficient method is to compute the edges procedurally in the fragment shader using the “Barycentric Coordinate” technique.24 By adding a 3-component attribute to each vertex representing its position relative to the triangle (e.g., (1,0,0), (0,1,0), and (0,0,1)), the fragment shader can calculate the distance to the nearest edge:

 

OpenGL Shading Language

 

float edgeFactor() {
    vec3 d = fwidth(vBarycentric);
    vec3 a3 = smoothstep(vec3(0.0), d * uEdgeWidth, vBarycentric);
    return min(min(a3.x, a3.y), a3.z);
}

This single-pass shader renders both the shaded surface and the sharp edges, preserving the 60 FPS target while maintaining high visual clarity.24

WebGL State Management and Draw Call Reduction

The interaction between the CPU and GPU is governed by the WebGL state machine. Every time a property changes—such as switching a material, binding a new texture, or updating a uniform—the CPU must synchronize with the GPU, introducing a small delay.3

Minimizing Material Switches and Uniform Updates

In a dense CAD assembly, hundreds of parts may share identical physical properties but have different diffuse colors. In a naive implementation, each color would require a separate material, leading to shader switches. To avoid this, a “Material Atlas” should be used.3

The diffuse colors of all parts are packed into a small (e.g., 64×64 or 256×256) data texture. Each geometry in the BatchedMesh or InstancedMesh is assigned a “part ID” attribute.3 The fragment shader uses this ID to look up the correct color from the texture atlas. This allows an entire assembly with thousands of different colored parts to be rendered with a single material and a single draw call.3

State Change Performance Cost (iGPU) Optimization
Shader Switch High Use one material + Atlas Texture
Texture Bind Medium Pack data into larger textures
Uniform Update Low-Medium Use Uniform Buffer Objects (UBOs)
Vertex Buffer Bind Medium Use BatchedMesh

Memory Optimization and Stability in Electron Apps

Large-scale CAD applications in Electron face a double-edged sword: they have access to the full system memory but must manage it across isolated processes. 8GB of RAM is a tight constraint when one million triangles are being processed, decoded, and rendered simultaneously.2

SharedArrayBuffer and Worker-Based Decoding

Loading a million-triangle STL file can freeze the browser’s main thread for several seconds during parsing. To maintain responsiveness, the parsing logic should be moved to a Web Worker.28 However, traditional message passing between workers and the main thread involves “Structured Cloning,” which creates a full copy of the data, potentially doubling the memory usage and triggering the Electron OOM (Out of Memory) killer.28

The solution is SharedArrayBuffer.28 The Web Worker parses the STL data directly into a SharedArrayBuffer, and the main thread creates a Three.js BufferAttribute that points directly to this shared memory space. This “zero-copy” architecture allows for multi-threaded geometry generation without the memory overhead of data duplication.28

Preventing GPU Memory Leaks

Electron apps are notorious for GPU memory leaks because the Chromium GPU process is separate from the renderer process.27 When a CAD model is “deleted” in Three.js, simply removing it from the scene graph does not free the associated GPU buffers.31

A strict disposal pattern is mandatory:

  1. Scene Traversal: Use scene.traverse() to find all meshes.31
  2. Explicit Disposal: Call .dispose() on every geometry, material, and texture found.31
  3. Render List Cleanup: Call renderer.renderLists.dispose() to clear internal caches.31
  4. TypedArray Reuse: Use a buffer pool for the SharedArrayBuffers so that memory is recycled rather than re-allocated for every new model loaded.28

Render-on-Demand vs. Continuous Rendering

By default, Three.js uses a requestAnimationFrame loop that renders 60 times per second, regardless of whether the scene has changed.34 For a laptop on battery with an iGPU, this is inefficient and leads to thermal throttling, which eventually drops the performance.1

CAD models are often static for extended periods while the user reviews data. A “Render-on-Demand” system should be implemented where the render loop is paused when no changes occur.5

  • Change Detection: Invalidate the frame only when user input (camera rotation, mouse click) is detected or when a background process (model loading) completes.35
  • Invalidation Flag: Use a simple needsUpdate boolean. When an event occurs, set needsUpdate = true and trigger one frame of the loop.35

This approach preserves CPU and GPU cycles, keeping the system cool and responsive for when the user starts a complex interaction.14

Shading Model Comparison for Integrated Hardware

The choice of material shading model has a direct impact on the fragment shader’s complexity and the overall frame budget.

MeshStandardMaterial vs. MeshPhongMaterial

MeshStandardMaterial is a Physically Based Rendering (PBR) model that uses metallic-roughness workflows. It is the modern standard for realism but is the most computationally expensive material in Three.js, requiring complex calculations for energy conservation and Fresnel reflections.37 On Intel UHD GPUs, rendering 1M+ triangles with PBR can saturate the fragment processors, dropping the FPS to 30 or lower.37

MeshPhongMaterial uses the non-physically based Blinn-Phong model. It provides per-fragment specular highlights that are often sufficient for mechanical CAD visualization (giving parts a “shiny plastic” or “brushed metal” look) but with significantly higher performance on low-end hardware.37

Material Lighting Model Complexity Best for iGPU
MeshBasicMaterial Unlit Very Low Backgrounds/Debugging
MeshLambertMaterial Diffuse only Low Matte CAD parts
MeshPhongMaterial Blinn-Phong Medium High-perf CAD viewing
MeshStandardMaterial PBR High High-end workstations

For the ultimate performance on Intel UHD, a custom ShaderMaterial implementing a “Flat Shaded + Edge” model is recommended. By ignoring standard light-source calculations and using a pre-calculated lighting term based on the face normal, the fragment shader cost is minimized, allowing the hardware to focus on geometry density.25

Conclusion: A Multi-Layered Performance Strategy

Successfully rendering 1M+ triangle CAD models at 60 FPS on integrated GPUs requires moving away from general-purpose 3D rendering patterns and toward a specialized CAD architecture. The system must prioritize the reduction of draw calls through BatchedMesh and atlas textures, manage large coordinate precision via origin-shifting, and utilize worker-based SharedArrayBuffer workflows for memory stability.9 By combining these structural optimizations with intelligent visibility determination (Depth Pre-Pass) and a Render-on-Demand execution model, developers can provide a workstation-grade visualization experience on standard mobile hardware.1

Works cited

  1. Rendering 600 units in the browser with Three.js what broke and what actually helped : r/webdev – Reddit, accessed April 16, 2026, https://www.reddit.com/r/webdev/comments/1rnhr5j/rendering_600_units_in_the_browser_with_threejs/
  2. What’s the minimum requirement on GPU for using Three.js in a goo way? : r/threejs – Reddit, accessed April 16, 2026, https://www.reddit.com/r/threejs/comments/1134a2p/whats_the_minimum_requirement_on_gpu_for_using/
  3. Draw Calls: The Silent Killer | Three.js Roadmap, accessed April 16, 2026, https://threejsroadmap.com/blog/draw-calls-the-silent-killer
  4. BufferGeometry – three.js docs, accessed April 16, 2026, https://threejs.org/docs/pages/BufferGeometry.html
  5. 100 Three.js Tips That Actually Improve Performance (2026) – Utsubo, accessed April 16, 2026, https://www.utsubo.com/blog/threejs-best-practices-100-tips
  6. InstancedBufferGeometry or BufferGeometry or InstancedMesh or MeshSurfaceSampler. What are main differences among them? What is the most performant? : r/threejs – Reddit, accessed April 16, 2026, https://www.reddit.com/r/threejs/comments/1g7sh7g/instancedbuffergeometry_or_buffergeometry_or/
  7. BufferGeometry.merge or BufferGeometryUtils.mergeBufferGeometries: Add support for transformation matrix · Issue #18918 · mrdoob/three.js – GitHub, accessed April 16, 2026, https://github.com/mrdoob/three.js/issues/18918
  8. Render big model – Questions – three.js forum, accessed April 16, 2026, https://discourse.threejs.org/t/render-big-model/79086
  9. BatchedMesh – three.js docs, accessed April 16, 2026, https://threejs.org/docs/pages/BatchedMesh.html
  10. BatchedMesh for High-Performance Rendering in Three.js – Wael …, accessed April 16, 2026, https://waelyasmina.net/articles/batchedmesh-for-high-performance-rendering-in-three-js/
  11. Why did the performance worsen significantly when I used _multiDrawInstances in BatchedMesh? – three.js forum, accessed April 16, 2026, https://discourse.threejs.org/t/why-did-the-performance-worsen-significantly-when-i-used-multidrawinstances-in-batchedmesh/78581
  12. How to crossFade two textures with shaders for morphTarget object animations? – Questions, accessed April 16, 2026, https://discourse.threejs.org/t/how-to-crossfade-two-textures-with-shaders-for-morphtarget-object-animations/75383
  13. Precision-Safe Rendering of Large-Coordinate CAD Drawings in …, accessed April 16, 2026, https://medium.com/@mlightcad/precision-safe-rendering-of-large-coordinate-cad-drawings-in-three-js-c49c299b3afc
  14. How can I optimise my THREE.JS rendering? – Questions, accessed April 16, 2026, https://discourse.threejs.org/t/how-can-i-optimise-my-three-js-rendering/42251
  15. Hierachical Z-Buffering for occlusion culling – Stack Overflow, accessed April 16, 2026, https://stackoverflow.com/questions/33984713/hierachical-z-buffering-for-occlusion-culling
  16. Hierarchical-Z map based occlusion culling – RasterGrid | Software Consultancy, accessed April 16, 2026, https://www.rastergrid.com/blog/2010/10/hierarchical-z-map-based-occlusion-culling/
  17. Speeding Up Three.JS with Depth-Based Fragment Culling – Casey …, accessed April 16, 2026, https://cprimozic.net/blog/depth-based-fragment-culling-webgl/
  18. LOD – three.js docs, accessed April 16, 2026, https://threejs.org/docs/pages/LOD.html
  19. Blend Geometry in between LOD – Questions – three.js forum, accessed April 16, 2026, https://discourse.threejs.org/t/blend-geometry-in-between-lod/87453
  20. Virtually Geometric – Showcase – three.js forum, accessed April 16, 2026, https://discourse.threejs.org/t/virtually-geometric/28420
  21. Building a Real-Time Dithering Shader – Codrops, accessed April 16, 2026, https://tympanus.net/codrops/2025/06/04/building-a-real-time-dithering-shader/
  22. Material.dithering – three.js docs, accessed April 16, 2026, https://threejs.org/docs/#api/en/materials/Material.dithering
  23. EdgesGeometry – three.js docs, accessed April 16, 2026, https://threejs.org/docs/pages/EdgesGeometry.html
  24. More efficient edge painting algorithm? (burnt toast) – Questions – three.js forum, accessed April 16, 2026, https://discourse.threejs.org/t/more-efficient-edge-painting-algorithm-burnt-toast/70460
  25. I just added shading features: smooth, flat, and auto in Three.js. We create shared-vertex geometry for smooth shading, duplicated-vertex geometry for flat shading, and hybrid-vertex geometry for auto shading from unified mesh data. : r/threejs – Reddit, accessed April 16, 2026, https://www.reddit.com/r/threejs/comments/1nf4jeu/i_just_added_shading_features_smooth_flat_and/
  26. Improving functionality and performance with Three.js custom shaders – Partful, accessed April 16, 2026, https://partful.io/blog/extending-three-js-using-custom-shaders
  27. Read-only shared buffer (`ArrayBuffer`) shared from main process to renderer process · Issue #45034 · electron/electron – GitHub, accessed April 16, 2026, https://github.com/electron/electron/issues/45034
  28. SharedArrayBuffer and Memory Management in JavaScript | by Artem Khrienov – Medium, accessed April 16, 2026, https://medium.com/@artemkhrenov/sharedarraybuffer-and-memory-management-in-javascript-06738cda8f51
  29. SharedArrayBuffer – JavaScript – MDN Web Docs, accessed April 16, 2026, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
  30. How to send SharedArrayBuffer from main process to Window processes #10409 – GitHub, accessed April 16, 2026, https://github.com/electron/electron/issues/10409
  31. Dispose things correctly in three.js – Questions, accessed April 16, 2026, https://discourse.threejs.org/t/dispose-things-correctly-in-three-js/6534
  32. Three.js: how to correctly dispose a scene in memory – Stack Overflow, accessed April 16, 2026, https://stackoverflow.com/questions/55131538/three-js-how-to-correctly-dispose-a-scene-in-memory
  33. Memory pool for RenderTargets to help with complex EffectComposer pipelines · Issue #10981 · mrdoob/three.js – GitHub, accessed April 16, 2026, https://github.com/mrdoob/three.js/issues/10981
  34. The Animation Loop | Discover three.js, accessed April 16, 2026, https://discoverthreejs.com/book/first-steps/animation-loop/
  35. Scaling performance – React Three Fiber, accessed April 16, 2026, https://r3f.docs.pmnd.rs/advanced/scaling-performance
  36. Rendering only when needed | React Three Fiber, accessed April 16, 2026, https://gracious-keller-98ef35.netlify.app/docs/recipes/rendering-only-when-needed/
  37. Understanding Three.js Materials: From Basic to Physical – Dipankar Paul’s blog, accessed April 16, 2026, https://blog.iamdipankarpaul.com/understanding-threejs-materials-from-basic-to-physical
  38. MeshStandardMaterial – three.js docs, accessed April 16, 2026, https://threejs.org/docs/pages/MeshStandardMaterial.html
  39. A Comprehensive Guide to Materials in Three.js – Chris Courses, accessed April 16, 2026, https://chriscourses.com/blog/a-comprehensive-guide-to-materials-in-threejs
  40. MeshPhongMaterial – three.js docs, accessed April 16, 2026, https://threejs.org/docs/pages/MeshPhongMaterial.html

Building Efficient Three.js Scenes: Optimize Performance While Maintaining Quality, accessed April 16, 2026, https://tympanus.net/codrops/2025/02/11/building-efficient-three-js-scenes-optimize-performance-while-maintaining-quality/

About the Author
RapidMade | WebGL/Three.js CAD Rendering Optimization

Micah Chaban
Founder & Vice President
RapidMade, Inc.

For 15 years I have worn every hat in our factory. I have advised engineers, fixed 3D printers, and toiled in the shop before we had a single employee. I write technical content for people who make parts that need to work in the real world.

talk out to us!

Call

(503) 943-2781 ext 1

Email

info@rapidmade.com

Chat

Bottom right page corner

Contact
VP of Sales and Marketing

Contact
3D Print Sales Specialist