How to maximize HTML5 performance

How to maximize HTML5 performance

HTML5 is becoming more and more popular as an emerging field. However, in the context of mobile device hardware performance being weaker than PC, the demand for performance becomes more important. There is a huge difference between HTML5 performance before and after optimization, and few people know how to optimize to improve performance. This article takes the LayaAir engine as an example and explains in detail how to use the engine to optimize the performance of HTML5 through code examples.

[[205464]]

Topics include:

  • Code Execution Basics
  • Benchmarks
  • Memory optimization
  • Graphics rendering performance
  • Reduce CPU usage
  • Other optimization strategies

Section 1: Code Execution Basics

LayaAir engine supports AS3, TypeScript, and JavaScript development. However, no matter which development language is used, JavaScript code is ultimately executed. All the images you see are drawn by the engine, and the update frequency depends on the FPS specified by the developer. For example, if the frame rate is specified to be 60FPS, the execution time of each frame at runtime is one sixtieth of a second. Therefore, the higher the frame rate, the smoother the visual feeling. 60 frames is a full frame.

Since the actual operating environment is in the browser, the performance also depends on the efficiency of the JavaScript interpreter. The specified FPS frame rate may not be achieved in a low-performance interpreter, so this part is not something that developers can decide. What developers can do is to improve the FPS frame rate in low-end devices or low-performance browsers through optimization as much as possible.

The LayaAir engine redraws every frame. When optimizing performance, in addition to paying attention to the CPU consumption caused by executing logic code in each frame, you also need to pay attention to the number of drawing instructions called per frame and the number of texture submissions to the GPU.

Section 2: Benchmarks

The performance statistics tool built into the LayaAir engine can be used for benchmarking and real-time detection of current performance. Developers can use the laya.utils.Stat class to display the statistics panel through Stat.show(). The specific code is shown in the following example:

  • Stat.show(0,0); //AS3 panel call writing method
  • Laya.Stat.show(0,0); //Panel call writing method of TS and JS

Canvas rendering statistics:

Statistics for WebGL rendering:

The significance of statistical parameters:

FPS:

  • The number of frames rendered per second (higher numbers are better).
  • When using canvas rendering, the description field is displayed as FPS(Canvas), and when using WebGL rendering, the description field is displayed as FPS(WebGL).

Sprites:

  • The number of render nodes (lower numbers are better).
  • Sprite counts all rendering nodes (including containers). The size of this number affects the number of engine node traversal, data organization and rendering.

DrawCall:

  • DrawCall has different meanings in canvas and WebGL rendering (the fewer the better).
  • Canvas indicates the number of times each frame is drawn, including pictures, text, and vector graphics. Try to limit it to less than 100.
  • In WebGL, rendering submission batches are represented. Each process of preparing data and notifying the GPU to render is called a DrawCall. In each DrawCall, in addition to the time-consuming process of notifying the GPU to render, switching materials and shaders is also a very time-consuming operation. The number of DrawCalls is an important indicator of performance, and should be limited to less than 100 as much as possible.

Canvas:

Three values ​​- the number of canvases redrawn per frame / the number of canvases with a cache type of "normal" / the number of canvases with a cache type of "bitmap".

CurMem: WebGL rendering only, indicates the memory usage and video memory usage (the lower the better).

Shader: For WebGL rendering only, this indicates the number of shader submissions per frame.

Whether in Canvas mode or WebGL mode, we need to focus on the three parameters of DrawCall, Sprite, and Canvas, and then optimize them accordingly. (See "Graphics Rendering Performance")

Section 3: Memory Optimization

Object Pool

Object pooling involves the constant reuse of objects. A certain number of objects are created during the initialization of the application and stored in a pool. When an operation is completed on an object, the object is returned to the pool and can be retrieved when a new object is needed.

Since instantiating objects is expensive, using an object pool to reuse objects can reduce the need to instantiate objects. It can also reduce the chances of the garbage collector running, thereby increasing the running speed of the program.

The following code demonstrates the use of

Laya.utils.Pool:

  1. ar SPRITE_SIGN = 'spriteSign' ;
  2. var sprites = [];
  3. function initialize()
  4. {
  5. for (var i = 0; i < 1000; i++)
  6. {
  7. var sp = Pool.getItemByClass(SPRITE_SIGN, Sprite)
  8. sprites.push(sp);
  9. Laya.stage.addChild(sp);
  10. }
  11. }
  12. initialize();

Create an object pool of size 1000 in initialize.

The following code removes all display objects from the display list when the mouse is clicked and reuses them for other tasks later:

  1. Laya.stage. on ( "click" , this, function ()
  2. {
  3. var sp;
  4. for (var i = 0, len = sprites.length; i < len; i++)
  5. {
  6. sp = sprites.pop();
  7. Pool.recover(SPRITE_SIGN, sp);
  8. Laya.stage.removeChild(sp);
  9. }
  10. });

After calling Pool.recover, the specified object will be recycled into the pool.

Using Handler.create

During the development process, Handler is often used to complete asynchronous callbacks. Handler.create uses built-in object pool management, so Handler.create should be used to create callback processors when using Handler objects. The following code uses Handler.create to create a loaded callback processor:

  1. Laya.loader. load (urls, Handler. create (this, onAssetLoaded));

In the above code, after the callback is executed, the Handler will be reclaimed by the object pool. At this point, consider what happens in the following code:

  1. Laya.loader. load (urls, Handler. create (this, onAssetLoaded), Handler. create (this, onLoading));

In the above code, the handler returned by Handler.create is used to handle the progress event. At this time, the callback is executed once and then recycled by the object pool, so the progress event is only triggered once. At this time, the four parameters named once need to be set to false:

  1. Laya.loader. load (urls, Handler. create (this, onAssetLoaded), Handler. create (this, onLoading, null , false ));

Freeing up memory

The JavaScript runtime cannot start the garbage collector. To ensure that an object can be recycled, delete all references to the object. The destroy function provided by Sprite will help set internal references to null.

For example, the following code ensures that the object can be garbage collected:

  1. var sp = new Sprite();
  2. sp.destroy();

When an object is set to null, it is not immediately removed from memory. The garbage collector runs only when the system deems that the memory is low enough. Memory allocation (not object deletion) triggers garbage collection.

Garbage collection can be CPU intensive and affect performance. Try to limit the use of garbage collection by reusing objects. Also, set references to null whenever possible so that the garbage collector spends less time looking for objects. Sometimes (such as when two objects reference each other) it is not possible to set both references to null at the same time, and the garbage collector will scan for unreachable objects and clear them, which can be more expensive than reference counting.

Resource Unloading

When the game is running, many resources will always be loaded. These resources should be unloaded in time after use, otherwise they will remain in the memory.

The following example demonstrates loading a resource and comparing the resource status before and after unloading:

  1. var assets = [];
  2. assets.push( "res/apes/monkey0.png" );
  3. assets.push( "res/apes/monkey1.png" );
  4. assets.push( "res/apes/monkey2.png" );
  5. assets.push( "res/apes/monkey3.png" );
  6.  
  7. Laya.loader. load (assets, Handler. create (this, onAssetsLoaded));
  8.  
  9. function onAssetsLoaded()
  10. {
  11. for (var i = 0, len = assets.length; i < len; ++i)
  12. {
  13. var asset = assets[i];
  14. console.log(Laya.loader.getRes(asset));
  15. Laya.loader.clearRes(asset);
  16. console.log(Laya.loader.getRes(asset));
  17. }
  18. }

About filters and masks

Try to minimize the use of filter effects. When you apply filters (BlurFilter and GlowFilter) to a display object, the runtime creates two bitmaps in memory. Each of these bitmaps is the same size as the display object. The first bitmap is created as a rasterized version of the display object, and then used to generate the other bitmap with the filter applied:


Two bitmaps in memory when the filter is applied

When you modify a property of a filter or display object, both bitmaps in memory are updated to create the resulting bitmap, which can take up a lot of memory. In addition, this process involves CPU calculations, which can degrade performance when dynamically updated (see "Graphics Rendering Performance - About cacheAs").

ColorFiter needs to calculate each pixel under Canvas rendering, while the GPU consumption under WebGL is negligible.

As a best practice, use bitmaps created with image authoring tools to simulate filters whenever possible. Avoiding dynamic bitmap creation at runtime can help reduce CPU or GPU load, especially for images that have filters applied and will not be modified.

Section 4: Graphics Rendering Performance

Optimizing Sprites

  1. Try to reduce unnecessary nested levels and the number of Sprites.
  2. Objects in non-visible areas should be removed from the display list or set visible=false.
  3. For containers with a lot of static content or content that doesn't change often (such as buttons), you can set the cacheAs property for the entire container, which can greatly reduce the number of sprites and significantly improve performance. If there is dynamic content, it is best to separate it from the static content so that only the static content can be cached.
  4. In a panel, direct child objects outside the panel area (child objects of child objects cannot be determined) will not be rendered, and child objects outside the panel area will not generate any consumption.

Optimize DrawCall

  1. Setting cacheAs for complex static content can greatly reduce DrawCall. Using cacheAs properly is the key to game optimization.
  2. Try to ensure that images in the same atlas are rendered in sequence. If different atlases are cross-rendered, the number of DrawCalls will increase.
  3. Try to use the same atlas for all resources in the same panel to reduce the number of submission batches.

Optimizing Canvas

When optimizing Canvas, we need to be careful not to use cacheAs in the following situations:

  1. If the object is very simple, such as a word or an image, setting cacheAs=bitmap will not improve performance but will actually reduce performance.
  2. If there is frequently changing content in the container, such as an animation or countdown, setting cacheAs=bitmap for the container will result in performance loss.

You can check the first value of the Canvas statistics to determine whether the Canvas cache is being refreshed.

About cacheAs

Setting cacheAs can cache the display object as a static image. When cacheAs is used, if the child object changes, it will be automatically re-cached. You can also manually call the reCache method to update the cache. It is recommended to cache complex content that does not change frequently as a static image, which can greatly improve rendering performance. cacheAs has three optional values: "none", "normal" and "bitmap".

  1. The default is "none", no caching is done.
  2. When the value is "normal", canvas caching is performed in canvas mode and command caching is performed in webgl mode.
  3. When the value is "bitmap", canvas cache is still used in canvas mode, and renderTarget cache is used in webGL mode. It should be noted that the renderTarget cache mode in webGL has a size limit of 2048. Exceeding 2048 will increase memory overhead. In addition, the overhead is also relatively large when redrawing continuously, but it will reduce drawcalls and have the highest rendering performance. The command cache mode in webGL will only reduce node traversal and command organization, but will not reduce drawcalls, and the performance is medium.

After setting cacheAs, you can also set staticCache=true to prevent automatic cache updates. You can also manually call the reCache method to update the cache.

cacheAs improves performance in two ways: first, reducing node traversal and vertex calculations; second, reducing drawCalls. Making good use of cacheAs will be a powerful tool for engine performance optimization.

The following example draws 10,000 texts:

  1. Laya.init(550, 400, Laya.WebGL);
  2. Laya.Stat.show();
  3.  
  4. var textBox = new Laya.Sprite();
  5.  
  6. var text;
  7. for (var i = 0; i < 10000; i++)
  8. {
  9. text = new Laya.Text();
  10. text.text = (Math.random() * 100).toFixed(0);
  11. text.color = "#CCCCCC" ;
  12.  
  13. text.x = Math.random() * 550;
  14. text.y = Math.random() * 400;
  15.  
  16. textBox.addChild(text);
  17. }
  18.  
  19. Laya.stage.addChild(textBox);

Below is a screenshot of the runtime on my computer, with the FPS stable at around 52.

When we set the container containing the text to cacheAs, as shown in the example below, the performance is greatly improved and the FPS reaches 60 frames.

  1. // ...omit other code... var textBox = new Laya.Sprite();
  2. textBox.cacheAs = "bitmap" ; // ...omit other code...

Text stroke

At runtime, text with strokes calls one more drawing instruction than text without strokes. At this time, the amount of CPU usage of text is proportional to the amount of text. Therefore, try to use alternative solutions to achieve the same requirements.

For text content that rarely changes, you can use cacheAs to reduce performance overhead. See “Graphics Rendering Performance – About cacheAs”.

For text fields whose content changes frequently but use a small number of characters, you can choose to use bitmap fonts.

Skip text layout and go straight to rendering

In most cases, many texts do not require complex typesetting, and just display a line of text. To meet this requirement, Text provides a method called changeText that can directly skip typesetting.

  1. var text = new Text();
  2. text.text = "text" ;
  3. Laya.stage.addChild(text);
  4. //The following is just to update the text content, using changeText can improve performance
  5. text.changeText( "text changed." );

Text.changeText will directly modify the last instruction of the text drawing in the drawing instructions. This behavior of the previous drawing instructions still existing will cause changeText to be used only in the following situations:

  • The text always consists of one line.
  • The style of the text remains the same (color, weight, italics, alignment, etc.).

Even so, such needs are still often used in actual programming.

Section 5: Reducing CPU usage

Reduce dynamic property lookups

Any object in JavaScript is dynamic, and you can add properties at will. However, it may be time-consuming to find a property among a large number of properties. If you need to use a property value frequently, you can use a local variable to save it:

  1. function foo()
  2. {
  3. var prop = target.prop;
  4. // Using props
  5. process1(prop);
  6. process2(prop);
  7. process3(prop);
  8. }

Timer

LayaAir provides two timer loops to execute code blocks.

  • The execution frequency of Laya.timer.frameLoop depends on the frame rate. You can view the current frame rate through Stat.FPS.
  • The execution frequency of Laya.timer.loop depends on the time specified by the parameter.

When the life cycle of an object ends, remember to clear its internal Timer:

  1. Laya.timer.frameLoop(1, this, animateFrameRateBased);
  2. Laya.stage. on ( "click" , this, dispose);
  3. function dispose()
  4. {
  5. Laya.timer.clear(this, animateFrameRateBased);
  6. }

How to get the display object boundary

In relative layout, it is often necessary to correctly obtain the bounds of the display object. There are also multiple ways to obtain the bounds of the display object, and it is important to know the differences between them.

1. Use getBounds/getGraphicBounds.

  1. var sp = new Sprite();
  2. sp.graphics.drawRect(0, 0, 100, 100, "#FF0000" );
  3. var bounds = sp.getGraphicBounds();
  4. Laya.stage.addChild(sp);

getBounds can meet most needs, but because it needs to calculate the boundaries, it is not suitable for frequent calls.

2. Set the container's autoSize to true.

  1. var sp = new Sprite();
  2. sp.autoSize = true ;
  3. sp.graphics.drawRect(0, 0, 100, 100, "#FF0000" );
  4. Laya.stage.addChild(sp);

The above code can correctly get the width and height at runtime. AutoSize will recalculate when the width and height are obtained and the display list status changes (autoSize calculates the width and height through getBoudns). Therefore, it is not advisable to apply autoSize to a container with a large number of child objects. If size is set, autoSize will not take effect.

Get the width and height after using loadImage:

  1. var sp = new Sprite();
  2. sp.loadImage( "res/apes/monkey2.png" , 0, 0, 0, 0, Handler. create (this, function ()
  3. {
  4. console.log(sp.width, sp.height);
  5. }));
  6. Laya.stage.addChild(sp);

loadImage can correctly obtain the width and height only after the loading completion callback function is triggered.

3. Call size settings directly:

  1. Laya.loader. load ( "res/apes/monkey2.png" , Handler. create (this, function ()
  2. {
  3. var texture = Laya.loader.getRes( "res/apes/monkey2.png" );
  4. var sp = new Sprite();
  5. sp.graphics.drawTexture(texture, 0, 0);
  6. sp. size (texture.width, texture.height);
  7. Laya.stage.addChild(sp);
  8. }));

Using Graphics.drawTexture does not automatically set the width and height of the container, but you can use the width and height of the Texture to assign it to the container. Undoubtedly, this is the most efficient way.

Note: getGraphicsBounds is used to obtain the width and height of the vector drawing.

Change frame rate based on activity state

There are three frame rate modes: Stage.FRAME_SLOW maintains the FPS at 30; Stage.FRAME_FAST maintains the FPS at 60; and Stage.FRAME_MOUSE selectively maintains the FPS at 30 or 60 frames.

Sometimes it is not necessary to run the game at 60FPS, because 30FPS can meet the response of human vision in most cases, but 30FPS may cause discontinuity of the picture when the mouse interacts, so Stage.FRAME_MOUSE came into being.

The following example shows how to move the mouse on the canvas at a frame rate of Stage.FRAME_SLOW so that the ball follows the mouse:

  1. Laya.init(Browser.width, Browser.height);
  2. Stat.show();
  3. Laya.stage.frameRate = Stage.FRAME_SLOW;
  4.  
  5. var sp = new Sprite();
  6. sp.graphics.drawCircle(0, 0, 20, "#990000" );
  7. Laya.stage.addChild(sp);
  8.  
  9. Laya.stage. on (Event.MOUSE_MOVE, this, function ()
  10. {
  11. sp.pos(Laya.stage.mouseX, Laya.stage.mouseY);
  12. });

At this time, the FPS shows 30, and when the mouse moves, you can feel that the update of the ball position is not coherent. Set Stage.frameRate to Stage.FRAME_MOUSE:

  1. Laya.stage.frameRate = Stage.FRAME_MOUSE;

At this time, after the mouse moves, the FPS will be displayed as 60, and the picture fluency will be improved. After the mouse is still for 2 seconds, the FPS will return to 30 frames.

Using callLater

callLater delays the execution of the code block until the current frame is rendered. If the current operation frequently changes the state of an object, you can consider using callLater to reduce repeated calculations.

Consider a shape, for which setting any properties that change its appearance will cause the shape to be redrawn:

  1. var rotation = 0,
  2. scale = 1,
  3. position = 0;
  4.  
  5. function setRotation(value)
  6. {
  7. this.rotation = value;
  8. update ();
  9. }
  10.  
  11. function setScale(value)
  12. {
  13. this.scale = value;
  14. update ();
  15. }
  16.  
  17. function setPosition(value)
  18. {
  19. this.position = value;
  20. update ();
  21. }
  22.  
  23. Function   update ()
  24. {
  25. console.log( 'rotation: ' + this.rotation + '\tscale: ' + this.scale + '\tposition: ' + position);
  26. }

Call the following code to change the state:

  1. setRotation(90); setScale(2); setPosition(30);

The console print result is

  1. rotation: 90 scale: 1 position: 0  
  2. rotation: 90 scale: 2 position: 0  
  3. rotation: 90 scale: 2 position: 30

update is called three times, and the final result is correct, but the first two calls are unnecessary.

Try changing the three updates to:

  1. Laya.timer.callLater(this, update );

At this point, update will only be called once, and that is the result we want.

Image/atlas loading

After the image/atlas is loaded, the engine will start processing the image resources. If an atlas is loaded, each sub-image will be processed. If a large number of images are processed at once, this process may cause long-term lag.

When loading game resources, you can load resources by level, scene, etc. The fewer images you process at the same time, the faster the game will respond. After using the resources, you can also unload them to free up memory.

Section 6: Other Optimization Strategies

  1. Reduce the number of particles used. In mobile platform Canvas mode, try not to use particles;
  2. In Canvas mode, try to minimize the use of rotation, scaling, alpha and other attributes, as these attributes will consume performance. (Can be used in WebGL mode);
  3. Do not create objects or complex calculations in the timeloop;
  4. Try to reduce the use of autoSize on containers and getBounds() as much as possible, as these calls will generate more calculations;
  5. Try to use try catch as little as possible, as the execution of the function caught by try catch will become very slow;

<<:  Android Annotations Quick Start and Practical Analysis

>>:  10+ Apps You Must Uninstall During the National Day Holiday

Recommend

From the ground to the sky, maybe this is the future of mobile phones?

Introduction In ancient times, people basically c...

How to achieve growth from 0 to 500,000 for an independently operated mini program?

It’s been a long time since I updated the article...

A guide to avoiding pitfalls for new consumer brands in 2022

The "excitement" of new consumption has...

Human "Death" Diary: Can a tiny insect kill people?

Source of this article: Knowledge Keer. Reprint p...

Tiankeng, unexpected fall

This is the fallen earth It is God's "pi...

Zhihu Product Analysis Report

Zhihu has become a high-quality question-and-answ...

5-step guide to Tik Tok influencer placement

An old client who I cooperated with on event mark...

NetEase Wugu Reading Product Analysis Report

In today's information-shock world, reading h...