Katalyst – A simple & small but powerful ui framework

:fire:Katalyst – A simple & small but powerful ui framework

Yo! I cooked up something called Katalyst a simple & small UI framework that makes building reactive GUIs way less painful. No more spaghetti signal connections or fighting with layout.


Documentation Website - For more information

:puzzle_piece: Core Idea

Katalyst wraps Roblox ui instances in a proxy so you can:

  • Bind properties directly to state
  • React to changes with computed props (functions)
  • Manage children declaratively
  • Hook into events with a clean self (the proxy, not just the raw instance)

Think of it like Roact/Fusion, but lean and without the ceremony.


Features

  1. Reactive State :magnifying_glass_tilted_right:
Summary
local State = Katalyst.State
local New = Katalyst.New

local count = State(0)

local label = New("TextLabel")({
	Text = function()
		return "Count: " .. count:get()
	end,
})

count:set(count:get() + 1)

Change the state, UI updates. Easy.

  1. Events, but cleaner :sparkles:
Summary
local New = Katalyst.New

local button = New("TextButton")({
	Text = "Click Me",
	OnActivated = function(self)
		self.Text = "Clicked!"
	end,
})

If you ever need the raw Roblox instance, just call self().

Summary
OnEnter = function(self)
	TweenService:Create(self(), TweenInfo.new(0.3, Enum.EasingStyle.Back, Enum.EasingDirection.Out), { BackgroundTransparency = 0.5 }):Play()
end
  1. Drag it :ok_hand:
    Any UI object can be made draggable in one line:
Summary
local New = Katalyst.New

local window = New("Frame")({
	Draggable = true,
	OnDragEnd = function(self, pos)
		print("Dropped at", pos)
	end,
})
  1. Mapping made easy :world_map:
    Say goodbye to manual looping. Katalyst comes with a Map helper:
Summary
local New = Katalyst.New
local Map = Katalyst.Map

local items = { "One", "Two", "Three" }

local list = New("Frame")({
	Children = {
		New("UIListLayout")({}),
		Map(items, function(item, i)
			return New("TextLabel")({
				Text = i .. ". " .. item,
			})
		end),
	},
})

It automatically sets LayoutOrder for you, so the order just works™.

  1. Refs for control :bullseye:
    Need to grab your component or run cleanup when it’s destroyed? Use Ref:
Summary
local New = Katalyst.New

local box = New("TextBox")({
	Ref = function(self)
		-- self is the proxy
		self.Text = "Hello world!"

		-- optional cleanup when instance is destroyed
		return function()
			print("TextBox destroyed!")
		end
	end,
})
  1. Still just an Instance :building_construction:
    Even though you interact with components through the proxy, under the hood they’re real Roblox Instances.
    That means you can still do:
Summary
local frame = New("Frame")({})

frame:Destroy() -- works fine
frame():Clone()   -- also fine
  1. Works with Hoarcekat :framed_picture:
    Katalyst plays nice with Hoarcekat, so you can build out components in isolation and preview them instantly.
    Just return your Katalyst UI from a story file and it works:
Summary
return function(target)
	return New("TextLabel")({
		Parent = target,
		Text = "Hello from Katalyst in Hoarcekat!",
	})
end

-- or
local Label = New("TextLabel")({
	Text = "Hello from Katalyst in Hoarcekat!",
})
return function(target)
	Label.Parent = target
end

:light_bulb: Why use Katalyst?

  • Less boilerplate
  • Reactive props without glue code
  • Clean event API with proxy self
  • Call self() if you need the raw instance
  • Still a normal Instance, so :Destroy(), :Clone(), Tween* all work
  • Built-in drag handling
  • Easy .Map for lists
  • Ref for mount logic & cleanup

Ready to try? Get the model here
https://create.roblox.com/store/asset/102585843889477/Katalyst

15 Likes

Do we really need another UI library?
They are kind of useless and i even gave up on my own library realizing how pointless it is really
Declaritiveness may be an argument but you can use Luau sugar such as:

local new = Instance.new

local Frame = new "Frame" do
   Frame.Name = "test"
end

Without downsides and perfomance hit like libraries do and ending up spamming GETTABLEKS in bytecode and especially for libraries that run past creation with “reactive states”.

7 Likes

Wow! Another fusionreactroact library! Everyone migrate!!! Sugoi!

6 Likes

I mean i get ur point, some people doesnt want some extra code to just making a ui. Me myself are web developer, and i kinda miss a bit how i usually built a ui. Here in roblox is kinda tricky to make the same ui as the web, dont get me wrong, the roblox default itself are not that bad, but sometimes the code are getting harder to maintain as ur ui getting larger in my opinion.
Thats why i tried to make this library as small as possible, just a single file, thats it. Already satisfy my needs. And i just want to share it here so people that has kinda same idea with me can use it.
So im sorry if im triggering some people with my ui slops library

1 Like

Because UI for games ~= UI for web.
For games you must make it actually optimized (even if its so silly as class hierarchy on roblox) all of this libraries fully kill perfomance with “reactive features”.
If you knew bytecode you would’ve understood why i am so hostile against bloat.

1 Like

bBUT It’s JUSt like REAct from FACEBOOk :face_with_symbols_on_mouth:
:+1::mate:

1 Like

i understood, and im totally with you for that.
but trying to scarifies some couple frametime for DX its also fine too, as long as not exceeding the frametime budget.

and im a bit curios how my framework perform, and i tried to benchmark it and compare with normal instance.new, i know its not 1:1 to the real world, but it gives the idea how the reactivity perform.

and heres the result updating 100 times per frame with 1000 elements, running for 3x

=== AVERAGES OVER 3 RUNS ===  -  Client - Client:224
  10:39:55.250  Raw              -> FPS: 104.0 | UPS: 10401.2 | HeapΔ: 6740.7 KB | Writes: 26224.3  -  Client - Client:225
  10:39:55.250  Framework Direct -> FPS: 103.9 | UPS: 10391.6 | HeapΔ: 8650.7 KB | Writes: 26243.0  -  Client - Client:234
  10:39:55.250  Framework React. -> FPS: 103.4 | UPS: 10338.5 | HeapΔ: 8866.0 KB | Writes: 26126.0  -  Client - Client:243
  10:39:55.251  
=== RELATIVE PERFORMANCE (vs Raw) ===  -  Client - Client:253
  10:39:55.251  Direct: FPS 99.9% | UPS 99.9% | Heap 128.3% | Writes 100.1%  -  Client - Client:254
  10:39:55.251  React.: FPS 99.4% | UPS 99.4% | Heap 131.5% | Writes 99.6%  -  Client - Client:263

is not that bad

and this is the benchmark code

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Framework = require(ReplicatedStorage.Shared.Lib.Katalyst)
local RunService = game:GetService("RunService")

local NUM_ELEMENTS = 1000
local UPDATES_PER_FRAME = 100
local TEST_DURATION = 5
local RUNS = 3
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")

local SIZE_ON = UDim2.fromOffset(100, 100)
local SIZE_OFF = UDim2.fromOffset(0, 0)

local function heapKB()
	return collectgarbage("count")
end

-- Shared toggle sequence (so all modes do the same updates)
local updateIndices = {}
do
	math.randomseed(1234) -- deterministic for fairness
	for frame = 1, 600 do -- ~10s worth of frames
		local batch = {}
		for j = 1, UPDATES_PER_FRAME do
			batch[j] = math.random(1, NUM_ELEMENTS)
		end
		updateIndices[frame] = batch
	end
end

----------------------------------------------------------------
-- RAW benchmark
----------------------------------------------------------------
local function benchmarkRaw()
	local gui = Instance.new("ScreenGui", playerGui)
	local parent = Instance.new("Frame", gui)

	local children = {}
	local frameCount, updates, engineWrites = 0, 0, 0

	local startTime = tick()

	for i = 1, NUM_ELEMENTS do
		local child = Instance.new("Frame")
		child.Size = SIZE_OFF
		child.Position = UDim2.fromOffset((i % 40) * 10, math.floor(i / 40) * 10)
		child.Parent = parent
		children[i] = child

		child:GetPropertyChangedSignal("Size"):Connect(function()
			engineWrites += 1
		end)
	end

	local conn
	conn = RunService.RenderStepped:Connect(function()
		frameCount += 1
		local batch = updateIndices[(frameCount % #updateIndices) + 1]
		for _, idx in ipairs(batch) do
			local child = children[idx]
			if updates % 2 == 0 then
				child.Size = SIZE_ON
			else
				child.Size = SIZE_OFF
			end
			updates += 1
		end
	end)

	task.wait(TEST_DURATION)
	conn:Disconnect()

	local elapsed = tick() - startTime
	local fps = frameCount / elapsed
	local ups = updates / elapsed
	local endHeap = heapKB()

	gui:Destroy()
	return fps, ups, endHeap, engineWrites, nil -- no latency for raw
end

----------------------------------------------------------------
-- FRAMEWORK DIRECT benchmark
----------------------------------------------------------------
local function benchmarkFrameworkDirect()
	local gui = Instance.new("ScreenGui", playerGui)
	local parent = Instance.new("Frame", gui)

	local proxies = {}
	local frameCount, updates, engineWrites = 0, 0, 0

	local startTime = tick()

	for i = 1, NUM_ELEMENTS do
		local proxy = Framework.New("Frame")({
			Parent = parent,
			Size = SIZE_OFF,
			Position = UDim2.fromOffset((i % 40) * 10, math.floor(i / 40) * 10),
		})
		proxies[i] = proxy
		proxy():GetPropertyChangedSignal("Size"):Connect(function()
			engineWrites += 1
		end)
	end

	local conn
	conn = RunService.RenderStepped:Connect(function()
		frameCount += 1
		local batch = updateIndices[(frameCount % #updateIndices) + 1]
		for _, idx in ipairs(batch) do
			local inst = proxies[idx]()
			if updates % 2 == 0 then
				inst.Size = SIZE_ON
			else
				inst.Size = SIZE_OFF
			end
			updates += 1
		end
	end)

	task.wait(TEST_DURATION)
	conn:Disconnect()

	local elapsed = tick() - startTime
	local fps = frameCount / elapsed
	local ups = updates / elapsed
	local endHeap = heapKB()

	gui:Destroy()
	return fps, ups, endHeap, engineWrites, nil
end

----------------------------------------------------------------
-- FRAMEWORK REACTIVE benchmark
----------------------------------------------------------------
local function benchmarkFrameworkReactive()
	local gui = Instance.new("ScreenGui", playerGui)
	local parent = Instance.new("Frame", gui)

	local states = {}
	local frameCount, updates, engineWrites = 0, 0, 0

	local startTime = tick()

	for i = 1, NUM_ELEMENTS do
		local s = Framework.State(SIZE_OFF)
		local proxy = Framework.New("Frame")({
			Parent = parent,
			Size = s,
			Position = UDim2.fromOffset((i % 40) * 10, math.floor(i / 40) * 10),
		})
		states[i] = s
		proxy():GetPropertyChangedSignal("Size"):Connect(function()
			engineWrites += 1
		end)
	end

	local conn
	conn = RunService.RenderStepped:Connect(function()
		frameCount += 1
		local batch = updateIndices[(frameCount % #updateIndices) + 1]
		for _, idx in ipairs(batch) do
			if updates % 2 == 0 then
				states[idx].__lastSet = tick()
				states[idx]:set(SIZE_ON)
			else
				states[idx].__lastSet = tick()
				states[idx]:set(SIZE_OFF)
			end
			updates += 1
		end
	end)

	task.wait(TEST_DURATION)
	conn:Disconnect()

	local elapsed = tick() - startTime
	local fps = frameCount / elapsed
	local ups = updates / elapsed
	local endHeap = heapKB()

	gui:Destroy()
	return fps, ups, endHeap, engineWrites
end

----------------------------------------------------------------
-- Multi-run driver
----------------------------------------------------------------
local function average(list)
	local sumFPS, sumUPS, sumHeap, sumWrites, sumLatency = 0, 0, 0, 0, 0
	for _, v in ipairs(list) do
		sumFPS += v[1]
		sumUPS += v[2]
		sumHeap += v[3]
		sumWrites += v[4]
		if v[5] then
			sumLatency += v[5]
		end
	end
	return sumFPS / #list, sumUPS / #list, sumHeap / #list, sumWrites / #list
end

task.delay(6, function()
	local rawResults, fwDirectResults, fwReactiveResults = {}, {}, {}

	for run = 1, RUNS do
		print("=== RAW Run " .. run .. " ===")
		table.insert(rawResults, { benchmarkRaw() })
		task.wait(1)

		print("=== FRAMEWORK DIRECT Run " .. run .. " ===")
		table.insert(fwDirectResults, { benchmarkFrameworkDirect() })
		task.wait(1)

		print("=== FRAMEWORK REACTIVE Run " .. run .. " ===")
		table.insert(fwReactiveResults, { benchmarkFrameworkReactive() })
		task.wait(1)
	end

	local rawFPS, rawUPS, rawHeap, rawWrites = average(rawResults)
	local dirFPS, dirUPS, dirHeap, dirWrites = average(fwDirectResults)
	local reaFPS, reaUPS, reaHeap, reaWrites = average(fwReactiveResults)

	print("\n=== AVERAGES OVER " .. RUNS .. " RUNS ===")
	print(
		string.format(
			"Raw              -> FPS: %.1f | UPS: %.1f | HeapΔ: %.1f KB | Writes: %.1f",
			rawFPS,
			rawUPS,
			rawHeap,
			rawWrites
		)
	)
	print(
		string.format(
			"Framework Direct -> FPS: %.1f | UPS: %.1f | HeapΔ: %.1f KB | Writes: %.1f",
			dirFPS,
			dirUPS,
			dirHeap,
			dirWrites
		)
	)
	print(
		string.format(
			"Framework React. -> FPS: %.1f | UPS: %.1f | HeapΔ: %.1f KB | Writes: %.1f",
			reaFPS,
			reaUPS,
			reaHeap,
			reaWrites
		)
	)

	print("\n=== RELATIVE PERFORMANCE (vs Raw) ===")
	print(
		string.format(
			"Direct: FPS %.1f%% | UPS %.1f%% | Heap %.1f%% | Writes %.1f%%",
			(dirFPS / rawFPS) * 100,
			(dirUPS / rawUPS) * 100,
			(dirHeap / rawHeap) * 100,
			(dirWrites / rawWrites) * 100
		)
	)
	print(
		string.format(
			"React.: FPS %.1f%% | UPS %.1f%% | Heap %.1f%% | Writes %.1f%%",
			(reaFPS / rawFPS) * 100,
			(reaUPS / rawUPS) * 100,
			(reaHeap / rawHeap) * 100,
			(reaWrites / rawWrites) * 100
		)
	)
end)

if something that you can improve for my benchmark, just let me know.

(edit: i forgot to turn off default styling override)

i make the benchmark more intense which using textbutton with changing text, and i added Fusion for comparison

heres the results:

=== AVERAGES OVER 3 RUNS ===  -  Client - Client:313
  11:11:16.127  Raw              -> FPS: 54.1 | UPS: 5406.5 | HeapΔ: 11598.3 KB | Writes: 13643.3  -  Client - Client:314
  11:11:16.128  Framework Direct -> FPS: 54.4 | UPS: 5441.1 | HeapΔ: 11843.7 KB | Writes: 13779.3  -  Client - Client:323
  11:11:16.128  Framework React. -> FPS: 51.1 | UPS: 5114.6 | HeapΔ: 19882.3 KB | Writes: 12957.7  -  Client - Client:332
  11:11:16.128  Fusion           -> FPS: 24.3 | UPS: 2430.1 | HeapΔ: 21936.0 KB | Writes: 6184.7  -  Client - Client:342
  11:11:16.128  
=== RELATIVE PERFORMANCE (vs Raw) ===  -  Client - Client:352
  11:11:16.128  Direct: FPS 100.6% | UPS 100.6% | Heap 102.1% | Writes 101.0%  -  Client - Client:353
  11:11:16.128  React.: FPS 94.6% | UPS 94.6% | Heap 171.4% | Writes 95.0%  -  Client - Client:362
  11:11:16.128  Fusion : FPS 44.9% | UPS 44.9% | Heap 189.1% | Writes 45.3%  -  Client - Client:371

the bench code:

Summary
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Framework = require(ReplicatedStorage.Shared.Lib.Katalyst)
local Fusion = require(ReplicatedStorage.Shared.Lib.Fusion)
local scoped = Fusion.scoped
local scope = scoped(Fusion)
local RunService = game:GetService("RunService")

local NUM_ELEMENTS = 1000
local UPDATES_PER_FRAME = 100
local TEST_DURATION = 5
local RUNS = 3
local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")

local SIZE_ON = UDim2.fromOffset(100, 100)
local SIZE_OFF = UDim2.fromOffset(0, 0)

local function heapKB()
	return collectgarbage("count")
end

-- Shared toggle sequence (so all modes do the same updates)
local updateIndices = {}
do
	math.randomseed(1234) -- deterministic for fairness
	for frame = 1, 600 do -- ~10s worth of frames
		local batch = {}
		for j = 1, UPDATES_PER_FRAME do
			batch[j] = math.random(1, NUM_ELEMENTS)
		end
		updateIndices[frame] = batch
	end
end

----------------------------------------------------------------
-- RAW benchmark
----------------------------------------------------------------
local function benchmarkRaw()
	local gui = Instance.new("ScreenGui", playerGui)
	local parent = Instance.new("Frame", gui)

	local children = {}
	local frameCount, updates, engineWrites = 0, 0, 0

	local startTime = tick()

	for i = 1, NUM_ELEMENTS do
		local child = Instance.new("TextButton")
		child.Size = SIZE_OFF
		child.Text = "State: Off (0)"
		child.Position = UDim2.fromOffset((i % 40) * 10, math.floor(i / 40) * 10)
		child.Parent = parent
		children[i] = child

		child:GetPropertyChangedSignal("Size"):Connect(function()
			engineWrites += 1
		end)
	end

	local conn
	conn = RunService.RenderStepped:Connect(function()
		frameCount += 1
		local batch = updateIndices[(frameCount % #updateIndices) + 1]
		for _, idx in ipairs(batch) do
			local child = children[idx]
			if updates % 2 == 0 then
				child.Size = SIZE_ON
				child.Text = "State: On (" .. updates .. ")"
			else
				child.Size = SIZE_OFF
				child.Text = "State: Off (" .. updates .. ")"
			end
			updates += 1
		end
	end)

	task.wait(TEST_DURATION)
	conn:Disconnect()

	local elapsed = tick() - startTime
	local fps = frameCount / elapsed
	local ups = updates / elapsed
	local endHeap = heapKB()

	gui:Destroy()
	return fps, ups, endHeap, engineWrites, nil -- no latency for raw
end

----------------------------------------------------------------
-- FRAMEWORK DIRECT benchmark
----------------------------------------------------------------
local function benchmarkFrameworkDirect()
	local gui = Instance.new("ScreenGui", playerGui)
	local parent = Instance.new("Frame", gui)

	local proxies = {}
	local frameCount, updates, engineWrites = 0, 0, 0

	local startTime = tick()

	for i = 1, NUM_ELEMENTS do
		local proxy = Framework.New("TextButton")({
			Parent = parent,
			Size = SIZE_OFF,
			Text = "State: Off (0)",
			Position = UDim2.fromOffset((i % 40) * 10, math.floor(i / 40) * 10),
		})
		proxies[i] = proxy
		proxy():GetPropertyChangedSignal("Size"):Connect(function()
			engineWrites += 1
		end)
	end

	local conn
	conn = RunService.RenderStepped:Connect(function()
		frameCount += 1
		local batch = updateIndices[(frameCount % #updateIndices) + 1]
		for _, idx in ipairs(batch) do
			local inst = proxies[idx]()
			if updates % 2 == 0 then
				inst.Size = SIZE_ON
				inst.Text = "State: On (" .. updates .. ")"
			else
				inst.Size = SIZE_OFF
				inst.Text = "State: Off (" .. updates .. ")"
			end
			updates += 1
		end
	end)

	task.wait(TEST_DURATION)
	conn:Disconnect()

	local elapsed = tick() - startTime
	local fps = frameCount / elapsed
	local ups = updates / elapsed
	local endHeap = heapKB()

	gui:Destroy()
	return fps, ups, endHeap, engineWrites, nil
end

----------------------------------------------------------------
-- FRAMEWORK REACTIVE benchmark (same as your version)
----------------------------------------------------------------
local function benchmarkFrameworkReactive()
	local gui = Instance.new("ScreenGui", playerGui)
	local parent = Instance.new("Frame", gui)

	local states = {}
	local states2 = {}
	local states3 = {}
	local frameCount, updates, engineWrites = 0, 0, 0

	local startTime = tick()

	for i = 1, NUM_ELEMENTS do
		local s = Framework.State(SIZE_OFF)
		local s2 = Framework.State("Off")
		local s3 = Framework.State(0)
		local proxy = Framework.New("TextButton")({
			Parent = parent,
			Size = s,
			Text = function()
				return "State: " .. s2:get() .. " (" .. s3:get() .. ")"
			end,
			Position = UDim2.fromOffset((i % 40) * 10, math.floor(i / 40) * 10),
		})
		states[i] = s
		states2[i] = s2
		states3[i] = s3
		proxy():GetPropertyChangedSignal("Size"):Connect(function()
			engineWrites += 1
		end)
	end

	local conn
	conn = RunService.RenderStepped:Connect(function()
		frameCount += 1
		local batch = updateIndices[(frameCount % #updateIndices) + 1]
		for _, idx in ipairs(batch) do
			if updates % 2 == 0 then
				states[idx]:set(SIZE_ON)
				states2[idx]:set("On")
				states3[idx]:set(updates)
			else
				states[idx]:set(SIZE_OFF)
				states2[idx]:set("Off")
				states3[idx]:set(updates)
			end
			updates += 1
		end
	end)

	task.wait(TEST_DURATION)
	conn:Disconnect()

	local elapsed = tick() - startTime
	local fps = frameCount / elapsed
	local ups = updates / elapsed
	local endHeap = heapKB()

	gui:Destroy()
	return fps, ups, endHeap, engineWrites
end

----------------------------------------------------------------
-- FUSION benchmark
----------------------------------------------------------------
local function benchmarkFusion()
	local gui = Instance.new("ScreenGui", playerGui)
	local parent = Instance.new("Frame", gui)

	local states = {}
	local states2 = {}
	local states3 = {}
	local frameCount, updates, engineWrites = 0, 0, 0

	local startTime = tick()

	for i = 1, NUM_ELEMENTS do
		local s = scope:Value(SIZE_OFF)
		local s2 = scope:Value("Off")
		local s3 = scope:Value(0)
		local proxy = scope:New("TextButton")({
			Parent = parent,
			Size = s,
			Text = scope:Computed(function(use)
				return "State: " .. use(s2) .. " (" .. use(s3) .. ")"
			end),
			Position = UDim2.fromOffset((i % 40) * 10, math.floor(i / 40) * 10),
		})
		states[i] = s
		states2[i] = s2
		states3[i] = s3
		proxy:GetPropertyChangedSignal("Size"):Connect(function()
			engineWrites += 1
		end)
	end

	local conn
	conn = RunService.RenderStepped:Connect(function()
		frameCount += 1
		local batch = updateIndices[(frameCount % #updateIndices) + 1]
		for _, idx in ipairs(batch) do
			if updates % 2 == 0 then
				states[idx]:set(SIZE_ON)
				states2[idx]:set("On")
				states3[idx]:set(updates)
			else
				states[idx]:set(SIZE_OFF)
				states2[idx]:set("Off")
				states3[idx]:set(updates)
			end
			updates += 1
		end
	end)

	task.wait(TEST_DURATION)
	conn:Disconnect()

	local elapsed = tick() - startTime
	local fps = frameCount / elapsed
	local ups = updates / elapsed
	local endHeap = heapKB()

	gui:Destroy()
	return fps, ups, endHeap, engineWrites
end

----------------------------------------------------------------
-- Multi-run driver
----------------------------------------------------------------
local function average(list)
	local sumFPS, sumUPS, sumHeap, sumWrites, sumLatency = 0, 0, 0, 0, 0
	for _, v in ipairs(list) do
		sumFPS += v[1]
		sumUPS += v[2]
		sumHeap += v[3]
		sumWrites += v[4]
		if v[5] then
			sumLatency += v[5]
		end
	end
	return sumFPS / #list, sumUPS / #list, sumHeap / #list, sumWrites / #list
end

task.delay(6, function()
	local rawResults, fwDirectResults, fwReactiveResults, fwFusionResults = {}, {}, {}, {}

	for run = 1, RUNS do
		print("=== RAW Run " .. run .. " ===")
		table.insert(rawResults, { benchmarkRaw() })
		task.wait(1)

		print("=== FRAMEWORK DIRECT Run " .. run .. " ===")
		table.insert(fwDirectResults, { benchmarkFrameworkDirect() })
		task.wait(1)

		print("=== FRAMEWORK REACTIVE Run " .. run .. " ===")
		table.insert(fwReactiveResults, { benchmarkFrameworkReactive() })
		task.wait(1)

		print("=== FUSION Run " .. run .. " ===")
		table.insert(fwFusionResults, { benchmarkFusion() })
		task.wait(1)
	end

	local rawFPS, rawUPS, rawHeap, rawWrites = average(rawResults)
	local dirFPS, dirUPS, dirHeap, dirWrites = average(fwDirectResults)
	local reaFPS, reaUPS, reaHeap, reaWrites = average(fwReactiveResults)
	local fusFPS, fusUPS, fusHeap, fusWrites = average(fwFusionResults)

	print("\n=== AVERAGES OVER " .. RUNS .. " RUNS ===")
	print(
		string.format(
			"Raw              -> FPS: %.1f | UPS: %.1f | HeapΔ: %.1f KB | Writes: %.1f",
			rawFPS,
			rawUPS,
			rawHeap,
			rawWrites
		)
	)
	print(
		string.format(
			"Framework Direct -> FPS: %.1f | UPS: %.1f | HeapΔ: %.1f KB | Writes: %.1f",
			dirFPS,
			dirUPS,
			dirHeap,
			dirWrites
		)
	)
	print(
		string.format(
			"Framework React. -> FPS: %.1f | UPS: %.1f | HeapΔ: %.1f KB | Writes: %.1f",
			reaFPS,
			reaUPS,
			reaHeap,
			reaWrites
		)
	)

	print(
		string.format(
			"Fusion           -> FPS: %.1f | UPS: %.1f | HeapΔ: %.1f KB | Writes: %.1f",
			fusFPS,
			fusUPS,
			fusHeap,
			fusWrites
		)
	)

	print("\n=== RELATIVE PERFORMANCE (vs Raw) ===")
	print(
		string.format(
			"Direct: FPS %.1f%% | UPS %.1f%% | Heap %.1f%% | Writes %.1f%%",
			(dirFPS / rawFPS) * 100,
			(dirUPS / rawUPS) * 100,
			(dirHeap / rawHeap) * 100,
			(dirWrites / rawWrites) * 100
		)
	)
	print(
		string.format(
			"React.: FPS %.1f%% | UPS %.1f%% | Heap %.1f%% | Writes %.1f%%",
			(reaFPS / rawFPS) * 100,
			(reaUPS / rawUPS) * 100,
			(reaHeap / rawHeap) * 100,
			(reaWrites / rawWrites) * 100
		)
	)
	print(
		string.format(
			"Fusion : FPS %.1f%% | UPS %.1f%% | Heap %.1f%% | Writes %.1f%%",
			(fusFPS / rawFPS) * 100,
			(fusUPS / rawUPS) * 100,
			(fusHeap / rawHeap) * 100,
			(fusWrites / rawWrites) * 100
		)
	)
end)

1 Like

Do we really need to whine when people making/sharing something they make?
This forum have more people yapping than stack overflow, lol

7 Likes