Let me take a few moments to share some optimization tips for building well-performing Feathers user interfaces. Many of these tips are suggested throughout the Feathers documentation already, but I wanted to bring them together into a single source — and add some additional thoughts.
These tips are in no particular order, and they’re not necessarily strict rules that you should never be willing to break. Too often, I see people advocating the complete avoidance of certain APIs or even core ActionScript language features simply because some blog had a benchmark that showed something may be slower than something else in extreme cases. Unless you see an actual bottleneck in your app, don’t allow yourself to throw out a perfectly good tool. For example, a number of these tips explain when to avoid ScrollContainer, but that doesn’t mean that you should never use ScrollContainer.
Note: These performance tips were last updated on February 20, 2018 for Feathers 3.4
Keep the display list shallow
As your display list gets deeper, your device’s CPU needs to work harder. Try to keep the display list as shallow as possible. Avoid nesting multiple levels of LayoutGroups and ScrollContainers when it’s not strictly necessary.
ScrollContainer is particularly expensive when nested because it requires two levels of depth all on its own. A ScrollContainer inside another a ScrollContainer is four levels, so the children of the inner ScrollContainer will automatically have a depth of five, instead of three.
Take a look at the diagram above, showing two display lists. On both sides, the item at the top is a container. On the left side, the display list is three levels deep, because there’s an inner container that has two children. On the right side, the display list is only two levels deep because the children at the deepest level could be moved up a level by using a different layout.
It’s often convenient to nest multiple containers so that you can alternate between HorizontalLayout and VerticalLayout. However, there may be a layout that allows you to use one container. For instance, AnchorLayout supports positioning a child of container relative to other children (or the edge of the container). This often makes it possible to achieve the same type of layout with only one container.
When using ScrollContainers, Lists, Panels, or anything else that supports scrolling, you often need to scroll in only one direction. These containers are optimized to skip large sections of code when they can be certain that they don’t need to scroll.
list.horizontalScrollPolicy = ScrollPolicy.OFF;
list.verticalScrollPolicy = ScrollPolicy.OFF;
Use the horizontalScrollPolicy and verticalScrollPolicy properties to control how scrolling works in each direction.
Use LayoutGroup instead of ScrollContainer if you don’t need scrolling at all
LayoutGroup is very similar to ScrollContainer because it allows you to apply layouts to its children, but it does not have the ability to scroll. It’s meant purely for layout. ScrollContainer requires a lot of extra measurement overhead to determine when scrolling is necessary, so LayoutGroup ends up running much less code when it validates.
Manually size and position objects when appropriate
While it’s not ideal, manual positioning and resizing children in a container will almost always perform much faster than one of the layout classes, especially if you only have a handful of children. It will require a bit more code, but it will also give some relief to your device’s CPU, which is certainly already doing quite a bit of work during every frame.
While built-in layout classes like HorizontalLayout, VerticalLayout, and TiledRowsLayout have been heavily optimized, they’re also full of code that needs to be around to handle edge cases that don’t always apply to every container. If you’re only positioning a few children instead of tens or hundreds, it’s sometimes in your best interest to skip the convenience that these classes provide.
The following code will certainly run much faster for a simple vertical layout than the 1000+ lines in the VerticalLayout class that implement many options that this layout doesn’t require:
var yPosition:Number = 0;
var gap:Number = 10;
var buttons:Vector.<FeathersControl> = new <FeathersControl>
[
startGameButton,
leaderboardsButton,
settingsButton
];
var buttonCount:int = buttons.length;
for(var i:int = 0; i < buttonCount; i++)
{
var button:FeathersControl = buttons[ i ];
button.y = yPosition;
// you may need to validate feathers controls to get the proper dimensions
button.validate();
yPosition += button.height + gap;
}
Try to use List instead of using ScrollContainer
It’s often tempting to quickly grab a ScrollContainer to group together some items so that they can scroll. However, you should always keep in mind that List is highly optimized for displaying many more items than ScrollContainer. List optimizes the display list to add only what is visible within its view port at any given scroll position. This is called layout virtualization, and we’re talking about the ability to display orders of magnitude more items than ScrollContainer. The performance difference starts manifesting with smaller sets of items, though. On slower mobile devices, you can often see a difference almost immediately after the container needs to scroll.
Don’t assume that a List’s item renderer can only to display a simple label and icon. The overall positioning of the item renderers themselves is handled inside the List, but all of the layout inside each item renderer is completely out of the List’s control.
A custom item renderer will let you add any number of children to an item renderer, and you can customize the layout completely. The easiest way to create a custom item renderer is to extend one of the LayoutGroup item renderer classes. However, if you need something at a lower level, you can implement the IListItemRenderer interface from scratch.
Optimize your List’s data provider and the default item renderer
As we learned above, item renderers are reused by List as it scrolls. This means that a single item renderer may display many different items over a short period of time while the List is scrolling. In order to keep that scrolling smooth, we want to avoid creating a lot of new objects. We don’t want the garbage collector using up our precious CPU cycles and causing stuttering from dropped frames.
In most cases, you probably don’t need to put Images or ImageLoaders into your List’s data provider to display a texture in the icon or accessory. Adding and removing those display objects over and over can affect performance. You can have the item renderer manage an ImageLoader instance automatically, and your data provider can contain the textures. Instead of using iconField or accessoryField to point to display objects, point to the textures with iconSourceField and accessorySourceField instead. If you need to change properties on the ImageLoader, you can customize iconLoaderFactory and accessoryLoaderFactory.
function itemRendererIconLoaderFactory():ImageLoader
{
var loader:ImageLoader = new ImageLoader();
// the ImageLoader will auto-size, but you can set dimensions manually
loader.setSize( 100, 100 );
return loader;
}
function itemRendererFactory():IListItemRenderer
{
var itemRenderer:DefaultListItemRenderer = new DefaultListItemRenderer();
itemRenderer.iconSourceField = "texture";
itemRenderer.iconLoaderFactory = itemRendererIconLoaderFactory;
return itemRenderer;
}
list.dataProvider = new ListCollection(
[
{ label: "Milk", texture: atlas.getTexture("milk-thumb") },
{ label: "Eggs", texture: atlas.getTexture("eggs-thumb") },
{ label: "Bread", texture: atlas.getTexture("bread-thumb") },
{ label: "Apples", texture: atlas.getTexture("apples-thumb") }
]);
list.itemRendererFactory = itemRendererFactory;
Additionally, you can manage the icon or the accessory as a label in the same way. You can use iconLabelField or accessoryLabelField to point to a String in each item in the data provider, and a text renderer will be automatically managed by the item renderer. The iconLabelFactory or the accessoryLabelFactory can be customized to change properties on the text renderer.
You may be aware that properties like iconFunction and accessoryFunction can be used as alternatives to iconField or accessoryField. If getting the icon or accessory needs to be more complicated than accessing a field on the item, you can have the item renderer pass its item to a function to do more complicated processing. These functions provide a lot of convenience, but it’s extremely easy to hurt performance with them too.
Try to avoid using these functions at all for best results, but if you have no choice, do your best to avoid creating new objects in these functions. As I already mentioned, they will be called often as the list scrolls and each item renderer’s data changes. If you return a new object every time an iconFunction is called, the old icon will be removed from the display list and set to null, setting it up for garbage collection. That’s a lot of icons being needlessly garbage collected as the list scrolls when it’s often possible to reuse your icon and accessory and simply change its properties.
A Dictionary could be used to associate each item in the data provider with the object returned by an iconFunction or an accessoryFunction:
private var accessoryDictionary:Dictionary = new Dictionary( true );
private function myList_accessoryFunction( item:Object ):void
{
var check:Check = this.accessoryDictionary[ item ];
if(!check)
{
check = new Check();
this.accessoryDictionary[ item ] = check;
}
return check;
}
The function above creates a Check control for each item in the data provider, but only if one hasn’t already been created. Please note that this code doesn’t listen for Event.CHANGE, but you can certainly add event listeners and set other properties, as needed.
One thing to watch out for: any display objects returned by iconFunction and accessoryFunction will not be disposed by the List. You will need to dispose them manually. For instance, with the example code above, you might loop through the keys of the Dictionary and dispose each of the Check components in an override of dispose().
override public function dispose():void
{
for(var item:Object in this.accessoryDictionary)
{
var check:Check = item as Check;
check.dispose();
}
super.dispose();
}
Choose the right component for your root class
It’s often the first instinct to simply extend starling.display.Sprite when you create your Starling root class, but that usually only makes your display list deeper with no benefit. If the real root of your app is a StackScreenNavigator, extend that instead.
public class Main extends StackScreenNavigator
{
}
Note: If you need the root component to be skinned by a theme, you can instantiate the theme before you call super() in the constructor.
public function Main()
{
new MetalWorksMobileTheme();
super();
}
Disable masks when they’re not required
Many components like Panel and ScrollContainer will use a mask to ensure that children are not displayed outside of the container’s bounds. This is often required, but it’s worth noting that masks can add some performance overhead because it requires an extra draw call. With that in mind, you should avoid clipping when it’s not strictly necessary.
In some situations, a container like PanelScreen will fill the entire width and height of the stage. There’s no way for the children of this container to be accidentally revealed because Starling never renders outside the bounds of its stage. Masking this container would add extra overhead with no benefit, so its mask can be safely disabled. Usually, Feathers components have a clipContent property will control this behavior. Set this property to false to disable the mask.
Avoid invalidating a component while it validates
If you subclass a Feathers component, you might override a function like draw() or commitData() that is called while the component validates. If you call invalidate() in your function (or if you set a property that results in invalidate() being called), the component will need to validate again during the next cyle. In the best case, it will only need to happen once. In the worst case, you may accidentally cause the component to validate again and again (every single frame, infinitely), even if it doesn’t need to. As you can imagine, extra code running every single frame can potentially hurt performance quite a bit.
Often times, there is a better place to change the property. If you only need to change this property once, you may be able to override initialize() instead, or you may be able to do it in the constructor. Another place to consider changing this property is wherever you instantiate the component. Finally, if you’re using a theme, you may want to change the property there, assuming that it changes the visual appearance of the component in some way.
If you absolutely need to change a property during validation, it’s possible to do that without completely invalidating the component. Generally, every property on a Feathers component has a getter, a setter, and a protected variable that stores the value. The setter changes the variable, and then it calls invalidate() with a flag to indicate what has changed. When you override draw() or another function, instead of changing the property with its setter, change the protected variable directly. After that, call the setInvalidationFlag() function instead of the invalidate() function. This function is designed to store the flag, without queuing up the component for validation again. Now, when you call super.draw(), the superclass will be able to see that you’ve changed the property, but the component won’t need to validate again.
Mobile requires more optimization than desktop
After several years of developing apps and games with Adobe AIR on mobile, I’ve become much more focused on performance with my approach to creating user interfaces. Gone are the days of doing almost anything I wanted because I knew that a desktop CPU could handle it. Parts of Feathers have default optimizations that would have never mattered back in the days that I developed Flex apps for desktop computers. Even with all of the performance work I put into the library, every individual app that you create will still benefit from careful understanding of what does and what doesn’t work well on mobile, and you should always think carefully about what’s going to have the least CPU impact.
While many of the tips above focus on reducing CPU load, I highly encourage you to understand what can be done to reduce the GPU’s load as well. If you’ve never worked with the GPU before starting with Starling and Feathers, it’s absolutely essential that you understand things like minimizing state changes, using texture atlases, and many of the other points mentioned in the Starling Performance Optimization documentation. Know that stuff inside and out because while the GPU can bring you much greater performance, it’s also very easy to ask it to do something it can’t handle very well, and that will absolutely destroy performance instead.