Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/NeonD00m/feces/llms.txt

Use this file to discover all available pages before exploring further.

When replicating large amounts of data frequently, routing each component type through its own typed remote can significantly reduce packet overhead compared to sending everything through a single generic event. This guide shows how to do that using Blink, a typed networking library for Roblox.
The same pattern applies to any serialized networking library, not just Blink. The key idea is a lookup table that maps component names to dedicated remotes, with a generic fallback for any component that doesn’t have its own remote.
Define your network events in a .blink file. Create a dedicated typed event for each high-frequency component and a generic component event as a catch-all for everything else.
-- network.blink

event requestFullPacket {
    from: Client,
    type: Reliable,
    call: SingleAsync
}

type Entity = u16
event entityDeleted {
    from: Server,
    type: Reliable,
    call: SingleAsync,
    data: u16[]
}

map EntityMap<T> = { [Entity]: T }
enum SpecialChange = { __n, __d }
struct ComponentPacket<T> {
    value: EntityMap<T>,
    special: EntityMap<SpecialChange>
}

-- Generic fallback: handles any component without a dedicated event.
-- The first argument is the component id so the client knows what to apply.
event component {
    from: Server,
    type: Reliable,
    call: SingleAsync,
    data: (Entity, ComponentPacket<unknown>)
}

-- Dedicated typed event for the Transform component — avoids unknown serialization.
event componentTransform {
    from: Server,
    type: Reliable,
    call: SingleAsync,
    data: ComponentPacket<CFrame>
}

-- Add a new event like componentTransform for every component you want to optimize.
The generic component event handles any component that does not have its own dedicated remote, so you can start with just the generic event and add optimized remotes only for the components that are sent most frequently or carry the largest payloads.

Server Side

The fire() helper

fire() resolves whether a component has its own dedicated Blink remote. If a component{Name} event exists in the network module it uses that; otherwise it falls back to the generic component event, passing the component id as the first argument so the client can route it correctly.
local function fire(network, player, component, entities)
    -- Look up a dedicated remote by component name, e.g. "componentTransform"
    local remote = network[`component{getNameFromComponent(component)}`]
    if remote == nil then
        -- No dedicated remote — use the generic fallback.
        network.component.fire(player, component, entities)
        return
    end

    -- Dedicated remote found — fire without the component id prefix.
    remote.fire(player, entities)
end

The replicate() function

local function replicate()
    local network = require(ReplicatedStorage.Blink.Server)

    -- Handle full-snapshot requests from clients joining the game.
    local fullPacket = nil
    for _, player in network.requestFullPacket.iter() do
        fullPacket = fullPacket or full()
        for component, data in fullPacket do
            fire(network, player, component, data)
        end
    end

    -- Send per-player delta packets for all changed components.
    local changes, deleted = delta()
    for component, players in changes do
        for player, entities in players do
            fire(network, player, component, entities)
        end
    end

    -- Send entity deletions through the dedicated entityDeleted event.
    for player, entities in deleted do
        network.entityDeleted.fire(player, entities)
    end
end

Client Side

The receive() function

On the client, iterate over each Blink event and apply incoming data to the local feces instance. The generic component event carries a component id so apply knows which component to update; dedicated remotes use the component id from the Components lookup table.
local function receive()
    local network = require(ReplicatedStorage.Blink.Client)

    -- Generic fallback: component id is passed as the first argument.
    for _, component, entities in network.component.iter() do
        apply({ [component] = entities })
    end

    -- Dedicated per-component remotes.
    for name, id in Components do
        local remote = network[`component{getNameFromComponent(name)}`]
        if not remote then
            continue
        end

        for _, entities in remote.iter() do
            apply({ [id] = entities })
        end
    end

    -- Apply entity deletions.
    for _, entities in network.entityDeleted.iter() do
        apply({ __d = entities })
    end
end

Pattern Summary

Dedicated remotes

One Blink event per high-frequency component. The server fires typed data; the client iterates the event and applies directly. No component id in the payload — the remote name implies the type.

Generic fallback

The component event handles everything without a dedicated remote. The component id is sent as the first argument so the client can route it to the correct component on apply.
Call receive() in your client-side heartbeat or at the start of your frame, before your ECS systems run, so that replicated state is available immediately for that frame’s queries.
RunService.Heartbeat:Connect(function()
    receive()
end)

Build docs developers (and LLMs) love