How do I optimize a scrolling frame for over 1000 frames?

You don’t have to iterate through all the frames every time the canvas position changes. You can calculate exactly which frames are going to be visible just from knowing the canvas position and size and such.

Imagine that the frames are in a grid like this:

So the upper left frame has coordinate (0, 0), the one to the right of that (1, 0), and the bottom right in the image has coordinate (2, 3). The Y coordinate just keeps going as you go down in the list.

Now imagine that we number the frames from 1 to 1000, starting with frame (0, 0), then frame (1, 0), (2, 0), (0, 1), (1, 1), etc. I.e. just like reading, one row at a time and going to the next row when you reach the right side. You can convert between frame index and frame coordinate like this:

local frame_x = frame_index % list_width
local frame_y = math.floor( frame_index / list_width )

Trying this for frame_index = 1..7 I get

0	0	1	1
1	0	2	2
2	0	3	3
0	1	4	4
1	1	5	5
2	1	6	6
0	2	7	7

… which seems right. You can convert the other way like so:

frame_index = 1 + frame_x + frame_y * list_width

In other words, if you know the number of a frame in the list, you also know it’s Y coordinate in the “frame grid”. You can convert that to its Y pixel coordinate in the scrolling frame, something like this:

local pixel_y = frame_y * frame_height

… and convert the other way like this:

local frame_y = pixel_y / frame_height

So if you know the Y coordinates of the top of the scrolling frame and the bottom of the scrolling frame, then you can convert that to a list of frames in the list that are currently visible:

local visible_frames = {} --a global in the script

--- ... later in the script...

function update_visible_frames()
    local frame_y_top = list_canvas_top / frame_height
    local frame_y_bottom = list_canvas_bottom / frame_height

    local newly_visible_frames = {}
    for y = frame_y_top, frame_y_bottom do
        local done_iterating = false
        for x = 0, 2 do
            local frame = frame_array[1 + x + y * list_width]
            if frame then
                table.insert(visible_frames, frame)
            else
                --We reached the end of the frame_array, and since we're iterating in order, no frames could possibly come after this one, so we don't need to keep iterating
                done_iterating = true
                break
            end
            
            if done_iterating then
                break
            end
        end
    end

You can then toggle only the necessary frames like so:

    --Turn on newly visible frames
    for _, newly_visible_frame in pairs(newly_visible_frames) do
        if not table.find(previously_visible_frames, newly_visible_frame) then
            newly_visible_frame.Visible = true
        end 
    end

    --Turn on newly invisible frames
    for _, previously_visible_frame in pairs(visible_frames) do
        if not table.find(newly_visible_frames, previously_visible_frame) then
            previously_visible_frame.Visible = false
        end
    end

… and don’t for get to update the list of visible frames for the next time we update:

    visible_frames = newly_visible_frames
end

I hope this helps! I’ve left finding the top and bottom of the canvas up to you, let me know if you need help with that or if anything is unclear :slight_smile: Also… I haven’t tested this with real GUIs, so let me know if something doesn’t work because my thinking may be a bit flawed :sweat_smile:

In the end though, this solution should make it so you only have to iterate over the 24 or so frames that could possible have changed visibility.

15 Likes

I wrote this code! From looking at what you have so far, you are definitely on the right track.

There are a few reasons why you are getting worse performance than you expect:

  • You are testing every single frame to see if it is visible, when you can actually calculate an exact range of frames thare are visible in one step (explained in reply above)
  • You are looking for an info frame and then destroying / cloning for every frame, when you can instead use a pool and reparent as necessary
  • You are calling GetChildren every time when you can instead track your main item frames in an array manually (this also helps you be sure about indices & order which is relevant when updating).

Here is an overview of how it works at Bubble Gum Simulator:

  • Main grid contains a single blank frame for every item in the inventory
  • Pool of detail / info frames to draw from during an update.

Updating to respond to scrolling / size changes works like this:

  1. Calculate first and last on-screen/visible frame indices using roughly the same techniques helpfully given in the reply above
  2. For each frame in that range, grab a detail / info frame from the pool and reparent it to the frame, also setting any properties in the detail / info frame as necessary
  3. For any remaining detail / info frames in the pool, parent those to nil.
10 Likes

Thank you for replying. So I was reading over your post. And I’m having a little trouble understanding the variables that you have made such as list_width, frame_height, and frame_width. Not sure what the difference is between the list_width and the frame_width is. But it sounds like to me the list_width is going to be how many frames I have in a horizontal row which is 3. Can you explain this a little further or tell me what exactly I am trying to find for those variables?

Sorry, that should be list_width as well. I edited my post to fix that.

list_width is how many frames are in a row, so 3 in your case.

frame_height is the height of each frame in pixels.

I’m not sure what frame_array is but I’m gonna assume thats a table containing all the frames with the index equal to which order we put them in the Scrolling Frame. Which I see a flaw with this, how would I update the “frame_array” since my content in my frame will be ordered by a name not by 1 … 2 … 3 … ect… efficiently. Also I have a few questions about the list_canvas_top and list_canvas_bottom. Is that the canvas position from top to bottom or the exact y coordinate on the screen if its the exact y coordinate how would I calculate?

Edit: I just realized I could use table.sort() to sort the table but I’m not sure if it will be efficient enough for this. But could you explain to me list_canvas_top and list_canvas_bottom?

https://gyazo.com/fe8040d2628366bc4493a9b730728fc7 Table.sort() isn’t good enough to sort huge tables.

Just so you are aware I want to be able to use this with SortOrdering on the UIGridLayout.

2 Likes

I’ve actually just experimented with this. I created a virtual list of items (managed to get to about 667,000 items before it started glitching) by rendering only what’s visible.

I’ve just made it public if you’d like to have a look at the source code:

4 Likes

How would I use this with layout ordering? Something like this when I click the button it orders it from rarity. https://gyazo.com/8e62901292359af2bbdfc74564300275

1 Like

You’d have to manually sort your array of items before adding them to the list. What I made was by no means perfect or optimised, or even designed for production. It was more of a personal proof of concept.

These two articles helped me a little when making it:

2 Likes

If you don’t want the lag spike when the ordering changes, keep both sorted lists around and insert/remove individual items as they’re inserted/removed.
It takes 2x as much memory but that’s not a big deal.

Hm. Can you explain to me what exactly you mean keeping both sorted lists? As I recall you are indexing from a huge table of frames which means I would have to sort that “frame_array” that contains over 5000 frames using table.sort(). I want to know how to efficiently do this when clicking a button etc… not when something gets removed or added from the list. And lets say you already have 5,000 frames in this array but want to order them by name or order them by rank. Is there any efficient way to do this because anytime something is ordered differently with the layout I would have to sort it again with table.sort().

And on another note could you explain to me what you mean by list_canvas_top and list_canvas_bottom, as I understand everything else in your code except what these two actually mean.

I am totally lost on what I should be putting for canvas top and bottom.


I actually found out how to sort tables faster by dividing them individually. I just need to understand one more thing about this. Can you explain to me what list_canvas_top and list_canvas_bottom is? Also how I could get this correctly?

1 Like

Nevermind. I believe I have it all working. Thanks for pointing me towards a good method.

1 Like

Hi Scripting Void I was wondering if you can give the final code for this or something. I am still confused what CanvasTop and Canvas Bottom is. Very sorry. :confused:

Alright, here is what I have come up with. I’ll just put this here for learning purposes and for people that are still struggling.

local List = {}

local NewlyVisibleFrames = {}

local PreviouslyVisible = {}
--- ... later in the script...

function update_visible_frames(AmmountInTable)
	local CellPaddingY = UIGridLayout.CellPadding.Y.Offset
	local TopCanvasPosition = Nest.AbsolutePosition.Y
	local TopWindowPosition = ScrollingFrame.AbsolutePosition.Y
	local TopOfficialSize = (TopWindowPosition-TopCanvasPosition)
	local FrameYTop = math.floor((TopOfficialSize/(UIGridLayout.AbsoluteCellSize.Y+UIGridLayout.CellPadding.Y.Offset))*UIGridLayout.AbsoluteCellCount.X + .5)
	local AmmountInWindow = math.floor((ScrollingFrame.AbsoluteSize.Y/(UIGridLayout.AbsoluteCellSize.Y+UIGridLayout.CellPadding.Y.Offset))*UIGridLayout.AbsoluteCellCount.X + .5)
	FrameYTop = math.clamp(FrameYTop-UIGridLayout.AbsoluteCellCount.X, 0, 2e9)
	local FrameYBottom = (FrameYTop + AmmountInWindow)+(UIGridLayout.AbsoluteCellCount.X*2)
	if FrameYBottom > AmmountInTable then
		FrameYBottom = AmmountInTable
	end
	PreviouslyVisible = NewlyVisibleFrames
	NewlyVisibleFrames = {}
	for i = FrameYTop, FrameYBottom do
		if List[i] then
			local Frame = List[i]
			local find = table.find(PreviouslyVisible, Frame)
			if find then
				table.remove(PreviouslyVisible, find)
			end
			if not Frame:FindFirstChildOfClass("Folder") then
				ReplicatedStorage.Info:Clone().Parent = Frame
			end
			table.insert(NewlyVisibleFrames, Frame)
		end
	end
	for i, v in pairs(PreviouslyVisible) do
		if v:FindFirstChildOfClass("Folder") then
			v.Info:Destroy()
		end
	end
end

To get this to work properly you will need a Frame that is parented to the scrolling frame which I named Nest and then you put in the UIGridLayout.

image

You will always need to update the size when the absolute size changes and make sure you always update the canvas size when the scrolling frame canvas size changes with these two examples.

function UpdateNest()
	Nest.Size = UDim2.new(Nest.Size.X.Scale, 0, 0, UIGridLayout.AbsoluteContentSize.Y)
end

function UpdateCanvasSize()
	ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, UIGridLayout.AbsoluteContentSize.Y)
end

I’ll leave the rest up to you to figure out. If you need any help just ask.

1 Like

UIGridLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
        UpdateNest()
        UpdateCanvasSize()
        update_visible_frames(10)
    end)

Wait I am confused for update visible function…

The update_visible_frames() takes the number of items you have in the list. So it would be #list. Also make sure you have created a bunch of blank templates and inserted them into the table.

This is what I mean by creating templates and inserting them inside the Nest. Make sure you also are inserting them into the list table.

local Entrys = 5000
for i = 1, Entrys do
	local clone = script.Template:Clone()
	clone.Name = HttpService:GenerateGUID(false)
	clone.Parent = ScrollingFrame.Nest
	table.insert(List, clone)
	
	--if i % (math.ceil(Entrys*1)) == 0 or i == 1 then
		--PilesOfLists[#PilesOfLists+1] = {}
	--end
	
	--table.insert(PilesOfLists, HttpService:GenerateGUID(false))
end

local Entrys = 5000
    for i = 1, Entrys do
	local clone = game.ReplicatedStorage.Info:Clone()
	clone.Name = HttpService:GenerateGUID(false)
	clone.Parent = ScrollingFrame.Nest
	table.insert(List, clone)
	
	--if i % (math.ceil(Entrys*1)) == 0 or i == 1 then
		--PilesOfLists[#PilesOfLists+1] = {}
	--end
	
	--table.insert(PilesOfLists, HttpService:GenerateGUID(false))
    end
	UIGridLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
        UpdateNest()
        UpdateCanvasSize()

        update_visible_frames(#List)
    end)

It still lags. Nothing gets destroyed either.

We are not putting the cloned info into a table at all but instead the templates. We are cloning blank templates into the nest and once the template is shown to the user it grabs the info from ReplicatedStorage and parents it inside the blank template,


This is where your info Cloning should be only and what it does is search through the table and find which blank templates are being currently shown, then it parents the info frame from replicated storage inside it. Once the info is cloned you should add all the events and stuff onto it or whatever you want to be shown.

Also here is an example of how you should be updating them.

function UpdateAll()

--UpdateScrollingFrame()

UpdateNest()

UpdateCanvasSize()

update_visible_frames(#List)

end

UpdateAll()

ScrollingFrame:GetPropertyChangedSignal("CanvasPosition"):Connect(function()

update_visible_frames(#List)

end)

ScrollingFrame:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()

UpdateAll()

--UpdateNest()

--UpdateScrollingFrame()

--update_visible_frames(#List)

--UpdateUIGridLayout()

end)

Ok so I wanted to come back to this thing. The adding of info frames and placement of everything works nicely. Problem is that its still lagging. Getting around 50 fps.

local List = {}
local ReplicatedStorage = game.ReplicatedStorage
local UIGridLayout = script.Parent.Nest.UIGridLayout
local HTTPSService = game:GetService("HttpService")
local ScrollingFrame = script.Parent
local Nest = script.Parent.Nest
local NewlyVisibleFrames = {}

local PreviouslyVisible = {}
--- ... later in the script...
function UpdateNest()
	Nest.Size = UDim2.new(Nest.Size.X.Scale, 0, 0, UIGridLayout.AbsoluteContentSize.Y)
end

function UpdateCanvasSize()
	ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, UIGridLayout.AbsoluteContentSize.Y)
end
function update_visible_frames(AmmountInTable)
	local CellPaddingY = UIGridLayout.CellPadding.Y.Offset
	local TopCanvasPosition = Nest.AbsolutePosition.Y
	local TopWindowPosition = ScrollingFrame.AbsolutePosition.Y
	local TopOfficialSize = (TopWindowPosition-TopCanvasPosition)
	local FrameYTop = math.floor((TopOfficialSize/(UIGridLayout.AbsoluteCellSize.Y+UIGridLayout.CellPadding.Y.Offset))*UIGridLayout.AbsoluteCellCount.X + .5)
	local AmmountInWindow = math.floor((ScrollingFrame.AbsoluteSize.Y/(UIGridLayout.AbsoluteCellSize.Y+UIGridLayout.CellPadding.Y.Offset))*UIGridLayout.AbsoluteCellCount.X + .5)
	FrameYTop = math.clamp(FrameYTop-UIGridLayout.AbsoluteCellCount.X, 0, 2e9)
	local FrameYBottom = (FrameYTop + AmmountInWindow)+(UIGridLayout.AbsoluteCellCount.X*2)
	if FrameYBottom > AmmountInTable then
		FrameYBottom = AmmountInTable
	end
	PreviouslyVisible = NewlyVisibleFrames
	NewlyVisibleFrames = {}
	for i = FrameYTop, FrameYBottom do
		if List[i] then
			local Frame = List[i]
			local find = table.find(PreviouslyVisible, Frame)
			if find then
				table.remove(PreviouslyVisible, find)
			end
			if not Frame:FindFirstChildOfClass("Folder") then
				ReplicatedStorage.Info:Clone().Parent = Frame
			end
			table.insert(NewlyVisibleFrames, Frame)
			--print(Frame)
			--print(NewlyVisibleFrames)
		end
	end
	for i, v in pairs(PreviouslyVisible) do
		if v:FindFirstChildOfClass("Folder") then
			v.Info:Destroy()
			
		end
	end
end
local Entrys = 10000
for i = 1, Entrys do
	local clone = script.Template:Clone()
	clone.Name = HTTPSService:GenerateGUID(false)
	clone.Parent = ScrollingFrame.Nest
	table.insert(List, clone)

	--if i % (math.ceil(Entrys*1)) == 0 or i == 1 then
	--PilesOfLists[#PilesOfLists+1] = {}
	--end

	--table.insert(PilesOfLists, HttpService:GenerateGUID(false))
end
function UpdateAll()

	--UpdateScrollingFrame()

	UpdateNest()

	UpdateCanvasSize()

	update_visible_frames(#List)

end

UpdateAll()

ScrollingFrame:GetPropertyChangedSignal("CanvasPosition"):Connect(function()

	update_visible_frames(#List)

end)

ScrollingFrame:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()

	UpdateAll()

	--UpdateNest()

	--UpdateScrollingFrame()

	--update_visible_frames(#List)

	--UpdateUIGridLayout()

end)