First off, thanks for all the replies! They really helped in making my Replication System which is now running 2,000 parts with 10 kB/s and pretty decent Server time:
I am marking my message as a solution so anyone who comes across this topic sees it immediately, but all the credit goes to the people that replied to help me develop this System, now i’m going to break down how i really did it, some downsides of the system and upsides:

Upsides:
-
This system can run upwards of 10,000+ Parts and still be under 50 kB/s (The only limiting factor here is the CPU / Server, because you have to use a for loop to update each Object’s position every task.wait() / Heartbeat
-
The system uses buffers, a community module called “Packet”, workspace:BulkMoveTo and Lerping as it’s main functions. (Very easy to use)
-
Pretty scalable and is very easy to add new systems / functions to it.

Downsides:
-
The system runs with 5 FPS , meaning the objects get replicated 5 times per second, which makes it seem "laggy", but this is disguised using Lerping, and most of the times it’s either unnoticeable or just doesn’t matter.
-
This system does not manipulate the Rotation of the Objects being replicated, meaning if you want to do that you’d have to either add it into the System itself or have the Client do that for some of the Parts you add in.
-
Every Object is lerped with the same speed as to the Server replicating Objects. (1s / FPS) (Again this is can be easily changed by just doing custom speeds on the client or passing them in with each Object.
-
Every Object is always replicated, even if they aren’t moving.
So with all that sorted let’s get into how this System is made:
First off in the Server we made a new ModuleScript and make a variable for the Packet Module, which you can find here: Packet Module
After this we will make a new Packet Object which basically acts the same way as a Remote Event, but instead of sending info normally, it compacts the data using buffers, which will reduce the amount of data being sent by the Server to the Client, next up the second Parameter (after the name of the Packet Object), we will insert a table of Buffers as our type that will be sent to all the Clients:
local PACKET = require(game:GetService("ReplicatedStorage").Packet);
local REPLICATION_PACKET = PACKET("Replication", {PACKET.Buffer}); -- Packet Object
After this we make a Module variable that will store all Objects that will be replicated to all Clients:
Replication_Service.Instances_To_Replicate = {} :: {[number]: buffer};
After doing that we make a Start() function which when executed will do this code:
function Replication_Service:Start(): ()
while task.wait(TIMES_PER_SECOND_TO_SEND_INFO_TO_CLIENTS) do
REPLICATION_PACKET:Fire(Replication_Service.Instances_To_Replicate);
end
end
This code will replicate all Objects to every Client every set X amount of seconds which is this calculation: 1 / FPS | Ex: 1 / 20 = 0.05s
So before we continue now let’s get into the compression of Data and how we will handle that:
We will send 7 bytes of data per Object, these 7 bytes amount to 4 variables:
Instance Name: number, X Position: number, Y Position: number, Z Position: number
Now to compress this data to 7 Bytes we will use the buffer Library.
I will get to why the Instance Name is a number and not a string, here is a sample code of how i did the compression of data:
local function Get_Compressed_Data(Instance_Name: number, Given_CFrame: CFrame): buffer
local Compressed_Buffer = buffer.create(7);
buffer.writeu8(Compressed_Buffer, 0, Instance_Name);
buffer.writei16(Compressed_Buffer, 1, math.round(Given_CFrame.X));
buffer.writei16(Compressed_Buffer, 3, math.round(Given_CFrame.Y));
buffer.writei16(Compressed_Buffer, 5, math.round(Given_CFrame.Z));
return Compressed_Buffer;
end
I am using 16 bit integers just so this is more scalable with higher scale projects, you can lower this to 4 bytes easily but it’s very dependant on what you are going to do with this System, since the limitations of using only 4 bytes is a lot.
Here is also the Function to extract the Compressed Buffer Data:
-- Client Side:
local function Extract_Compressed_Data(Compressed_Buffer: buffer): (number, CFrame)
return buffer.readu8(Compressed_Buffer, 0), CFrame.new(buffer.readi16(Compressed_Buffer, 1), buffer.readi16(Compressed_Buffer, 3), buffer.readi16(Compressed_Buffer, 5));
end
-- Server Side:
local function Extract_Compressed_Data(Compressed_Buffer: buffer): (number, CFrame)
return CFrame.new(buffer.readi16(Compressed_Buffer, 1), buffer.readi16(Compressed_Buffer, 3), buffer.readi16(Compressed_Buffer, 5));
end
Ok so how do we add Objects to replicate using this System? Here’s a function for that:
--[[
This Function takes is extremely simple, and inserts the compressed data returned by
the Compressed Data Function into the Module Variable we made earlier.
It also uses a Highest Index variable to keep track of all the Objects so they don't
get out of sync inside the Table, if you were to remove one of them from it.
If you want to make a Function to remove an Object from Replication it's extremely
simple, all you gotta do is do the same as this Function but in reverse, while
sending an event to all Clients to Destroy() the Object that is not being replicated.
]]
function Replication_Service.Add_To_Replication(Instance_To_Replicate: number, Starting_CFrame: CFrame): number
HIGHEST_INDEX += 1;
table.insert(Replication_Service.Instances_To_Replicate, Get_Compressed_Data(Instance_To_Replicate, Starting_CFrame));
return HIGHEST_INDEX;
end
For optimal performance this System does any necessary Code outside of the Start() Function.
This makes it so we do not lose any performance by doing unnecessary things every time we
replicate Objects.
Now to update the CFrame / Position of a Replicated Object, all we need to do is this:
function Replication_Service.Update_Instance_CFrame(Instance_Name: number, Instance_ID: number, New_CFrame: CFrame): ()
-- Sanity Checks
if not Replication_Service.Instances_To_Replicate[Instance_ID] then
if RUN_SERVICE:IsStudio() then warn(`No Instance Object for Instance ID: {Instance_ID} was found!`) end;
return
end
if not Instance_Name then
if RUN_SERVICE:IsStudio() then warn(`No Instance Name for Instance ID: {Instance_ID} was provided!`) end;
return;
end
-- Keep in mind the Instance_ID Variable can be acquired via the Add_To_Replication Function.
Replication_Service.Instances_To_Replicate[Instance_ID] = Get_Compressed_Data(Instance_Name, New_CFrame);
end
And that is the Server-side done, extremely simple, now let’s get to the Client-side:
We will start with doing the exact same thing as in the Server, grabbing the Packet and the Packet
Object variables.
After this we will make 2 new variables:
local MOVE_CONNECTION: RBXScriptConnection = nil; -- Connection for Lerping
local REPLICATED_OBJECT_STORAGE_FOLDER = REPLICATED_STORAGE.Replicated_Object_Storage; -- Folder where all Replicated Objects are stored.
And then 2 Setting Variables:
local MOVE_TIME = 1 / 5; -- This needs to be the same value as the Server's "TIMES_PER_SECOND_TO_SEND_INFO_TO_CLIENTS" Variable
local INSTANCE_NAMES = { -- This is where we get the Object's name, using the Instance_Name Variable from the Server.
[1] = "TEST_PART";
};
After this we connect to when the Server sends us the Replicated Objects:
REPLICATION_PACKET.OnClientEvent:Connect(function(Instances_To_Move)
local OBJECTS_TO_MOVE = {};
local CFRAME_LIST = {};
for Instance_ID: number, Data: string in ipairs(Instances_To_Move) do
local Instance_Name, Grabbed_CFrame = Extract_Compressed_Data(Data); -- Convert Compressed Data.
Instance_Name = INSTANCE_NAMES[Instance_Name]; -- Grab the Instance's Name.
local PART_TO_MOVE = REPLICATED_OBJECTS_FOLDER:FindFirstChild(`{Instance_Name};{Instance_ID}`) or REPLICATED_OBJECT_STORAGE_FOLDER[Instance_Name]:Clone();
if not PART_TO_MOVE.Parent then PART_TO_MOVE.Name = `{Instance_Name};{Instance_ID}`; PART_TO_MOVE.Parent = REPLICATED_OBJECTS_FOLDER end;
-- Grab the Part that we will move, and insert it into a Table.
table.insert(OBJECTS_TO_MOVE, PART_TO_MOVE);
table.insert(CFRAME_LIST, Grabbed_CFrame);
end
if MOVE_CONNECTION then MOVE_CONNECTION:Disconnect() end; -- Disconnect any previous Lerps.
-- If we have any Objects to Lerp, do so.
if #OBJECTS_TO_MOVE > 0 then
local Alpha = 0;
local MOVE_PARTS = {}; -- Start Position of all Objects that we will move.
local CFRAMES_TO_MOVE_TO = {}; -- End Position of all Objects that we will move.
for _, Object in ipairs(OBJECTS_TO_MOVE) do
table.insert(MOVE_PARTS, Object.CFrame);
end
MOVE_CONNECTION = RUN_SERVICE.Heartbeat:Connect(function(deltaTime)
CFRAMES_TO_MOVE_TO = {};
Alpha = math.min(Alpha + deltaTime / MOVE_TIME, 1); -- Calculate our Alpha.
for i, Object: BasePart in ipairs(OBJECTS_TO_MOVE) do -- Loop through all Objects an d calculate their next Position, and insert it into a Table.
table.insert(CFRAMES_TO_MOVE_TO, MOVE_PARTS[i]:Lerp(CFRAME_LIST[i], Alpha));
end
workspace:BulkMoveTo(OBJECTS_TO_MOVE, CFRAMES_TO_MOVE_TO,
Enum.BulkMoveMode.FireCFrameChanged);
-- Move all Objects all at once, for optimal performance!
-- If we have finished, disconnect!
if Alpha == 1 then
MOVE_CONNECTION:Disconnect();
end
end)
end
end)
And that is the whole System, easy to use and to implement.
If anyone has improvements that they can make to this System, please let me know!
And for anyone that will use this System, no need to thank me
.