Performance and Optimization

As you develop your application, you should always consider how your design choices affect performance. Despite ongoing core improvements, mobile devices still face fundamental constraints in processing power, memory usage, and battery life. Therefore, performance and optimization is crucial to achieving faster response time, minimizing memory usage, and maximizing battery life.

Using Memory Efficiently

Memory is a critical resource on mobile devices. Some devices may even terminate your application if you consume too much memory.

  1. Eliminate memory leaks — your application should not have memory leaks. Although Lua handles memory management and cleanup automatically, memory leaks can still occur. For example, global variables are never considered garbage; it's up to you to tell Lua that these variables are garbage by setting them to nil. Global variables are not recommended in general, but if you must use them for convenience, ensure that you remove them from memory when they're no longer needed.

  2. Keep resource files as small as possible — resource files used by your application typically reside on the disk. They must be loaded into memory before they can be used. Images and audio files should be as small as possible. You should also reuse the same image assets whenever it's feasible. For example, if you design a flip-style menu with the same background image for each page, you should use that image on all pages and layer the necessary foreground images over it. For more details, please see Conserving Texture Memory below.

  3. Load resources lazily — avoid loading resource files until they're actually needed. While pre-loading resource files might seem like good practice, this can actually backfire because of how devices respond to low memory situations. One notable exception is audio files. In general, you should load sound effects for a particular scene or level before it begins, because loading them on demand can cause a slight skip during time-critical code. For more information, see Managing Audio below.

  4. Remove objects from the display hierarchy — when a display object is created, it is implicitly added to the display hierarchy. When you no longer need a display object, you should remove it from the display hierarchy and set its reference to nil. This makes the object eligible for garbage collection. However, this is no guarantee that the object will be removed from memory. If other variables in memory reference the display object, Lua will not consider it garbage. See the Display Objects guide for more information on removing objects.

Reducing Power Consumption

Battery life is inherently limited on mobile devices because of their small form factor. You can improve battery life by adhering to the following practices.

Network access consumes a considerable amount of power. You can minimize the impact of network traffic by following these guidelines:

  1. Do not "poll"; instead, connect to external network servers only when necessary.

  2. Optimize the data that you transmit so it's as small as possible.

  3. Transmit in bursts. More power is consumed the longer the network is actively transmitting data. It's better to transmit data in bursts rather than spreading it out into smaller transmission packets over time.

GPS and accelerometer hardware also consumes power. If you access location data via GPS, stop collecting it when you have the data you need. If you use the accelerometer, try to limit it to scenes where it's essential for the desired functionality.

Disk access — reading and writing files to the device's local disk — should be handled similarly to network access. It's better to transmit larger packets of data to/from the disk rather than spreading it out over numerous smaller transactions.

Transitions and Animations

If you need to set or transition a specific property of several display objects to the same value — for example, fade an entire overlay menu to alpha=0 — it's better to add the objects to a display group and modify the property of the entire group. It's easier to code and it optimizes memory and speed. See the Group Programming guide for more information.

If you're using sprite animations, a common oversight is allowing offscreen or invisible sprites to continue animating. While these sprites may not be visible to the user, they'll continue to use processor power while animating. We suggest that you pause all animations that move off the screen or otherwise become inactive.

Conserving Texture Memory

Texture memory is often ignored until it reaches "critical mass," at which point it's time-consuming to make the required changes to art assets.

As general practice, remember these tips in respect to managing texture memory:

  1. Always unload textures (remove them from the display hierarchy) when they're no longer needed.

  2. If you're using image sheets, consider using a tool like TexturePacker to pack your images into the smallest configuration possible.

Important

There is a limit on the maximum texture size that a device will support. If you exceed this limit, the texture will automatically downscale to fit within the maximum. You can use the system.getInfo( "maxTextureSize" ) command to determine the maximum texture size for a particular device. See system.getInfo() for more information.

Draw Calls / Batching

On devices, OpenGL performs best when you are able to minimize state changes. This is because multiple objects can be batched into a single draw call if there are no state changes required between consecutive display objects.

Corona's rendering engine attempts to identify situations where multiple display objects can be submitted in a single draw call. Whenever possible, you should try to arrange the display object hierarchy such that consecutive display objects — meaning, the order in which they are rendered — can be batched into a single draw call.

There are certain situations where this can occur. The general rule is that consecutive display objects which use the same texture can be batched. This includes display objects that use different frames from the same image sheet, since the underlying texture is the same. In these situations, you can vary the position, tint, and alpha of each object without breaking the batch, but remember that some actions may prevent batching, for example adding a shader effect to an object.

Lua Optimizations

At the code level, you should adhere to as many Lua optimizations as possible. Most of the performance tricks below pertain primarily to time-critical routines — that is, points in your app where there is a lot happening or where the user experience could be adversely affected by sluggish performance. However, every bit helps and we suggest that you follow these as a habit.

Localize, Localize

While avoiding global variables and functions isn't always possible across the board, minimal usage is the best practice. Access to local variables and functions is simply faster, especially in time-critical routines.

--NON-LOCAL (DISCOURAGED)
CCX = display.contentCenterX  --global variable
for i = 1,100 do
    local image = display.newImage( "myImage" )
    image.x = CCX
end

--LOCAL (RECOMMENDED)
local CCX = display.contentCenterX  --local variable
for i = 1,100 do
    local image = display.newImage( "myImage" )
    image.x = CCX
end

This also applies to core Lua libraries like the math library. In time-critical routines, you should always localize library functions.

--NON-LOCAL (DISCOURAGED)
local function foo( x )
    for i = 1,100 do
        x = x + math.sin(i)
    end
    return x
end

--"EXTERNAL" LOCAL (RECOMMENDED)
local sin = math.sin  --local reference to 'math.sin'
local function foo(x)
    for i = 1,100 do
        x = x + sin(i)
    end
    return x
end

Finally, remember that functions should be localized whenever possible. Of course, this will require proper scoping.

--NON-LOCAL (DISCOURAGED)
function func1()
   func2( "myValue" )
end

function func2( y )
   print( y )
end

func1()

--LOCAL (RECOMMENDED)
local function func2( y )  --'func2' properly scoped above 'func1'
   print( y )
end

local function func1()
   func2( "myValue" )
end

func1()

Avoid "table.insert()"

Let's compare four methods that all achieve the same thing: the common act of inserting values into a table. Of the four, the Lua table.insert() function is a mediocre performer and should be avoided.

--table.insert() (DISCOURAGED)
local a = {}
local table_insert = table.insert

for i = 1,100 do
   table_insert( a, i )
end

--LOOP INDEX METHOD (RECOMMENDED)
local a = {}

for i = 1,100 do
    a[i] = i
end

--TABLE SIZE METHOD (ACCEPTABLE)
local a = {}

for i = 1,100 do
   a[#a+1] = i
end

--COUNTER METHOD (RECOMMENDED)
local a = {}
local index = 1

for i = 1,100 do
   a[index] = i
   index = index+1
end

Avoid "unpack()"

The Lua unpack() function is not a great performer. Fortunately, a simple loop can be written to accomplish the same thing.

--unpack() (DISCOURAGED)
local a = { 100, 200, 300, 400 }

for i = 1,100 do
   print( unpack(a) )
end

--LOOP METHOD (RECOMMENDED)
local a = { 100, 200, 300, 400 }

for i = 1,100 do
   print( a[1],a[2],a[3],a[4] )
end

The caveat is that you must know the length of the table to retrieve all of its values in the loop method. Thus, unpack() still has its uses — in a table of unknown length, for example — but it should be avoided in time-critical routines.

Avoid "ipairs()"

When iterating through a table, the overhead of the Lua ipairs() function does not justify its use, especially when you can accomplish the same thing using a Lua construct.

--ipairs() (DISCOURAGED)
local t1 = {}
local t2 = {}
local t3 = {}
local t4 = {}
local a = { t1, t2, t3, t4 }

for i,v in ipairs( a ) do
   print( i,v )
end

--LUA CONSTRUCT (RECOMMENDED)
local t1 = {}
local t2 = {}
local t3 = {}
local t4 = {}
local a = { t1, t2, t3, t4 }

for i = 1,#a do
   print( a[i] )
end

Math Performance

Certain mathematical functions and processes are faster than others. For example, multiplication is faster than division and you should multiply by a decimal instead of dividing.

--MULTIPLICATION BY DECIMAL (RECOMMENDED)
x * 0.5 ; y * 0.125

--DIVISION (DISCOURAGED)
x/2 ; y/8

Multiplication is also faster than exponentiation:

--MULTIPLICATION (RECOMMENDED)
x * x * x

--EXPONENTIATION (DISCOURAGED)
x^3

Finally, avoid math.fmod() for positive numbers and use the modulus operator instead:

--math.fmod() (DISCOURAGED)
local fmod = math.fmod
for i = 1,100 do
   if ( fmod( i,30 ) < 1 ) then
      local x = 1
   end
end

--MODULUS OPERATOR (RECOMMENDED)
for i = 1,100 do
   if ( ( i%30 ) < 1 ) then
      local x = 1
   end
end

Managing Audio

Sound effects for an app should almost always be pre-loaded in non-time-critical code, for example, before a scene or level begins. Additionally, you should compress/sample sounds to the smallest acceptable quality in most cases. 11khz mono (not stereo) is considered acceptable in most cases, since the user will likely be listening through the phone/tablet speaker or earbuds. Also, using simple, cross-platform formats like WAV do not tax the CPU heavily.

If desired, sound effects can be organized in a table as follows, for easy reference and eventual disposal.

local soundTable = {
   mySound1 = audio.loadSound( "a.wav" ),
   mySound2 = audio.loadSound( "b.wav" ),
   mySound3 = audio.loadSound( "c.wav" ),
   mySound4 = audio.loadSound( "d.wav" ),
   mySound5 = audio.loadSound( "e.wav" ),
   mySound6 = audio.loadSound( "f.wav" ),
   mySound7 = audio.loadSound( "g.wav" ),
   mySound8 = audio.loadSound( "h.wav" ),
}

With this structure, playback is as simple as:

local mySound = audio.play( soundTable["mySound1"] )

Remember, you must dispose of audio files when they're no longer needed and clear any references to them.

local ST = soundTable

for s=#ST,1,-1 do
    audio.dispose( ST[s] ) ; ST[s] = nil
end