2.reactivity.md 16 KB


title: Reactivity

description: Learn how to effectively use Vue's reactivity system with TresJS while maintaining optimal performance in high-frequency render loops.

Understanding Reactivity in 3D

Vue's reactivity system is one of its most powerful features, automatically tracking changes and updating the UI accordingly. However, when working with 3D scenes that render at 60+ frames per second, we need to be mindful of how reactivity impacts performance.

TresJS leverages Vue's reactivity while providing patterns that maintain optimal performance in continuous render loops.

The Performance Challenge

Vue Reactivity Under the Hood

Vue's reactivity is built on JavaScript Proxies, which intercept property access and mutations to track dependencies and trigger updates.

// Vue creates a Proxy wrapper around your data
const data = reactive({ x: 0, y: 0, z: 0 })

// Every property access is intercepted
data.x = 5 // Triggers reactivity system
console.log(data.y) // Also intercepted for dependency tracking

The 60FPS Problem

In a typical 3D scene running at 60 FPS, the render loop executes 60 times per second. If you're updating reactive objects in each frame, Vue's reactivity system processes these changes 60 times per second:

<script setup lang="ts">
// ❌ This creates performance issues
const position = reactive({ x: 0, y: 0, z: 0 })

const { onBeforeRender } = useLoop()
onBeforeRender(() => {
  // This triggers Vue's reactivity 60 times per second
  position.x = Math.sin(Date.now() * 0.001) * 3
  position.y = Math.cos(Date.now() * 0.001) * 2
})
</script>

Performance Impact

Here's a benchmark comparing reactive vs non-reactive object updates:

:::card-group ::card{title="Plain Object" icon="i-lucide-zap"} ~50M operations/second Direct property access without proxy overhead ::

::card{title="Reactive Object" icon="i-lucide-turtle"} ~2M operations/second Proxy interception adds significant overhead :: :::

Source: Proxy vs Plain Object Performance

Template Refs: The Preferred Approach

Template refs provide direct access to Three.js instances without reactivity overhead, making them ideal for animations and frequent updates.

Basic Template Ref Usage

<script setup lang="ts">
import type { TresInstance } from '@tresjs/core'

// Create a template ref for direct instance access
const meshRef = shallowRef<TresInstance | null>(null)

const { onBeforeRender } = useLoop()
onBeforeRender(({ elapsed }) => {
  if (meshRef.value) {
    // Direct property mutation - no reactivity overhead
    meshRef.value.rotation.x = elapsed * 0.5
    meshRef.value.rotation.y = elapsed * 0.3
    meshRef.value.position.y = Math.sin(elapsed) * 2
  }
})
</script>

<template>
  <TresCanvas>
    <TresPerspectiveCamera :position="[0, 0, 5]" />
    <TresAmbientLight />

    <!-- Template ref connects to the Three.js instance -->
    <TresMesh ref="meshRef">
      <TresBoxGeometry />
      <TresMeshStandardMaterial color="#ff6b35" />
    </TresMesh>
  </TresCanvas>
</template>

Multiple Template Refs

For complex scenes with multiple animated objects:

<script setup lang="ts">
import type { TresInstance } from '@tresjs/core'

const sphere1Ref = shallowRef<TresInstance | null>(null)
const sphere2Ref = shallowRef<TresInstance | null>(null)
const sphere3Ref = shallowRef<TresInstance | null>(null)

const { onBeforeRender } = useLoop()
onBeforeRender(({ elapsed }) => {
  // Animate multiple objects independently
  if (sphere1Ref.value) {
    sphere1Ref.value.position.x = Math.sin(elapsed) * 3
  }

  if (sphere2Ref.value) {
    sphere2Ref.value.position.x = Math.sin(elapsed + Math.PI * 0.5) * 3
  }

  if (sphere3Ref.value) {
    sphere3Ref.value.position.x = Math.sin(elapsed + Math.PI) * 3
  }
})
</script>

<template>
  <TresCanvas>
    <TresPerspectiveCamera :position="[0, 0, 5]" />

    <TresMesh ref="sphere1Ref" :position="[0, 2, 0]">
      <TresSphereGeometry />
      <TresMeshStandardMaterial color="red" />
    </TresMesh>

    <TresMesh ref="sphere2Ref" :position="[0, 0, 0]">
      <TresSphereGeometry />
      <TresMeshStandardMaterial color="green" />
    </TresMesh>

    <TresMesh ref="sphere3Ref" :position="[0, -2, 0]">
      <TresSphereGeometry />
      <TresMeshStandardMaterial color="blue" />
    </TresMesh>
  </TresCanvas>
</template>

Shallow Reactivity: When You Need Some Reactivity

Sometimes you need reactivity for UI controls while maintaining performance for animations. shallowRef and shallowReactive provide the perfect balance.

shallowRef vs ref

::code-group

<script setup lang="ts">
// ✅ Only .value access is reactive
const position = shallowRef({ x: 0, y: 0, z: 0 })
const meshRef = shallowRef<TresInstance | null>(null)

// UI control that triggers reactivity
const updatePosition = () => {
  position.value = { x: 5, y: 0, z: 0 } // Reactive update
}

const { onBeforeRender } = useLoop()
onBeforeRender(() => {
  if (meshRef.value) {
    // Direct mutation - no reactivity
    meshRef.value.rotation.y += 0.01
  }
})
</script>

<template>
  <div>
    <button @click="updatePosition">Update Position</button>
    <TresCanvas>
      <TresMesh ref="meshRef" :position="position">
        <TresBoxGeometry />
        <TresMeshStandardMaterial color="teal" />
      </TresMesh>
    </TresCanvas>
  </div>
</template>
<script setup lang="ts">
// ❌ Deep reactivity causes performance issues
const position = ref({ x: 0, y: 0, z: 0 })

// This would be reactive and expensive
const updatePosition = () => {
  position.value.x = 5 // Deep reactive mutation
}

const { onBeforeRender } = useLoop()
onBeforeRender(() => {
  // ❌ This triggers reactivity every frame
  position.value.x = Math.sin(Date.now() * 0.001) * 3
})
</script>

::

shallowReactive for Object Properties

When you need to reactively update multiple properties independently:

<script setup lang="ts">
// ✅ Top-level properties are reactive, nested ones aren't
const meshProps = shallowReactive({
  color: '#ff6b35',
  wireframe: false,
  visible: true,
  position: { x: 0, y: 0, z: 0 } // This object isn't deeply reactive
})

const meshRef = shallowRef<TresInstance | null>(null)

// UI controls that modify appearance
const toggleWireframe = () => {
  meshProps.wireframe = !meshProps.wireframe // Reactive
}

const changeColor = () => {
  meshProps.color = `#${Math.floor(Math.random() * 16777215).toString(16)}` // Reactive
}

const { onBeforeRender } = useLoop()
onBeforeRender(() => {
  if (meshRef.value) {
    // Direct position mutation - no reactivity overhead
    meshRef.value.position.y = Math.sin(Date.now() * 0.001) * 2
  }
})
</script>

<template>
  <div>
    <div class="controls">
      <button @click="toggleWireframe">Toggle Wireframe</button>
      <button @click="changeColor">Random Color</button>
    </div>

    <TresCanvas>
      <TresPerspectiveCamera :position="[0, 0, 5]" />
      <TresAmbientLight />

      <TresMesh
        ref="meshRef"
        :visible="meshProps.visible"
      >
        <TresBoxGeometry />
        <TresMeshStandardMaterial
          :color="meshProps.color"
          :wireframe="meshProps.wireframe"
        />
      </TresMesh>
    </TresCanvas>
  </div>
</template>

Best Practices and Patterns

1. Initial Positioning vs Animation

Use reactive props for initial positioning and template refs for animation:

<script setup lang="ts">
// ✅ Reactive initial state
const initialPosition = ref([0, 0, 0])
const color = ref('#ff6b35')

// ✅ Template ref for animations
const meshRef = shallowRef<TresInstance | null>(null)

// UI control for initial position
const moveLeft = () => {
  initialPosition.value = [-3, 0, 0]
}

const { onBeforeRender } = useLoop()
onBeforeRender(({ elapsed }) => {
  if (meshRef.value) {
    // ✅ Animate relative to initial position
    meshRef.value.position.y = initialPosition.value[1] + Math.sin(elapsed) * 2
  }
})
</script>

<template>
  <TresCanvas>
    <TresMesh
      ref="meshRef"
      :position="initialPosition"
    >
      <TresBoxGeometry />
      <TresMeshStandardMaterial :color="color" />
    </TresMesh>
  </TresCanvas>
</template>

2. Computed Properties for Complex Calculations

Use computed properties for expensive calculations that shouldn't run in every frame:

<script setup lang="ts">
const settings = shallowReactive({
  radius: 3,
  speed: 1,
  objects: 5
})

// ✅ Computed property recalculates only when dependencies change
const orbitPositions = computed(() => {
  const positions = []
  for (let i = 0; i < settings.objects; i++) {
    const angle = (i / settings.objects) * Math.PI * 2
    positions.push({
      x: Math.cos(angle) * settings.radius,
      z: Math.sin(angle) * settings.radius
    })
  }
  return positions
})

const meshRefs = shallowRef<(TresInstance | null)[]>([])

const { onBeforeRender } = useLoop()
onBeforeRender(({ elapsed }) => {
  meshRefs.value.forEach((mesh, index) => {
    if (mesh && orbitPositions.value[index]) {
      const pos = orbitPositions.value[index]
      // Animate around the computed orbit positions
      mesh.position.x = pos.x + Math.sin(elapsed * settings.speed) * 0.5
      mesh.position.z = pos.z + Math.cos(elapsed * settings.speed) * 0.5
    }
  })
})
</script>

<template>
  <TresCanvas>
    <TresMesh
      v-for="(position, index) in orbitPositions"
      :key="index"
      :ref="(el) => (meshRefs[index] = el)"
      :position="[position.x, 0, position.z]"
    >
      <TresSphereGeometry :args="[0.2]" />
      <TresMeshStandardMaterial color="#ff6b35" />
    </TresMesh>
  </TresCanvas>
</template>

3. Lifecycle-Based Updates

Use Vue's lifecycle hooks for performance-sensitive updates:

<script setup lang="ts">
const meshRef = shallowRef<TresInstance | null>(null)
const isAnimating = ref(true)

// Performance-sensitive animation state
const animationState = {
  time: 0,
  amplitude: 2,
  frequency: 1
}

const { onBeforeRender } = useLoop()
onBeforeRender(({ delta, elapsed }) => {
  if (!isAnimating.value || !meshRef.value) { return }

  // Update non-reactive state
  animationState.time += delta

  // Apply to Three.js instance
  meshRef.value.position.y = Math.sin(animationState.time * animationState.frequency) * animationState.amplitude
  meshRef.value.rotation.y = elapsed * 0.5
})

// Reactive controls that update animation parameters
const increaseAmplitude = () => {
  animationState.amplitude += 0.5
}

const toggleAnimation = () => {
  isAnimating.value = !isAnimating.value
}
</script>

<template>
  <div>
    <div class="controls">
      <button @click="toggleAnimation">
        {{ isAnimating ? 'Pause' : 'Play' }} Animation
      </button>
      <button @click="increaseAmplitude">Increase Amplitude</button>
    </div>

    <TresCanvas>
      <TresMesh ref="meshRef">
        <TresBoxGeometry />
        <TresMeshStandardMaterial color="teal" />
      </TresMesh>
    </TresCanvas>
  </div>
</template>

Common Pitfalls and Solutions

❌ Pitfall 1: Reactive Animation Data

<script setup lang="ts">
// ❌ DON'T: Reactive objects in render loop
const rotation = reactive({ x: 0, y: 0, z: 0 })

const { onBeforeRender } = useLoop()
onBeforeRender(({ elapsed }) => {
  // This triggers Vue's reactivity system every frame
  rotation.x = elapsed * 0.5
  rotation.y = elapsed * 0.3
})
</script>

<template>
  <TresMesh :rotation="[rotation.x, rotation.y, rotation.z]">
    <TresBoxGeometry />
    <TresMeshStandardMaterial />
  </TresMesh>
</template>

Solution: Use template refs

<script setup lang="ts">
// ✅ DO: Direct instance manipulation
const meshRef = shallowRef<TresInstance | null>(null)

const { onBeforeRender } = useLoop()
onBeforeRender(({ elapsed }) => {
  if (meshRef.value) {
    meshRef.value.rotation.x = elapsed * 0.5
    meshRef.value.rotation.y = elapsed * 0.3
  }
})
</script>

<template>
  <TresMesh ref="meshRef">
    <TresBoxGeometry />
    <TresMeshStandardMaterial />
  </TresMesh>
</template>

❌ Pitfall 2: Deep Reactive Arrays

<script setup lang="ts">
// ❌ DON'T: Deep reactive array updates
const particles = reactive(Array.from({ length: 100 }, (_, i) => ({
  position: { x: i, y: 0, z: 0 },
  velocity: { x: 0, y: 0, z: 0 }
})))

const { onBeforeRender } = useLoop()
onBeforeRender(() => {
  particles.forEach((particle) => {
    // This is extremely expensive with 100 reactive objects
    particle.position.x += particle.velocity.x
    particle.position.y += particle.velocity.y
  })
})
</script>

Solution: Non-reactive data with template refs

<script setup lang="ts">
// ✅ DO: Plain objects + template refs
const particleData = Array.from({ length: 100 }, (_, i) => ({
  position: { x: i, y: 0, z: 0 },
  velocity: { x: (Math.random() - 0.5) * 0.1, y: 0, z: 0 }
}))

const particleRefs = shallowRef<(TresInstance | null)[]>([])

const { onBeforeRender } = useLoop()
onBeforeRender(() => {
  particleData.forEach((particle, index) => {
    // Update plain object data
    particle.position.x += particle.velocity.x
    particle.position.y += particle.velocity.y

    // Apply to Three.js instance
    const mesh = particleRefs.value[index]
    if (mesh) {
      mesh.position.set(particle.position.x, particle.position.y, particle.position.z)
    }
  })
})
</script>

<template>
  <TresCanvas>
    <TresMesh
      v-for="(_, index) in particleData"
      :key="index"
      :ref="(el) => (particleRefs[index] = el)"
    >
      <TresSphereGeometry :args="[0.05]" />
      <TresMeshBasicMaterial />
    </TresMesh>
  </TresCanvas>
</template>

Performance Monitoring

::examples-performance-monitor ::

Use @tresjs/leches built-in fpsgraph for monitoring performance in your TresJS applications. This control displays real-time FPS information:

<script setup lang="ts">
import { TresLeches, useControls } from '@tresjs/leches'

// Enable FPS monitoring with TresLeches
useControls('fpsgraph')
</script>

<template>
  <TresLeches />
  <TresCanvas>
    <TresPerspectiveCamera :position="[0, 0, 5]" />
    <TresAmbientLight />

    <TresMesh>
      <TresBoxGeometry />
      <TresMeshStandardMaterial color="teal" />
    </TresMesh>
  </TresCanvas>
</template>

::tip TresLeches automatically displays an FPS graph overlay when you use the fpsgraph control. This provides real-time performance insights without manual implementation. Learn more at TresLeches Documentation. ::

Key Takeaways

:::card-group ::card{title="Template Refs First" icon="i-lucide-target"} Use template refs for direct Three.js instance access in render loops to avoid reactivity overhead. ::

::card{title="Shallow Reactivity" icon="i-lucide-layers-2"} Use shallowRef and shallowReactive when you need some reactivity without deep proxy overhead. ::

::card{title="Separate Concerns" icon="i-lucide-git-branch"} Keep UI state reactive and animation state non-reactive for optimal performance. ::

::card{title="Monitor Performance" icon="i-lucide-activity"} Use Nuxt DevTools and @tresjs/leches performance monitoring to identify reactivity bottlenecks in your 3D scenes. :: :::

::tip Remember: Vue's reactivity is powerful for UI updates but can be expensive in high-frequency render loops. Choose the right tool for each use case - reactive for user interactions, template refs for animations. ::

By understanding and applying these reactivity patterns, you can create performant 3D experiences that leverage Vue's strengths while avoiding common performance pitfalls.