Only touch the DOM when a displayed value changes
The readout hung and then skipped numbers in a focused desktop window. render() runs every animation frame and was writing unconditionally — measured at 11 DOM mutations per frame — whether or not anything had changed. Two of those are the .time text node, a glyph run up to 190px, so every frame forced a style recalc and a full relayout to redraw digits that turn over once a second. Routing the per-frame writes through change guards takes a steady frame from 11 writes to 0. The timing itself was never at fault and is untouched here. elapsed() is one subtraction from a single monotonic performance.now() stamp and the reserve is debited once, in tap(), so nothing accumulates per frame and a dropped frame cannot cost or gift anyone a millisecond. Measured against wall clock over a running turn: 0.0000ms drift across 5s, 12s and 17s segments. Also fixes a real skip in fmt(). It switched from m:ss to tenths at ms < 10000 with both branches rounding up, so the coarse branch read a second high: counting down gave 0:11 for a full second, 0:10 for a single millisecond, then 10.0. Handing 10000ms to the tenths branch gives a monotone 0:11 -> 10.0 -> 9.9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+33
-11
@@ -659,7 +659,9 @@
|
||||
function fmt(ms){
|
||||
if(ms < 0) ms = 0;
|
||||
var t = Math.ceil(ms / 100) / 10;
|
||||
if(ms < 10000) return t.toFixed(1);
|
||||
// <= so 10000ms renders as "10.0" rather than "0:10": both branches round up,
|
||||
// so the coarse one reads a second high and 0:10 would flash for 1ms
|
||||
if(ms <= 10000) return t.toFixed(1);
|
||||
var s = Math.ceil(ms / 1000);
|
||||
var m = Math.floor(s / 60);
|
||||
s = s % 60;
|
||||
@@ -679,6 +681,24 @@
|
||||
return Math.max(0, st.reserve[i] - consumed(i));
|
||||
}
|
||||
|
||||
/* ---- write only on change ----
|
||||
render() runs 60 times a second but the digits turn over once a second, and
|
||||
.time is a ~190px glyph run: rewriting its text node every frame forces a
|
||||
style recalc and relayout for nothing, which is what makes the readout
|
||||
stutter. The cached marker avoids reading the DOM back, which would flush
|
||||
style by itself. */
|
||||
function setText(node, value){
|
||||
var s = String(value);
|
||||
if(node.__v !== s){ node.__v = s; node.textContent = s; }
|
||||
}
|
||||
function setAttr(node, name, value){
|
||||
if(node.getAttribute(name) !== value) node.setAttribute(name, value);
|
||||
}
|
||||
function setStyle(node, prop, value){
|
||||
var k = "__s_" + prop; // per property, so a second one can't alias
|
||||
if(node[k] !== value){ node[k] = value; node.style[prop] = value; }
|
||||
}
|
||||
|
||||
/* ============ render ============ */
|
||||
function render(){
|
||||
var e = elapsed();
|
||||
@@ -697,21 +717,23 @@
|
||||
|
||||
if(live && main <= 0 && st.phase === RUN){ flag(i); return; }
|
||||
|
||||
el[i].time.textContent = fmt(main);
|
||||
el[i].moves.textContent = st.moves[i];
|
||||
el[i].you.textContent = st.score[i]; // each panel is "You" to its own player
|
||||
el[i].them.textContent = st.score[1 - i];
|
||||
el[i].target.textContent = cfg.points;
|
||||
setText(el[i].time, fmt(main));
|
||||
setText(el[i].moves, st.moves[i]);
|
||||
setText(el[i].you, st.score[i]); // each panel is "You" to its own player
|
||||
setText(el[i].them, st.score[1 - i]);
|
||||
setText(el[i].target, cfg.points);
|
||||
|
||||
var showDelay = live && cfg.delay > 0 && dLeft > 0;
|
||||
el[i].delay.classList.toggle("on", showDelay);
|
||||
el[i].delay.classList.toggle("on", showDelay); // no-op when already right
|
||||
if(showDelay){
|
||||
el[i].fill.style.transform = "scaleX(" + (dLeft / cfg.delay) + ")";
|
||||
el[i].dnum.textContent = Math.ceil(dLeft / 1000);
|
||||
// the bar really does move every frame, but transform is compositor-only
|
||||
setStyle(el[i].fill, "transform", "scaleX(" + (dLeft / cfg.delay).toFixed(4) + ")");
|
||||
setText(el[i].dnum, Math.ceil(dLeft / 1000));
|
||||
}
|
||||
|
||||
if(st.phase === FLAG && i === st.active) el[i].root.dataset.state = "flagged";
|
||||
else el[i].root.dataset.state = (i === st.active && st.phase !== IDLE) ? "active" : "idle";
|
||||
setAttr(el[i].root, "data-state",
|
||||
(st.phase === FLAG && i === st.active) ? "flagged"
|
||||
: (i === st.active && st.phase !== IDLE) ? "active" : "idle");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user