Performance Optimization
1. Memory Optimization Methods
1.1 Optimize Memory Through Object Pooling
Object pooling is one of the most important optimization techniques in game development and a key factor affecting performance. In games, many objects are constantly being created and destroyed, such as bullets fired by characters, visual effects appearing and disappearing, and NPCs being removed or respawned. These frequent creations and deletions can be costly, especially when the number of objects is large. The object pool technique effectively solves this problem — when an object is removed, it is recycled into the pool, and when a new object is needed, it is retrieved from the pool.
The advantages are:
- Reduces the overhead of instantiation.
- Reuses objects to minimize memory allocation and garbage collection operations.
Note: When an object is removed, it is not immediately deleted from memory. Garbage collection only occurs when the system determines that memory is low. This process can cause lag because it consumes a lot of CPU resources. By using an object pool, you can reduce the number of temporary objects and significantly improve performance and stability.
For more information, see Object Pool.
1.2 Releasing Memory
JavaScript runtime cannot manually trigger garbage collection. To ensure an object can be collected, all references to it must be removed.
The destroy()
method of Sprite
helps by setting internal references to null
.
Example:
// Create a Sprite instance
var sp: Laya.Sprite = new Laya.Sprite();
// Destroy and clear internal references
sp.destroy();
Setting an object to null
does not immediately delete it. The garbage collector runs only when the system considers memory low.
Garbage collection consumes CPU and can affect performance. Reusing objects helps reduce GC pressure.
You should also set unnecessary references to null
to make GC faster.
Sometimes, when two objects reference each other, GC must scan to detect unreferenced objects, which is slower than reference counting.
1.3 Resource Unloading
During gameplay, many assets are loaded. These should be released once they are no longer needed, or they will remain in memory.
Example:
var assets: Array<any> = [];
assets.push("resources/apes/monkey0.png");
assets.push("resources/apes/monkey1.png");
assets.push("resources/apes/monkey2.png");
assets.push("resources/apes/monkey3.png");
Laya.loader.load(assets).then(() => {
for (var i: number = 0, len: number = assets.length; i < len; ++i) {
var asset: string = assets[i];
// Before clearing, resource exists in memory
console.log(Laya.loader.getRes(asset));
// Clear resource
Laya.loader.clearRes(asset);
// After clearing, resource is released
console.log(Laya.loader.getRes(asset));
}
});
1.4 Filters and Masks
Try to minimize the use of filters. Applying filters such as BlurFilter
and GlowFilter
to display objects causes two bitmaps to be created in memory.
Each bitmap matches the display object’s size — the first is a rasterized version, and the second is the filtered output:
(Figure 1-1)
When you modify a filter or the object itself, both bitmaps are recalculated, consuming memory and CPU.
In contrast, ColorFilter
has negligible GPU cost under WebGL.
Best practice: use image-editing tools to pre-bake filter effects into static bitmaps. Avoid runtime filter generation to reduce CPU/GPU load — especially for static graphics that won’t change.
2. Rendering Optimization
2.1 Optimizing Sprites
- Minimize unnecessary nesting and the number of Sprites.
- Remove or hide objects outside the visible area (
visible = false
). - Use
cacheAs
on containers with static content to reduce draw calls. Separate dynamic and static elements. - Objects outside a
Panel
’s visible area are not rendered, so they don’t consume performance.
2.2 Optimizing Draw Calls
- Use
cacheAs
on complex static content to reduce draw calls. - Ensure that images from the same atlas are rendered consecutively — interleaving different atlases increases draw calls.
- Use a single atlas for all resources in one panel when possible.
2.3 Optimizing Canvas
Avoid using cacheAs
in the following cases:
- Simple objects (like a single character or image) — caching won’t help.
- Containers with frequently changing content (like animations or timers).
You can check the first value in the Canvas statistics to see if it’s constantly redrawing.
2.4 CacheAs
cacheAs
can cache a display object as a static image. When a child changes, the cache updates automatically, or you can manually call reCache()
.
Cache modes:
"none"
— no caching (default)."normal"
— command caching."bitmap"
— renderTarget caching (WebGL only, limited to 2048x2048).
Setting staticCache = true
prevents auto updates; call reCache()
manually.
Benefits:
- Reduces node traversal and vertex calculations.
- Reduces draw calls.
Example (10,000 texts):
class Test {
private text: Laya.Text;
constructor() {
Laya.init(550, 400, Laya.WebGL);
Laya.Stat.show();
var textBox = new Laya.Sprite();
for (var i = 0; i < 10000; i++) {
this.text = new Laya.Text();
this.text.text = (Math.random() * 100).toFixed(0);
this.text.color = "#CCCCCC";
this.text.x = Math.random() * 550;
this.text.y = Math.random() * 400;
textBox.addChild(this.text);
}
Laya.stage.addChild(textBox);
}
}
Without cache: FPS ~52
With cache (textBox.cacheAs = "bitmap";
): FPS ~60
(Figure 2-2)
2.5 Text Stroke
Outlined text requires an extra draw call, doubling CPU cost. Alternatives:
- Use
cacheAs
for static text. - Use bitmap fonts for frequently changing text.
2.6 Skip Text Layout (Direct Render)
If text layout is unnecessary (e.g., single-line), use changeText()
to update directly without layout recalculation:
this.text.text = "text";
Laya.stage.addChild(this.text);
this.text.changeText("text changed.");
Conditions:
- Single-line only.
- No style changes (color, font weight, alignment, etc.).
3. Reducing CPU Usage
3.1 Minimize Dynamic Property Lookups
Frequent property lookups are costly. Cache them in local variables:
foo() {
var prop = this.target.prop;
this.process1(prop);
this.process2(prop);
this.process3(prop);
}
3.2 Recycle Performance-Heavy Operations
Avoid running unnecessary loops or timers when not in use.
Example:
Laya.timer.frameLoop(1, this, this.animateFrameRateBased);
Laya.stage.on("click", this, this.dispose);
dispose() {
Laya.timer.clear(this, this.animateFrameRateBased);
}
Always clear timers when objects are destroyed.
3.3 Getting Display Object Bounds
getBounds()
orgetGraphicBounds()
— calculates boundaries (not efficient if called frequently).- Set
autoSize = true
— auto-calculates size when children change (not suitable for containers with many children). - Use
size(width, height)
manually for best performance.
3.4 Adjust Frame Rate by Activity
Stage.FRAME_FAST
: Full frame rate (e.g., 60 or 120).Stage.FRAME_SLOW
: Half the display refresh rate.Stage.FRAME_MOUSE
: Switches between fast and slow based on mouse activity.
When using FRAME_MOUSE
, FPS increases during interaction and drops when idle.
3.5 Using callLater
callLater()
defers function execution to the end of the current frame, avoiding redundant updates.
Example:
Laya.timer.callLater(this, update);
Combining multiple property changes will now trigger update()
only once instead of multiple times.
3.6 Image/Atlas Loading
Processing many images or atlases at once can cause lag. Load resources in groups (by level, scene, etc.) and unload unused ones to free memory.
4. Other Optimization Strategies
4.1 Limit Particle Effects
Particles consume CPU when drawn as vectors. WebGL uses GPU acceleration, but excessive particles still impact performance, especially on mobile.
4.2 Limit Rotation, Scaling, and Alpha
These properties consume performance. WebGL reduces the cost, but minimizing usage is still beneficial.
4.3 Avoid Creating Objects or Heavy Computation Inside Timers
Laya.timer.loop()
and frameLoop()
run continuously, so avoid creating objects or running complex logic inside them.
4.4 Avoid Excessive autoSize and getBounds
Both require computation and impact performance — use sparingly.
4.5 Avoid try-catch Inside Hot Functions
Functions wrapped in try-catch
run significantly slower.
5. Using Chrome Performance Profiler
You can access the Chrome DevTools Profiler via right-click → “Inspect” or pressing F12
.
5.1 CPU Usage Analysis
Start Profiling:
Select Record JavaScript CPU Profile
, then click Start.
(Figure 5-1)
Stop Profiling:
Click Stop to finish.
(Figure 5-2)
View Results:
CPU usage will appear as a function list, where you can identify costly functions.
(Figure 5-3)
5.2 Memory Usage Analysis
Start Memory Snapshot:
Select Take Heap Snapshot
and click Take Snapshot.
(Figure 5-4)
Snapshot Records:
Snapshots will appear in the sidebar. Click to view object counts and memory sizes.
(Figure 5-5)
Compare Snapshots:
Take multiple snapshots and compare them using Comparison mode to identify changes in memory usage.
(Figure 5-6)
(Figure 5-7)
6. Using Texture Compression
Benefits:
- Reduces memory usage — crucial for mobile devices.
- Reduces GPU bandwidth usage — improves rendering performance and reduces overheating.
For details, see Texture Compression.