Skip to content

Commit b9924ff

Browse files
authored
perf: defer UI updates away from input logic (@Miodec) (monkeytypegame#7162)
Together with monkeytypegame#7119, input handling is 3x faster. Achieved by: - deferring all UI updates to when the browser is ready and debouncing ui calls. - using vanilla js where needed - caching dom elements - disabling expensive checks if the timer is slow - switching to a timer that uses RAF instead of setTimeout - moving some code around This should make the site smother on slower devices and fix lag spikes causing weird test data.
1 parent 741ab7c commit b9924ff

File tree

21 files changed

+984
-768
lines changed

21 files changed

+984
-768
lines changed

frontend/src/html/pages/test.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@
192192
<div id="premidTestMode" class="hidden"></div>
193193
<div id="premidSecondsLeft" class="hidden"></div>
194194
</div>
195+
<div class="loading hidden">
196+
<i class="fas fa-circle-notch fa-spin"></i>
197+
</div>
195198
<div id="result" class="content-grid full-width hidden" tabindex="-1">
196199
<div class="wrapper">
197200
<div class="stats">

frontend/src/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<load src="html/warnings.html" />
88
<div id="fpsCounter" class="hidden"></div>
99
<div class="customBackground"></div>
10-
<div id="backgroundLoader" style="display: none"></div>
10+
<div id="backgroundLoader" class="hidden"></div>
1111
<div id="bannerCenter" class="focus"></div>
1212
<div id="notificationCenter">
1313
<div class="clearAll button invisible" style="display: none">

frontend/src/styles/animations.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
}
1111
}
1212

13+
@keyframes fadeIn {
14+
from {
15+
opacity: 0;
16+
}
17+
to {
18+
opacity: 1;
19+
}
20+
}
21+
1322
@keyframes caretFlashSmooth {
1423
0%,
1524
100% {

frontend/src/styles/core.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ body {
192192
background: var(--main-color);
193193
animation: loader 2s cubic-bezier(0.38, 0.16, 0.57, 0.82) infinite;
194194
z-index: 9999;
195+
opacity: 0;
195196
}
196197

197198
key {

frontend/src/styles/test.scss

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@
589589
}
590590

591591
#wordsInput {
592-
width: 1ch;
592+
width: 0;
593593
font-size: 1em;
594594
height: 1em;
595595
opacity: 0;
@@ -631,6 +631,22 @@
631631
}
632632
}
633633

634+
.pageTest > .loading {
635+
text-align: center;
636+
& > i {
637+
font-size: 2rem;
638+
color: var(--main-color);
639+
}
640+
opacity: 0;
641+
&:not(.hidden) {
642+
animation-name: fadeIn;
643+
animation-duration: 0.125s;
644+
animation-delay: 0.5s;
645+
animation-fill-mode: forwards;
646+
animation-timing-function: ease;
647+
}
648+
}
649+
634650
#result {
635651
&:focus-visible {
636652
outline: none;

frontend/src/ts/elements/keymap.ts

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getActiveFunboxNames } from "../test/funbox/list";
1616
import { areSortedArraysEqual } from "../utils/arrays";
1717
import { LayoutObject } from "@monkeytype/schemas/layouts";
1818
import { animate } from "animejs";
19+
import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame";
1920

2021
export const keyDataDelimiter = "\uE000";
2122

@@ -72,62 +73,67 @@ function findKeyElements(char: string): JQuery {
7273

7374
function highlightKey(currentKey: string): void {
7475
if (Config.mode === "zen") return;
75-
if (currentKey === "") currentKey = " ";
76-
try {
77-
$(".activeKey").removeClass("activeKey");
76+
requestDebouncedAnimationFrame("keymap.highlightKey", async () => {
77+
if (currentKey === "") currentKey = " ";
78+
try {
79+
document
80+
.querySelectorAll(".activeKey")
81+
.forEach((el) => el.classList.remove("activeKey"));
7882

79-
if (Config.language.startsWith("korean")) {
80-
currentKey = Hangul.disassemble(currentKey)[0] ?? currentKey;
81-
}
83+
if (Config.language.startsWith("korean")) {
84+
currentKey = Hangul.disassemble(currentKey)[0] ?? currentKey;
85+
}
8286

83-
const $target = findKeyElements(currentKey);
84-
$target.addClass("activeKey");
85-
} catch (e) {
86-
if (e instanceof Error) {
87-
console.log("could not update highlighted keymap key: " + e.message);
87+
const $target = findKeyElements(currentKey);
88+
$target.addClass("activeKey");
89+
} catch (e) {
90+
if (e instanceof Error) {
91+
console.log("could not update highlighted keymap key: " + e.message);
92+
}
8893
}
89-
}
94+
});
9095
}
9196

9297
async function flashKey(key: string, correct?: boolean): Promise<void> {
9398
if (key === undefined) return;
99+
requestDebouncedAnimationFrame(`keymap.flashKey.${key}`, async () => {
100+
const $target = findKeyElements(key);
94101

95-
const $target = findKeyElements(key);
96-
97-
const elements = $target.toArray();
98-
if (elements.length === 0) return;
102+
const elements = $target.toArray();
103+
if (elements.length === 0) return;
99104

100-
const themecolors = await ThemeColors.getAll();
105+
const themecolors = await ThemeColors.getAll();
101106

102-
try {
103-
let startingStyle = {
104-
color: themecolors.bg,
105-
backgroundColor: themecolors.sub,
106-
borderColor: themecolors.sub,
107-
};
108-
109-
if (correct || Config.blindMode) {
110-
startingStyle = {
111-
color: themecolors.bg,
112-
backgroundColor: themecolors.main,
113-
borderColor: themecolors.main,
114-
};
115-
} else {
116-
startingStyle = {
107+
try {
108+
let startingStyle = {
117109
color: themecolors.bg,
118-
backgroundColor: themecolors.error,
119-
borderColor: themecolors.error,
110+
backgroundColor: themecolors.sub,
111+
borderColor: themecolors.sub,
120112
};
121-
}
122113

123-
animate(elements, {
124-
color: [startingStyle.color, themecolors.sub],
125-
backgroundColor: [startingStyle.backgroundColor, themecolors.subAlt],
126-
borderColor: [startingStyle.borderColor, themecolors.sub],
127-
duration: 250,
128-
easing: "out(5)",
129-
});
130-
} catch (e) {}
114+
if (correct || Config.blindMode) {
115+
startingStyle = {
116+
color: themecolors.bg,
117+
backgroundColor: themecolors.main,
118+
borderColor: themecolors.main,
119+
};
120+
} else {
121+
startingStyle = {
122+
color: themecolors.bg,
123+
backgroundColor: themecolors.error,
124+
borderColor: themecolors.error,
125+
};
126+
}
127+
128+
animate(elements, {
129+
color: [startingStyle.color, themecolors.sub],
130+
backgroundColor: [startingStyle.backgroundColor, themecolors.subAlt],
131+
borderColor: [startingStyle.borderColor, themecolors.sub],
132+
duration: 250,
133+
easing: "out(5)",
134+
});
135+
} catch (e) {}
136+
});
131137
}
132138

133139
export function hide(): void {

frontend/src/ts/elements/loader.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
1-
const element = $("#backgroundLoader");
2-
let timeout: NodeJS.Timeout | null = null;
3-
let visible = false;
1+
import { animate, JSAnimation } from "animejs";
2+
import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame";
43

5-
function clearTimeout(): void {
6-
if (timeout !== null) {
7-
window.clearTimeout(timeout);
8-
timeout = null;
9-
}
10-
}
4+
const element = document.querySelector("#backgroundLoader") as HTMLElement;
5+
let showAnim: JSAnimation | null = null;
116

127
export function show(instant = false): void {
13-
if (visible) return;
14-
15-
if (instant) {
16-
element.stop(true, true).show();
17-
visible = true;
18-
} else {
19-
timeout = setTimeout(() => {
20-
element.stop(true, true).show();
21-
}, 125);
22-
visible = true;
23-
}
8+
requestDebouncedAnimationFrame("loader.show", () => {
9+
showAnim = animate(element, {
10+
opacity: 1,
11+
duration: 125,
12+
delay: instant ? 0 : 125,
13+
onBegin: () => {
14+
element.classList.remove("hidden");
15+
},
16+
});
17+
});
2418
}
2519

2620
export function hide(): void {
27-
if (!visible) return;
28-
clearTimeout();
29-
element.stop(true, true).fadeOut(125);
30-
visible = false;
21+
requestDebouncedAnimationFrame("loader.hide", () => {
22+
showAnim?.pause();
23+
animate(element, {
24+
opacity: 0,
25+
duration: 125,
26+
onComplete: () => {
27+
element.classList.add("hidden");
28+
},
29+
});
30+
});
3131
}

frontend/src/ts/elements/monkey-power.ts

Lines changed: 52 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import * as ThemeColors from "./theme-colors";
22
import * as SlowTimer from "../states/slow-timer";
33
import Config from "../config";
44
import { isSafeNumber } from "@monkeytype/util/numbers";
5+
import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame";
6+
7+
const html = document.querySelector("html") as HTMLElement;
8+
const body = document.body;
59

610
type Particle = {
711
x: number;
@@ -14,7 +18,7 @@ type Particle = {
1418

1519
type CTX = {
1620
particles: Particle[];
17-
caret?: JQuery;
21+
caret?: HTMLElement;
1822
canvas?: HTMLCanvasElement;
1923
context2d?: CanvasRenderingContext2D;
2024
rendering: boolean;
@@ -118,7 +122,7 @@ function updateParticle(particle: Particle): void {
118122
}
119123

120124
export function init(): void {
121-
ctx.caret = $("#caret");
125+
ctx.caret = document.querySelector("#caret") as HTMLElement;
122126
ctx.canvas = createCanvas();
123127
ctx.context2d = ctx.canvas.getContext("2d") as CanvasRenderingContext2D;
124128
}
@@ -155,7 +159,7 @@ function render(): void {
155159
}
156160
ctx.particles = keep;
157161

158-
if (ctx.particles.length && !SlowTimer.get()) {
162+
if (ctx.particles.length) {
159163
requestAnimationFrame(render);
160164
} else {
161165
ctx.context2d.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -168,14 +172,13 @@ export function reset(immediate = false): void {
168172
delete ctx.resetTimeOut;
169173

170174
clearTimeout(ctx.resetTimeOut);
171-
const body = $(document.body);
172-
body.css("transition", "all .25s, transform 0.8s");
173-
body.css("transform", `translate(0,0)`);
175+
body.style.transition = "all .25s, transform 0.8s";
176+
body.style.transform = `translate(0,0)`;
174177
setTimeout(
175178
() => {
176-
body.css("transition", "all .25s, transform .05s");
177-
$("html").css("overflow", "inherit");
178-
$("html").css("overflow-y", "scroll");
179+
body.style.transition = "all .25s, transform .05s";
180+
html.style.overflow = "inherit";
181+
html.style.overflowY = "scroll";
179182
},
180183
immediate ? 0 : 1000,
181184
);
@@ -201,47 +204,46 @@ function randomColor(): string {
201204
export async function addPower(good = true, extra = false): Promise<void> {
202205
if (Config.monkeyPowerLevel === "off" || SlowTimer.get()) return;
203206

204-
if (Config.blindMode) good = true;
205-
206-
// Shake
207-
if (["3", "4"].includes(Config.monkeyPowerLevel)) {
208-
$("html").css("overflow", "hidden");
209-
const shake = [
210-
Math.round(shakeAmount - Math.random() * shakeAmount),
211-
Math.round(shakeAmount - Math.random() * shakeAmount),
207+
requestDebouncedAnimationFrame("monkey-power.addPower", async () => {
208+
if (Config.blindMode) good = true;
209+
210+
// Shake
211+
if (["3", "4"].includes(Config.monkeyPowerLevel)) {
212+
html.style.overflow = "hidden";
213+
const shake = [
214+
Math.round(shakeAmount - Math.random() * shakeAmount),
215+
Math.round(shakeAmount - Math.random() * shakeAmount),
216+
];
217+
body.style.transform = `translate(${shake[0]}px, ${shake[1]}px)`;
218+
if (isSafeNumber(ctx.resetTimeOut)) clearTimeout(ctx.resetTimeOut);
219+
ctx.resetTimeOut = setTimeout(reset, 2000) as unknown as number;
220+
}
221+
222+
// Sparks
223+
const offset = ctx.caret?.getBoundingClientRect();
224+
const coords = [
225+
offset?.left ?? 0,
226+
(offset?.top ?? 0) + (ctx.caret?.offsetHeight ?? 0) / 2,
212227
];
213-
$(document.body).css(
214-
"transform",
215-
`translate(${shake[0]}px, ${shake[1]}px)`,
216-
);
217-
if (isSafeNumber(ctx.resetTimeOut)) clearTimeout(ctx.resetTimeOut);
218-
ctx.resetTimeOut = setTimeout(reset, 2000) as unknown as number;
219-
}
220228

221-
// Sparks
222-
const offset = ctx.caret?.offset();
223-
const coords = [
224-
offset?.left ?? 0,
225-
(offset?.top ?? 0) + (ctx.caret?.height() ?? 0),
226-
];
227-
228-
for (
229-
let i = Math.round(
230-
(particleCreateCount[0] + Math.random() * particleCreateCount[1]) *
231-
(extra ? 2 : 1),
232-
);
233-
i > 0;
234-
i--
235-
) {
236-
const color = ["2", "4"].includes(Config.monkeyPowerLevel)
237-
? randomColor()
238-
: good
239-
? await ThemeColors.get("caret")
240-
: await ThemeColors.get("error");
241-
ctx.particles.push(
242-
createParticle(...(coords as [x: number, y: number]), color),
243-
);
244-
}
245-
246-
startRender();
229+
for (
230+
let i = Math.round(
231+
(particleCreateCount[0] + Math.random() * particleCreateCount[1]) *
232+
(extra ? 2 : 1),
233+
);
234+
i > 0;
235+
i--
236+
) {
237+
const color = ["2", "4"].includes(Config.monkeyPowerLevel)
238+
? randomColor()
239+
: good
240+
? await ThemeColors.get("caret")
241+
: await ThemeColors.get("error");
242+
ctx.particles.push(
243+
createParticle(...(coords as [x: number, y: number]), color),
244+
);
245+
}
246+
247+
startRender();
248+
});
247249
}

0 commit comments

Comments
 (0)