React Three Fiber Performance, WebGL issue?

So, I am working on an app for a company which renders out a DXF file (there are plenty of GH examples of this and I have looked through most / built out my own solutions for handling this).

The issue… or problem users are encountering is that sometimes, if the CPU usage spikes high enough some react components will just all-together disappear. The issue seems to be that the Webcontext is lost? Here is a screenshot of a heavy scene I have, the only thing I can note is that there is a large up front loading and building cost of converting the DXF file into three js elements. So a lot of the typescript classes I have built use new THREE(…)_ etc to build them out.

I know without the code most are going to guess, and because the app is proprietary I can’t share really any of it.. What I can share is that screen shot and that I am using geometry merges for static elements and for the interactive elements of the drawing I am attaching proxy hit checks so that the ray caster I am using can determine if an interaction needs to be allowed in the scene. If I can get any code together that doesn’t contain too much business logic or whatever else I will attach to this!

Again, issue is when loading in a file, large CPU usage spikes cause the context to die (from what it appears), I have tried all what I know at this point to get down the draw calls and usage.

<Canvas
        id='dxf-canvas'
        resize={{ scroll: false }}
        orthographic
        dpr={[0.75, 1]}
        gl={{ antialias: false, powerPreference: 'high-performance' }}
        camera={{ position: [0, 0, 100], zoom: 15 }}
        style={{
          background: '#fff',
          position: 'absolute',
          inset: 0,
          pointerEvents: !modelReady ? 'none' : 'auto',
        }}
        onPointerDown={onPointerDownCanvas}
        onPointerMove={onPointerMoveCanvas}
        onPointerUp={onPointerUpCanvas}
        onContextMenu={destinationMode ? undefined : onCanvasContextMenu}
        onPointerMissed={() => {
          // true R3F way to clear overlays on empty click
          setContextMenu(null);
        }}
        raycaster={{
          params: {
            Line: { threshold: 0.12 },
            Points: { threshold: 0 },
            Mesh: undefined,
            LOD: undefined,
            Sprite: undefined,
          },
        }}
      >
        {/* Only render content when model is ready */}
        {modelReady && (
          <group key={`scene-${sceneResetKey}`}>
            <AlignAssemblies
              assemblyClassRef={assemblyClassRef}
              connectionMapping={connectionClassref.current.Connections}
              alignedAssembliesRef={alignedAssembliesRef}
            />
            <WebGLContextHandler />

            <KeyboardControls
              map={[
                { name: 'escape', keys: ['Escape'] },
                { name: 'delete', keys: ['Delete', 'Backspace'] },
                { name: 'bulkSelect', keys: ['ControlLeft', 'ControlRight'] },
              ]}
              onChange={onKeyBoardChange}
            >
              {/* Placed assemblies */}
              {assemblyClassRef.current.BuildPlacedAssemblies(
                placementMode,
                orbitRef,
                onPrimitiveEntityClick,
                handleSave,
                null,
                currentDrawing,
                placedAssembliesSnapshot,
                onTerminalHover, // Pass terminal hover handler
                onTerminalHoverEnd // Pass terminal hover end handler
              )}

              {/* Placement follower */}
              {placementMode && assemblyToPlace && ghostAssembly && (
                <group>
                  <mesh
                    position={[0, 0, 0]}
                    onPointerMove={(e: ThreeEvent<PointerEvent>) => {
                      e.stopPropagation();
                      if (ghostAssembly) {
                        ghostAssembly.position.copy(e.point);
                        ghostAssembly.updateMatrixWorld(true);
                      }
                    }}
                    onClick={onPlacedAssembly}
                  >
                    <planeGeometry args={[10000, 10000]} />
                    <meshBasicMaterial transparent opacity={0} depthWrite={false} />
                  </mesh>
                  <primitive object={ghostAssembly} dispose={null} />
                </group>
              )}

              {/* Texts from DXF - key prop forces remount when texts change */}
              <PrebuiltTexts
                key={`texts-${sceneResetKey}`}
                texts={texts}
                placedAssemblies={placedAssembliesSnapshot} // Pass assemblies directly
                minCapPx={5}
              />

              {/* Lines */}
              {lineClassRef.current.DrawLines(
                isPanningRef.current,
                selectedLineRef,
                selectedLineKey,
                handleLineSelect,
                currentDrawing,
                terminalClassRef
              )}

              {/* Cross-drawing connection arrows */}
              {lineClassRef.current.DrawCrossDrawingArrows(
                isPanningRef.current,
                selectedLineRef,
                selectedLineKey,
                handleLineSelect,
                currentDrawing,
                modelGroup,
                assemblyClassRef.current.PlacedAssemblies,
                arrowClassRef,
                allProjectDrawings,
                terminalClassRef
              )}

              {/* External connection stubs */}
              {arrowClassRef.current.GenerateArrow(isPanningRef.current)}
            </KeyboardControls>

            {/* Lights */}
            <ambientLight castShadow={false} intensity={0.5} />
            <pointLight castShadow={false} position={[100, 100, 100]} />

            {/* DXF Model */}
            {DXFModel}

            {/* Controls + one-time fit (only on fresh model) */}
            <OrbitControls
              ref={orbitRef}
              makeDefault
              enableZoom
              enablePan
              enableDamping={false}
              enableRotate={false}
              mouseButtons={{ MIDDLE: THREE.MOUSE.PAN }}
              zoomSpeed={1}
              panSpeed={0.8}
              zoomToCursor
            />
            {!userMovedRef.current && <ZoomToFitOnce target={modelGroup} controlsRef={orbitRef} />}
            {destinationMode && fitTarget && <ZoomToFitOnce target={fitTarget} controlsRef={orbitRef} margin={1.6} />}
          </group>
        )}
        <Perf />
        {/* <Perf /> -- Uncomment to see performance metrics about the 3D Viewer */}
      </Canvas>

Build Function

**
   * Builds JSX elements for each placed assembly, attaching event handlers for interaction.
   * @param placementMode - Indicates if the viewer is in placement mode.
   * @param orbitRef - A reference to the orbit controls to enable/disable during drag.
   * @param onPrimitiveEntityClick - Click event handler for primitive entities.
   * @param handleSave - Save handler callback
   * @param placedAssembly - An optional placed assembly to add to the built nodes.
   * @param currentDrawing - The current drawing context for visibility filtering.
   * @param placedAssembliesSnapshot - An optional state variable to pass in to render those
   * @param onTerminalHover - Terminal hover handler from parent
   * @param onTerminalHoverEnd - Terminal hover end handler from parent
   * @returns An array of JSX elements representing the placed assemblies.
   */
  public BuildPlacedAssemblies(
    placementMode: boolean,
    orbitRef: React.MutableRefObject<any>,
    onPrimitiveEntityClick: (ev: ThreeEvent<MouseEvent>) => void,
    handleSave: () => void,
    placedAssembly: PlacedAssembly | null = null,
    currentDrawing: IProjectStandardDrawing | null = null,
    placedAssembliesSnapshot: PlacedAssembly[] | null = null,
    onTerminalHover?: (ev: ThreeEvent<PointerEvent>) => void,
    onTerminalHoverEnd?: () => void
  ): JSX.Element[] {
    // Use snapshot if provided, otherwise use internal array
    const assembliesToRender = placedAssembliesSnapshot ?? this._placedAssemblies;

    // Generate fresh JSX elements each time this is called
    const elements = assembliesToRender.map((pa) => (
      <primitive
        key={pa.group.userData.compositeKey}
        object={pa.group}
        onPointerDown={(ev: ThreeEvent<PointerEvent>) =>
          this._onPointerDownAssembly(ev, pa.group, placementMode, orbitRef)
        }
        onPointerMove={(ev: ThreeEvent<PointerEvent>) => {
          this._onPointerMoveAssembly(ev, pa.group);
          // Also trigger terminal hover detection
          onTerminalHover?.(ev);
        }}
        onPointerUp={(ev: ThreeEvent<PointerEvent>) => this._onPointerUpAssembly(ev, pa.group, orbitRef, handleSave)}
        onPointerOut={onTerminalHoverEnd}
        onClick={onPrimitiveEntityClick}
      />
    ));

    // Apply visibility filtering
    this.DisplayAssembliesForDrawing(currentDrawing);

    return elements;
  }

Prebuilt text

import { useMemo, useRef, useState } from 'react';
import type { TextEntity } from '../ThreeComponentBuilder/ThreeComponentBuilder';
import { Text } from '@react-three/drei';
import * as THREE from 'three';
import { useFrame, useThree, extend } from '@react-three/fiber';

type Props = {
  texts?: TextEntity[];
  font?: string;
  sdfGlyphSize?: number;
  rotationIsDegrees?: boolean;
  minCapPx?: number;
  minCapPxExit?: number;
  minOrthoZoom?: number;
  recomputeHz?: number;
  placedAssemblies?: Array<{ group: THREE.Group; texts: TextEntity[] }>;
};

/**
 * Shader material for rendering SDF text with a texture atlas.
 * Uses custom vertex and fragment shaders to handle text rendering.
 */
class SDFTextMaterial extends THREE.ShaderMaterial {
  // A basic SDF text shader material, see link here for reference:
  // https://www.redblobgames.com/x/2403-distance-field-fonts/
  constructor() {
    super({
      uniforms: {
        uAtlas: { value: null },
        uAtlasSize: { value: new THREE.Vector2(1024, 1024) },
      },
      vertexShader: `
        attribute vec2 uv;
        attribute vec3 position;
        attribute vec3 instancePosition;
        attribute vec4 instanceRotation;
        attribute vec3 instanceScale;
        attribute vec3 instanceColor;
        attribute vec4 instanceUV;

        varying vec2 vUV;
        varying vec3 vColor;

        uniform mat4 modelViewMatrix;
        uniform mat4 projectionMatrix;

        vec3 applyQuaternion(vec3 v, vec4 q) {
          return v + 2.0 * cross(q.xyz, cross(q.xyz, v) + q.w * v);
        }

        void main() {
          vUV = instanceUV.xy + uv * instanceUV.zw;
          vColor = instanceColor;

          vec3 transformed = position * instanceScale;
          transformed = applyQuaternion(transformed, instanceRotation);
          transformed += instancePosition;

          gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
        }
      `,
      fragmentShader: `
        uniform sampler2D uAtlas;
        varying vec2 vUV;
        varying vec3 vColor;

        void main() {
          float dist = texture2D(uAtlas, vUV).a;
          float alpha = smoothstep(0.45, 0.55, dist);

          if (alpha < 0.01) discard;

          gl_FragColor = vec4(vColor, alpha);
        }
      `,
      transparent: true,
      depthWrite: false,
      depthTest: false,
      side: THREE.DoubleSide,
    });
  }
}

extend({ SDFTextMaterial });

function toThreeColor(c: number | THREE.Color): THREE.Color {
  return typeof c === 'number' ? new THREE.Color(c) : c;
}

function worldToPixels(world: THREE.Vector3, camera: THREE.Camera, w: number, h: number) {
  const ndc = world.clone().project(camera);
  return new THREE.Vector2((ndc.x * 0.5 + 0.5) * w, (-ndc.y * 0.5 + 0.5) * h);
}

/**
 * PrebuiltTexts component renders a collection of text entities in a 3D scene.
 * It handles visibility culling based on camera view and text size in pixels.
 *
 * It supports both static texts from DXF files and dynamic texts from placed assemblies.
 * Uses SDF text rendering for crisp text at various sizes. (SDF - Signed Distance Field)
 *
 * @param texts Array of TextEntity objects to render.
 * @param font URL or path to the font file to use for rendering text.
 * @param sdfGlyphSize Size of each glyph in the SDF texture atlas.
 * @param rotationIsDegrees If true, text rotation is interpreted as degrees; otherwise radians.
 * @param minCapPx Minimum pixel height for text to be visible when entering view.
 * @param minCapPxExit Minimum pixel height for text to remain visible when exiting view.
 * @param minOrthoZoom Minimum orthographic camera zoom level to show texts.
 * @param recomputeHz Frequency (in Hz) to recompute visibility culling.
 * @param placedAssemblies Array of placed assemblies, each with a group and associated texts.
 * @returns JSX elements rendering the texts.
 */
export function PrebuiltTexts({
  texts,
  font,
  sdfGlyphSize = 64,
  rotationIsDegrees = false,
  minCapPx = 10,
  minCapPxExit = minCapPx * 0.8,
  minOrthoZoom,
  recomputeHz = 12,
  placedAssemblies = [],
}: Readonly<Props>) {
  const { camera, size, invalidate } = useThree();
  const items = texts ?? [];

  // Track frame updates to force re-render
  const [, setFrameCount] = useState(0);
  const frameCountRef = useRef(0);

  // Generate character set
  const characters = useMemo(() => {
    const set = new Set<string>();
    for (const t of items) for (const ch of t.text) set.add(ch);

    for (const assembly of placedAssemblies) {
      for (const t of assembly.texts) {
        for (const ch of t.text) set.add(ch);
      }
    }

    return Array.from(set).join('');
  }, [items, placedAssemblies]);

  // Prepare render data for DXF texts (static)
  const dxfRenderData = useMemo(
    () =>
      items.map((t, i) => {
        const position = t.position.toArray() as [number, number, number];
        const rotationZ = rotationIsDegrees ? (t.rotation * Math.PI) / 180 : t.rotation;
        const color = toThreeColor(t.color);
        const key = `dxf-${t.blockHandle}-${i}`;
        return { key, position, rotationZ, color, t, isDynamic: false as const };
      }),
    [items, rotationIsDegrees]
  );

  // Prepare render data for assembly texts (dynamic)
  const assemblyRenderData = useMemo(() => {
    const data: Array<{
      key: string;
      assemblyGroup: THREE.Group;
      localPosition: THREE.Vector3;
      rotationZ: number;
      color: THREE.Color;
      t: TextEntity;
      isDynamic: true;
      worldPosition: THREE.Vector3;
    }> = [];

    placedAssemblies.forEach((pa) => {
      pa.texts.forEach((t, textIdx) => {
        const rotationZ = rotationIsDegrees ? (t.rotation * Math.PI) / 180 : t.rotation;
        const color = toThreeColor(t.color);
        const key = `assembly-${pa.group.uuid}-${textIdx}`;

        data.push({
          key,
          assemblyGroup: pa.group,
          localPosition: t.position.clone(),
          rotationZ,
          color,
          t,
          isDynamic: true,
          worldPosition: new THREE.Vector3(),
        });
      });
    });

    return data;
  }, [placedAssemblies, rotationIsDegrees]);
  // Combine both static and dynamic render data
  const allRenderData = useMemo(() => [...dxfRenderData, ...assemblyRenderData], [dxfRenderData, assemblyRenderData]);

  // Visibility culling
  const [visibleKeys, setVisibleKeys] = useState<Set<string | number>>(new Set());
  const frustum = useMemo(() => new THREE.Frustum(), []);
  const projView = useMemo(() => new THREE.Matrix4(), []);
  const tmpVec = useMemo(() => new THREE.Vector3(), []);
  const frameBudget = useRef(0);
  const step = 1 / Math.max(1, recomputeHz);
  const prevVisibleRef = useRef<Set<string | number>>(new Set());

  useFrame((_, delta) => {
    // Update world positions for dynamic assembly texts
    let anyMoved = false;
    assemblyRenderData.forEach((rd) => {
      if (!rd.assemblyGroup.visible) return;

      const prevX = rd.worldPosition.x;
      const prevY = rd.worldPosition.y;

      rd.assemblyGroup.updateMatrixWorld(true);
      rd.worldPosition.copy(rd.localPosition);
      rd.assemblyGroup.localToWorld(rd.worldPosition);

      // Check if position changed significantly
      if (Math.abs(prevX - rd.worldPosition.x) > 0.001 || Math.abs(prevY - rd.worldPosition.y) > 0.001) {
        anyMoved = true;
      }
    });

    // Force React re-render if any assembly moved
    if (anyMoved) {
      frameCountRef.current++;
      setFrameCount(frameCountRef.current);
      invalidate();
    }

    frameBudget.current += delta;
    if (frameBudget.current < step) return;
    frameBudget.current = 0;

    if (!allRenderData.length) {
      if (visibleKeys.size) setVisibleKeys(new Set());
      return;
    }

    projView.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
    frustum.setFromProjectionMatrix(projView);

    const nextVisible = new Set<string | number>();
    const isOrtho = (camera as THREE.OrthographicCamera).isOrthographicCamera;
    const orthoZoom = isOrtho ? (camera as THREE.OrthographicCamera).zoom : 1;

    if (isOrtho && minOrthoZoom && orthoZoom < minOrthoZoom) {
      if (visibleKeys.size) setVisibleKeys(new Set());
      prevVisibleRef.current = new Set();
      return;
    }

    for (const rd of allRenderData) {
      const worldPos = rd.isDynamic ? rd.worldPosition : tmpVec.set(rd.position[0], rd.position[1], rd.position[2]);

      if (!frustum.containsPoint(worldPos)) continue;

      if (rd.isDynamic && !rd.assemblyGroup.visible) continue;

      let capPx: number;
      if (isOrtho) {
        capPx = rd.t.height * orthoZoom;
      } else {
        const p0 = worldToPixels(worldPos, camera, size.width, size.height);
        const p1 = worldToPixels(
          worldPos.clone().add(new THREE.Vector3(0, rd.t.height, 0)),
          camera,
          size.width,
          size.height
        );
        capPx = Math.abs(p1.y - p0.y);
      }

      const wasVisible = prevVisibleRef.current.has(rd.key);
      const show = wasVisible ? capPx >= minCapPxExit : capPx >= minCapPx;
      if (show) nextVisible.add(rd.key);
    }

    prevVisibleRef.current = nextVisible;
    if (nextVisible.size !== visibleKeys.size || Array.from(nextVisible).some((k) => !visibleKeys.has(k))) {
      setVisibleKeys(nextVisible);
      invalidate();
    }
  });

  // Filter visible items
  const visibleItems = useMemo(
    () => allRenderData.filter((rd) => visibleKeys.has(rd.key)),
    [allRenderData, visibleKeys]
  );

  return (
    <>
      <Text characters={characters} visible={false} font={font} sdfGlyphSize={sdfGlyphSize} onSync={invalidate}>
        {undefined}
      </Text>

      {visibleItems.map((rd) => {
        // Create new array each time to force Text component update
        const position: [number, number, number] =
          rd.isDynamic && 'worldPosition' in rd
            ? [rd.worldPosition.x, rd.worldPosition.y, rd.worldPosition.z]
            : rd.position;

        return (
          <Text
            key={rd.key}
            position={position}
            rotation={[0, 0, rd.rotationZ]}
            fontSize={rd.t.height}
            color={rd.color}
            anchorX={rd.t.anchorX}
            anchorY={rd.t.anchorY}
            characters={characters}
            sdfGlyphSize={sdfGlyphSize}
            font={font}
            material-toneMapped={false}
            material-depthWrite={false}
            material-depthTest={false}
            renderOrder={1000}
            onSync={invalidate}
          >
            {rd.t.text}
          </Text>
        );
      })}
    </>
  );
}

Again, any and all help is really appreciated here!