At NOVA LAB station every watt of energy counts - likewise in Vue applications, every unnecessary render and computation is a waste of performance. Let's explore composable optimization techniques.
ref() creates deep reactivity - any change inside an object triggers an update. shallowRef() reacts only to a change of the entire value, not internal properties.1import { shallowRef, triggerRef } from 'vue'
2
3// Large data list - shallowRef is more efficient
4const sensorReadings = shallowRef([])
5
6// This will NOT trigger an update (shallow reactivity)
7sensorReadings.value.push({ id: 1, temp: 22 })
8
9// This will trigger an update - assigning a new array
10sensorReadings.value = [...sensorReadings.value, { id: 1, temp: 22 }]
11
12// Alternatively: manually trigger an update
13sensorReadings.value.push({ id: 2, temp: 23 })
14triggerRef(sensorReadings) // Force update1<script setup>
2import { shallowRef, triggerRef, computed } from 'vue'
3
4// Efficient composable for large datasets
5function useLargeDataset() {
6 const items = shallowRef([])
7 const count = computed(() => items.value.length)
8
9 function addItem(item) {
10 // Create a new array - triggers reactivity
11 items.value = [...items.value, item]
12 }
13
14 function addMany(newItems) {
15 // One update instead of N
16 items.value = [...items.value, ...newItems]
17 }
18
19 function removeItem(id) {
20 items.value = items.value.filter(item => item.id !== id)
21 }
22
23 function updateItem(id, updates) {
24 items.value = items.value.map(item =>
25 item.id === id ? { ...item, ...updates } : item
26 )
27 }
28
29 return { items, count, addItem, addMany, removeItem, updateItem }
30}
31</script>When to use
shallowRef:Computed properties are automatically cached - they recalculate only when their dependencies change. This is a key optimization.
1import { ref, computed } from 'vue'
2
3function useFilteredExperiments() {
4 const experiments = ref([])
5 const searchQuery = ref('')
6 const category = ref('all')
7
8 // GOOD: computed is cached
9 // Recalculates ONLY when experiments, searchQuery or category changes
10 const filtered = computed(() => {
11 console.log('Recalculating filtered experiments') // logs rarely
12 let result = experiments.value
13
14 if (searchQuery.value) {
15 const q = searchQuery.value.toLowerCase()
16 result = result.filter(e =>
17 e.name.toLowerCase().includes(q)
18 )
19 }
20
21 if (category.value !== 'all') {
22 result = result.filter(e => e.category === category.value)
23 }
24
25 return result
26 })
27
28 // BAD: function recalculates on every render
29 function getFiltered() {
30 console.log('Recalculating every time!') // logs frequently
31 return experiments.value.filter(...)
32 }
33
34 // GOOD: chaining computed in a pipeline
35 const stats = computed(() => ({
36 total: filtered.value.length,
37 active: filtered.value.filter(e => e.status === 'active').length,
38 completed: filtered.value.filter(e => e.status === 'completed').length
39 }))
40
41 return { experiments, searchQuery, category, filtered, stats }
42}watch and watchEffect differ not only in API but also in performance.1import { ref, watch, watchEffect } from 'vue'
2
3const temperature = ref(22)
4const pressure = ref(101)
5const humidity = ref(45)
6
7// watchEffect - tracks ALL reactive values used
8// Fires on change of temperature, pressure OR humidity
9watchEffect(() => {
10 console.log('watchEffect:', temperature.value)
11 // Even if you use pressure only in a condition,
12 // watchEffect tracks it ALWAYS
13 if (temperature.value > 30) {
14 console.log('Pressure:', pressure.value)
15 }
16})
17
18// watch - tracks ONLY the specified source
19// Fires ONLY on temperature change
20watch(temperature, (newTemp) => {
21 console.log('watch:', newTemp)
22 // You can read other refs without creating dependencies
23 if (newTemp > 30) {
24 console.log('Pressure:', pressure.value)
25 }
26})Rule: use
watch when you need to react to specific changes. Use watchEffect when you want automatic tracking of multiple dependencies.One of the most common problems - uncleaned resources cause memory leaks.
1import { ref, onMounted, onUnmounted, watch } from 'vue'
2
3// BAD - memory leak!
4function useLeakyTimer() {
5 const seconds = ref(0)
6
7 onMounted(() => {
8 setInterval(() => { // Never cleaned up!
9 seconds.value++
10 }, 1000)
11 })
12
13 return { seconds }
14}
15
16// GOOD - proper cleanup
17function useSafeTimer() {
18 const seconds = ref(0)
19 const isRunning = ref(false)
20 let intervalId = null
21
22 function start() {
23 if (isRunning.value) return
24 isRunning.value = true
25 intervalId = setInterval(() => {
26 seconds.value++
27 }, 1000)
28 }
29
30 function stop() {
31 if (!isRunning.value) return
32 isRunning.value = false
33 clearInterval(intervalId)
34 intervalId = null
35 }
36
37 // Always clean up in onUnmounted
38 onUnmounted(() => {
39 if (intervalId) {
40 clearInterval(intervalId)
41 }
42 })
43
44 return { seconds, isRunning, start, stop }
45}| Technique | When to use | |----------|-------------| |
shallowRef | Large arrays/objects, infrequent updates |
| computed instead of functions | Value depends on reactive state |
| watch instead of watchEffect | Specific source to observe |
| Cleanup in onUnmounted | Timers, event listeners, WebSocket |
| Batch updates | Multiple changes at once instead of one by one |
| triggerRef | Mutating shallowRef without creating a copy |1// Batch updates - one update instead of three
2import { ref } from 'vue'
3
4function useBatchUpdate() {
5 const data = ref({ temp: 0, pressure: 0, humidity: 0 })
6
7 // BAD: 3 separate updates
8 function updateBad(temp, pressure, humidity) {
9 data.value.temp = temp // render
10 data.value.pressure = pressure // render
11 data.value.humidity = humidity // render
12 }
13
14 // GOOD: 1 update (new object)
15 function updateGood(temp, pressure, humidity) {
16 data.value = { temp, pressure, humidity } // 1 render
17 }
18
19 return { data, updateGood }
20}