title: Reactivity
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.
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
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>
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 provide direct access to Three.js instances without reactivity overhead, making them ideal for animations and frequent updates.
<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>
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>
Sometimes you need reactivity for UI controls while maintaining performance for animations. shallowRef
and shallowReactive
provide the perfect balance.
::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>
::
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>
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>
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>
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>
<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>
<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>
::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.
::
:::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.