Skip to content

Reduce Reconciliation

In all likelihood, the primary source of performance gains for your app will come from reducing the amount of work that Roact's reconciliation process requires. This is accomplished by:

  • Indicating to Roact that some reconciliation work can be skipped
  • Making sure your elements only change in ways you intended

shouldUpdate Lifecycle Method

When a Roact Component's state or props change, it will call the Component's shouldUpdate method to determine whether or not to re-render it. The default implementation will always return true.

function Component:shouldUpdate(newProps, newState)
    return true
end

If you have a more complex component that only needs to re-render in certain situations, you can either use PureComponent (discussed below) or implement your own shouldUpdate and return false in any case where an update is not required.

Warning

Manually implementing shouldUpdate is dangerous! If done carelessly, it can easily create confusing or subtle bugs.

In most cases, the preferable solution is to use PureComponent instead, which has a simple and robust implementation of shouldUpdate.

PureComponent

One common implementation of shouldUpdate is to do a shallow comparison between current and previous props and state. Roact provides an extension of Roact.Component called Roact.PureComponent that uses this implementation.

Let's use the following example:

local Item = Roact.Component:extend("Item")

function Item:render()
    local icon = self.props.icon
    local layoutOrder = self.props.layoutOrder

    -- Create a list item with the item's icon and name
    return Roact.createElement("ImageLabel", {
        LayoutOrder = layoutOrder,
        Image = icon,
    })
end

local Inventory = Roact.Component:extend("Inventory")

function Inventory:render()
    -- An Inventory contains a list of items
    local items = self.state.items

    local itemList = {}
    -- Create a UIListLayout to space out our items
    itemList["Layout"] = Roact.createElement("UIListLayout", {
        SortOrder = Enum.SortOrder.LayoutOrder,
        FillDirection = Enum.FillDirection.Vertical,
    })
    for i, item in ipairs(items) do
        -- Add the element to our list
        itemList[i] = Roact.createElement(Item, {
            layoutOrder = i,
            icon = item.icon,
        })
    end

    -- The Inventory renders a frame containing the list of Items as children
    return Roact.createElement("Frame", {
        Size = UDim2.new(0, 200, 0, 400)
    }, itemList)
end

In the above example, adding a new item to the items prop of the Inventory would cause all of the child Item elements to re-render, even if they haven't changed at all. This means if you add an item to an Inventory that already has 5 items, the result will be 6 renders of the Item component.

Lets change Item to a PureComponent:

local Item = Roact.PureComponent:extend("Item")
Now, if we add a new item to the end of the Inventory or change something about an existing item, we'll only re-render the Inventory itself and the modified Item!

Warning

When working with PureComponent, it's critical to use immutable props. Immutability guarantees that a prop's reference will change any time its contents change.

Info

There's more to discuss about immutability. It deserves a fully fleshed-out section somewhere!

Stable Keys

Another performance improvement we can make is to use stable, unique keys to refer to our child elements.

When the list that we pass into the Inventory component changes, Roact updates our Roblox UI by adjusting the properties of each Roblox Instance according to the new list of elements.

For example, let's suppose our list of items is as follows:

{
    { id = "sword", icon = swordIcon }, -- [1]
    { id = "shield", icon = shieldIcon }, -- [2]
}

If we add a new item to the beginning, then we'll end up with a list like this:

{
    { id = "potion", icon = potionIcon } -- [1]
    { id = "sword", icon = swordIcon }, -- [2]
    { id = "shield", icon = shieldIcon }, -- [3]
}

When Roact updates the underlying ImageLabel objects, it will need to change their icons so that the item at [1] has the potion icon, the item at [2] has the sword icon, and a new ImageLabel is added at [3] with the shield icon.

We'd like for Roact to know that the new item was added at [1] and that the sword and shield items simply moved down in the list. That way it can adjust their LayoutOrder properties and let the Roblox UI system resolve the rest.

So let's fix it! We'll make our list of Item elements use the item's id for its keys instead of the indexes in the items list:

function Inventory:render()
    -- An Inventory contains a list of items
    local items = self.state.items

    local itemList = {}
    itemList["Layout"] = Roact.createElement("UIListLayout", {
        SortOrder = Enum.SortOrder.LayoutOrder,
        FillDirection = Enum.FillDirection.Vertical,
    })
    for i, item in ipairs(items) do
        -- Each element is now added at a stable key
        itemList[item.id] = Roact.createElement(Item, {
            layoutOrder = i,
            icon = item.icon,
        })
    end

    -- The Inventory renders a frame containing the list of Items as children
    return Roact.createElement("Frame", {
        Size = UDim2.new(0, 200, 0, 400)
    }, itemList)
end

Now the list of children is keyed by the stable, unique id of the item data. Their positions can change according to their LayoutOrder, but no other properties on the item need to be updated. When we add the third element to the list, Roact will set the LayoutOrder property on for each ImageLabel and only set the Image property on the newly added one!

Info

Switching to static keys might seem insignificant for this example, but if our Item component becomes more complicated and our inventory gets bigger, it can make a significant difference!