Reduce Attacking Troops Overlay Reflows (#3608)

## Description:

Vimacs on Discord pointed out a heavier than needed DOM load from the
[AttackingTroopsOverlay
PR](https://github.com/openfrontio/OpenFrontIO/pull/3427)

- Caches a single `labelTemplate` in `AttackingTroopsOverlay`, built
once on init and cloned per label instead of recreating it each time
- Removes redundant inline style assignments that were repeated on every
label creation
- Simplifies `updateLabelContent` by accessing template-guaranteed
children directly by index

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

Radyus
This commit is contained in:
Ralfi Salhon
2026-04-07 17:51:23 +01:00
committed by GitHub
parent ea7af50b25
commit 1cbee79cc7
@@ -36,6 +36,7 @@ interface AttackLabel {
export class AttackingTroopsOverlay implements Layer {
private container: HTMLDivElement;
private labelTemplate: HTMLDivElement;
private labels = new Map<string, AttackLabel>();
// Guard against queuing multiple worker requests in the same tick window.
private inFlightRequest = false;
@@ -63,6 +64,8 @@ export class AttackingTroopsOverlay implements Layer {
this.container.style.zIndex = "4";
document.body.appendChild(this.container);
this.labelTemplate = this.createLabelTemplate();
this.onAlternateView = (e) => {
this.isVisible = !e.alternateView;
this.container.style.display = this.isVisible ? "" : "none";
@@ -235,28 +238,39 @@ export class AttackingTroopsOverlay implements Layer {
}
}
private createLabelElement(
attackerTroops: number,
defenderTroops: number,
isIncoming: boolean,
): HTMLDivElement {
private createLabelTemplate(): HTMLDivElement {
const el = document.createElement("div");
el.style.position = "absolute";
el.style.display = "none";
el.style.alignItems = "center";
el.style.gap = "3px";
el.style.width = "max-content";
el.style.whiteSpace = "nowrap";
el.style.fontSize = "11px";
el.style.fontWeight = "bold";
el.style.fontFamily = this.game.config().theme().font();
el.style.padding = "1px 4px";
el.style.borderRadius = "3px";
el.style.backgroundColor = "rgba(0,0,0,0.55)";
el.style.pointerEvents = "none";
el.style.lineHeight = "1.3";
// Smooth the label to its new position as the front line advances.
el.style.transition = "transform 0.2s ease-out";
el.style.width = "max-content";
const icon = document.createElement("img");
icon.style.width = "10px";
icon.style.height = "10px";
el.appendChild(icon);
const span = document.createElement("span");
span.style.minWidth = "25px";
el.appendChild(span);
return el;
}
private createLabelElement(
attackerTroops: number,
defenderTroops: number,
isIncoming: boolean,
): HTMLDivElement {
const el = this.labelTemplate.cloneNode(true) as HTMLDivElement;
el.style.fontFamily = this.game.config().theme().font();
this.updateLabelContent(el, attackerTroops, defenderTroops, isIncoming);
this.container.appendChild(el);
return el;
@@ -268,17 +282,8 @@ export class AttackingTroopsOverlay implements Layer {
defenderTroops: number,
isIncoming: boolean,
) {
// Reuse existing children to avoid DOM churn on every tick.
let icon = el.querySelector("img") as HTMLImageElement | null;
let span = el.querySelector("span") as HTMLSpanElement | null;
if (!icon || !span) {
icon = document.createElement("img");
icon.style.width = "10px";
icon.style.height = "10px";
span = document.createElement("span");
el.replaceChildren(icon, span);
}
const icon = el.children[0] as HTMLImageElement;
const span = el.children[1] as HTMLSpanElement;
if (isIncoming) {
icon.src = shieldIcon;
span.style.color = troopDefenceColor(attackerTroops, defenderTroops);