Optimisation
Fusion tries to handle your code in the smartest way it can. To help achieve the best performance, you can give Fusion more information about what you're trying to do, or avoid a few problematic scenarios that slow Fusion down.
Update Skipping¶
Fusion tries to skip updates when they result in 'meaningless changes'.
TL;DR
When your computations return values that aren't meaningfully different, Fusion doesn't bother to perform further updates.
However, Fusion can't automatically do this for tables. So, you should freeze every table you create, unless you need to change what's inside the table later (for example, if it's a list that changes over time).
This allows Fusion to apply more aggressive optimisations for free.
Example¶
Imagine you have a number, and you're using a computed to calculate whether it's even or odd.
An observer is used to see how often this results in other code being run.
local number = scope:Value(1)
local isEven = scope:Computed(function(use)
return use(number) % 2 == 0
end)
scope:Observer(isEven):onChange(function()
print("-> isEven has changed to " .. peek(isEven))
end)
print("Number becomes 2")
number:set(2)
print("Number becomes 3")
number:set(3)
print("Number becomes 13")
number:set(13)
print("Number becomes 14")
number:set(14)
print("Number becomes 24")
number:set(24)
Number becomes 2
-> isEven has changed to true
Number becomes 3
-> isEven has changed to false
Number becomes 13
Number becomes 14
-> isEven has changed to true
Number becomes 24
Notice that the observer only runs when isEven
returns a meaningfully
different value:
-
When the number changed from 2 to 3,
isEven
returnedfalse
. This is meaningfully different from the previous value ofisEven
, which wastrue
. As a result, the observer is run and the printed message is seen. -
When the number changed from 3 to 13,
isEven
returnedfalse
. This is not meaningfully different from the previous value ofisEven
, which wasfalse
. As a result, the observer does not run, and no printed message is seen.
Similarity¶
When trying to determine if a change is 'meaningless', Fusion compares the old and new values, using what's called the similarity test.
The similarity test is a fast, approximate test that Fusion uses to guess which updates it can safely discard. If two values pass the similarity test, then you should be able to use them interchangeably without affecting most Luau code.
In Fusion's case, if the values before and after a change are similar, then Fusion won't continue updating other code beyond that change, because those updates aren't likely to have an effect on the outcome of computations.
Here's what the similarity test looks for:
- Different types:
- Two values of different types are never similar to each other.
- Tables:
- Frozen tables are similar to other values when they're
==
to each other. - Tables with a metatable are similar to other values when when they're
==
to each other. - Other kinds of table are never similar to anything.
- Frozen tables are similar to other values when they're
- Userdatas:
- Userdatas are similar to other values when they're
==
to each other.
- Userdatas are similar to other values when they're
- NaN:
- If each value does not
==
itself, then the two values are similar to each other. - This doesn't apply to tables or userdatas.
- If each value does not
- Any other values:
- Two values are similar to each other when they're
==
to each other.
- Two values are similar to each other when they're
Roblox data types
Roblox data types are not considered to be userdatas. Instead, the
similarity test follows typeof()
rules when determining type.
Optimising For Similarity¶
With this knowledge about the similarity test, you can experiment with how Fusion optimises different changes, and what breaks that optimisation.
Tables¶
Imagine you're setting a value object to a table of theme colours. You attach an observer object to see when Fusion thinks the theme meaningfully changed.
local LIGHT_THEME = {
name = "light",
-- imagine theme colours in here
}
local DARK_THEME = {
name = "dark",
-- imagine theme colours in here
}
local currentTheme = scope:Value(LIGHT_THEME)
scope:Observer(currentTheme):onChange(function()
print("-> currentTheme changed to " .. peek(currentTheme).name)
end)
print("Set to DARK_THEME")
currentTheme:set(DARK_THEME)
print("Set to DARK_THEME")
currentTheme:set(DARK_THEME)
print("Set to LIGHT_THEME")
currentTheme:set(LIGHT_THEME)
print("Set to LIGHT_THEME")
currentTheme:set(LIGHT_THEME)
Set to DARK_THEME
-> currentTheme changed to dark
Set to DARK_THEME
-> currentTheme changed to dark
Set to LIGHT_THEME
-> currentTheme changed to light
Set to LIGHT_THEME
-> currentTheme changed to light
Because the LIGHT_THEME
and DARK_THEME
tables aren't frozen, and they don't
have any metatables, Fusion will never skip over updates that change to or from
those values.
Why won't Fusion skip those updates?
In Fusion, it's common to update arrays without creating a new array. This is known as mutating the array.
local drinks = scope:Value({"beer", "pepsi"})
do -- add tea
local array = peek(drinks)
table.insert(array, "tea") -- mutation occurs here
drinks:set(array) -- still the same array, so it's ==
end
If Fusion skipped updates when the old and new values were ==
, then these
mutating changes wouldn't cause an update.
For that reason, Fusion doesn't skip updates for tables unless you do one of two things:
- You disable the ability to mutate the table (via
table.freeze
). - You indicate to Fusion that this isn't plain data by adding a metatable.
- Metatables are almost always used for OOP, where
==
is a sensible way of determining if two objects are similar. - You can also use metatables to define how equality should work, which Fusion will respect - though Fusion expects it to be symmetric.
- Metatables are almost always used for OOP, where
According to the similarity test (and the question section above), one way to skip these updates is by freezing the original tables.
local LIGHT_THEME = table.freeze {
name = "light",
-- imagine theme colours in here
}
local DARK_THEME = table.freeze {
name = "dark",
-- imagine theme colours in here
}
local currentTheme = scope:Value(LIGHT_THEME)
scope:Observer(currentTheme):onChange(function()
print("-> currentTheme changed to " .. peek(currentTheme).name)
end)
print("Set to DARK_THEME")
currentTheme:set(DARK_THEME)
print("Set to DARK_THEME")
currentTheme:set(DARK_THEME)
print("Set to LIGHT_THEME")
currentTheme:set(LIGHT_THEME)
print("Set to LIGHT_THEME")
currentTheme:set(LIGHT_THEME)
Set to DARK_THEME
-> currentTheme changed to dark
Set to DARK_THEME
Set to LIGHT_THEME
-> currentTheme changed to light
Set to LIGHT_THEME
Now, Fusion is confident enough to skip over the updates.
In general, you should freeze all of your tables when working with Fusion, unless you have a reason for modifying them later on. There's almost zero cost to freezing a table, making this modification essentially free. Plus, this lets Fusion optimise your updates more aggressively, which means you spend less time running computations on average.