3
\$\begingroup\$

I am making a game in which the player has a certain amount of available boost. To show this, I am using a blue bar inside a black frame. The bar can grow and shrink at different rates as available boost goes up and down. It works well and looks nice, but it almost halves my FPS.

The relevant snippet:

var boostStyle = document.getElementById("available-boost").style;
var currBoost = 100;
var maxBoost = 100;

function setBoost(boostAmount) {
  boostStyle.width = (boostAmount / maxBoost) * 100 + "%";
};

function demonstrateBoostChange() {
  currBoost -= 1;
  setBoost(currBoost);
  if (currBoost < 1) {
    currBoost = maxBoost;
  }
  requestAnimationFrame(demonstrateBoostChange);
}
requestAnimationFrame(demonstrateBoostChange);
#boost-bar {
  bottom: 10px;
  position: absolute;
  right: 20%;
  left: 20%;
  height: 20px;
  border-radius: 12px;
  border-style: solid;
  border-width: 2px;
  border-color: rgb(0, 0, 0);
}

#available-boost {
  margin: 0 auto;
  height: 20px;
  width: 100%;
  text-align: center;
  font-weight: bold;
  border-radius: 10px;
  background-color: rgb(0, 225, 255);
}
<div id="boost-bar">
  <div id="available-boost">Boost</div>
</div>
(I see the slight glitch where the text moves when very little boost remains, but I can solve that later. )

The performance hit is probably not noticeable since it is the only thing running, but I have used Chrome DevTools to see that it is the slowest part of my code. I think this is because changing the width is triggering a reflow.

How can I improve the performance of the above code?

Some considerations:

  • I am already using a canvas for some other things, would it be faster to just put the bar on that?
  • The boost can change at different rates, depending on the maxBoost.
\$\endgroup\$
2
\$\begingroup\$

Best performance

I am already using a canvas for some other things, would it be faster to just put the bar on that?

If you are already rendering to a canvas that is under the boost bar the answer is...

  • Yes much faster to draw the boost to the canvas.
  • No if text render quality is more important than frame rate.

Simple render function

You can either render the whole boarder, background, bar and text each frame, and still have a performance increase over HTML reflows

const boostBar = { // ? and values are unknown set them to what you need
    x : ?, // For visual quality ensure these values are floored
    y : ?, // ...
    width : ? // ...
    height : ? // ...
    borderWidth : ? //...
    text : "Boost",
    max : ? max boost,
    boost : ? current level,
    font : "size and font name",
    style : {
       bg : "CSS style value",
       boarder : "CSS style value",
       bar : "CSS style value",
       font : "CSS style value",        
    },       
    draw(ctx) {
       const bB = boostBar;  // aliases to reduce code noise
       const stl = bB.style;

       // set to bar local coords
       ctx.setTransform(1, 0, 0, 1, bB.x, bB.y); 

       // Draw outer boarder
       ctx.fillStyle = stl.boarder;
       ctx.fillRect(0, 0, bB.width, bB.height);

       // Draw inner background
       const bW = bB.borderWidth; 
       const w = bB.width - bW *2; // will need this again
       const h = bB.height - bW *2; // will need this again
       ctx.fillStyle = stl.bg;
       ctx.fillRect(bW, bW, w, h);

       // calculate bar size
       const barSize = (bB.boost / bB.max) * w;

       // draw bar
       ctx.fillStyle = stl.bg;
       ctx.fillRect((w - barSize) / 2, bW, barSize, h);

       ctx.font = bB.font;
       ctx.fillStyle = stl.font;
       ctx.textAlign = "center";
       ctx.textBaseline = "middle";
       ctx.fillText(bB.text, w / 2, bB.height / 2);


       // reset transform if you render other content at absolute coords
       ctx.setTransform(1, 0, 0, 1, 0, 0); 
       // Note that you can use
       // ctx.resetTransform(); // but check for support

   }
}

There are however some problems with this. The canvas text quality is very poor compared to the DOM, and if you are rendering text elsewhere using another font, switching fonts means that the code is not optimal.

The following addresses rendering issues.

Sprite sheet

Sprites are fast. Even a medium level device can render 400+ scaled rotated, non uniformly scaled, alpha faded sprites per frame @ 60FPS if done correctly, a medium Laptop 1000+ and top end machines, well I have got 64000+ @ 60FPS

Ideally for the best performance and quality you should have the (boost bar) background and text pre-rendered. Either as a loaded image (PNG) or (SVG) that you scale and render once to an offscreen canvas. Or render them with code to the offscreen canvas.

You should use a single offscreen canvas to group related sprites. As an example using only the background and text create a canvas that is twice the height. At game start render the background in the top half and the text in the bottom.

The code to render at run time then becomes

   // following on from above snippet

    spriteSheet : // an offscreen canvas containing background and text
    draw(ctx) {
       const bB = boostBar;  // aliases to reduce code noise
       const stl = bB.style;

       // set to bar local coords
       ctx.setTransform(1, 0, 0, 1, bB.x, bB.y); 

       // Draw sprite that has background and boarder
       ctx.drawImage(bB.spriteSheet, 
           0, 0, bB.width, bB.height,
           0, 0, bB.width, bB.height
       );

       // calculate bar size
       const barSize = (bB.boost / bB.max) * w;

       // draw bar
       ctx.fillStyle = stl.bg;
       ctx.fillRect((w - barSize) / 2, bW, barSize, h);

       // Draw sprite that has text overlay
       ctx.drawImage(bB.spriteSheet, 
           0, bB.height, bB.width, bB.height,
           0, 0, bB.width, bB.height
       );


       // reset transform if you render other content at absolute coords
       ctx.setTransform(1, 0, 0, 1, 0, 0); 
       // Note that you can use
       // ctx.resetTransform(); // but check for support

   }

Text quality & performance

Interactive full screen canvas apps can have many (100+) widgets rendered using the 2D API. The more you need the more favorably it is to render direct to canvas.

But...

Canvas text is notoriously poor quality and a good reason to take the DOM performance hit in favor of quality.

If you are already rendering text to the canvas and find it acceptable then for consistency, render all text the same way.

Sprite sheet

As outlined above pre rendering text to an offscreen canvas sprite sheet will have improvements in performance. Quality can be improved using some simple text rendering techniques.

Production render

You can render text in production using Photoshop or like software and load as a PNG

Using the above two methods has some benifits

No text at all

Text is language dependent, fonts rendering is inconstant from browser to browser, browser version, OS version, GPU setup. The best option is in fact to not use any text at all, or minimize text use in favor of icons.

Sub pixel rendering.

You can get very high quality pre-rendered text using sub pixel rendering (AKA True Type) Note however it is background color dependent and thus for the progress bar will need two sprites, one for text over background, and one for text over the bar. You then render both text sprites, clipping them to as needed.

Caveats

One frame per frame. Dont use more than one call torequestAnimationFrame` per display frame. Multi frames calls have many problems. Render all content from one main loop function.

Changing fonts during a frame render should be minimized, this includes the font size. The best is that you use only one font, set at a fixed resolution. For example set font once at start ctx.font = "32px Arial"; and then use the transform to size it. ctx.setTransform(fontSize / 32, 0, 0, fontSize / 32, textX, textY)

Warning: Avoid the following as they are frame rate killers and can make a 60FPS visualization drop to only a few frames a second.

  • ctx.filter Only for static renders
  • Shadows. Maybe if the blur is small and its one or two items, but it adds an order of magnitude to rendering time for each change to any shadow settings that are rendered.
  • Dashed lines (on Chrome) ctx.strokeText the number of dashes have a one time performance cost. Calling ctx.stroke with dashed line ctx.setLineDash(, or after changing the transform while there is an active line dash, there is performance hit directly related to the number of dashes drawn (including clipped strokes not rendered).

    In previous version of this answer I said "even if not rendered" referring to the performance hit of dashed strokes, that was incorrect. There is no cost until you call ctx.stroke

\$\endgroup\$
  • \$\begingroup\$ Thanks for these tips. I will probably render on the canvas then! \$\endgroup\$ – pydude Aug 3 '18 at 14:03
2
\$\begingroup\$

You may put your text "Boost" in a seperate element and position it. Check this:

var inner = document.querySelector(".inner");
var cb = function() {
  var k = 1 - Date.now() % 1500 / 1500;
  inner.style.width = (k * 100).toFixed(2) + "%";
  window.requestAnimationFrame(cb);
};
cb();
.boost {
  border: 2px solid #999;
  width: 300px;
  position: relative;
  font: 16px mono;
}

.boost .inner {
  margin: 0 auto;
  background-color: #999;
}

.boost .text {
  position: absolute;
  text-align: center;
  top: 0;
  left: 0;
  width: 100%;
}
<div class="boost">
  <div class="inner">&nbsp;</div>
  <div class="text">BOOST</div>
</div>

Note how position: absolute; behaves inside position: relative;.


After all I suggest you to drop this part and code it with canvas. Even CSS animations will outperform requestAnimationFrame.

Example with CSS animations:

var inner = document.querySelector(".inner");
inner.addEventListener("animationend", function () {
  // Animation just ended
  inner.parentNode.style = 'display:none;';
})
inner.style.animationDuration = '2s';
inner.style.animationName = 'boost';
.boost {
  border: 2px solid #999;
  width: 300px;
  position: relative;
  font: 16px mono;
}

.boost .inner {
  margin: 0 auto;
  background-color: #999;
  animation-timing-function: linear;
}

.boost .text {
  position: absolute;
  text-align: center;
  top: 0;
  left: 0;
  width: 100%;
}

@keyframes boost {
  0% { width: 100%; }
  100% { width: 0; }
}
<div class="boost">
  <div class="inner">&nbsp;</div>
  <div class="text">BOOST</div>
</div>

\$\endgroup\$
  • \$\begingroup\$ I don't think CSS animations will work because the rate of change varies. But you think I should just use canvas instead of individual elements? \$\endgroup\$ – pydude Aug 2 '18 at 20:20
  • \$\begingroup\$ @pydude, sure, it will work just fine. \$\endgroup\$ – sineemore Aug 2 '18 at 20:25
  • \$\begingroup\$ How could it possibly work? If the CSS animation says it will take 2 seconds for it to go down, it won't go any other speed. \$\endgroup\$ – pydude Aug 2 '18 at 20:35
  • \$\begingroup\$ @pydude, I mean canvas will work just fine (: \$\endgroup\$ – sineemore Aug 3 '18 at 10:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.