From 6475b421316c9bd99124e8aa8a1cc3dd7fd2c546 Mon Sep 17 00:00:00 2001 From: Ashiq Renju Date: Thu, 4 Dec 2025 08:58:31 -0500 Subject: [PATCH 1/4] fix(friends): change "max streak" tooltip to "longest streak" (@ashiqr-dev) (#7165) ### Description Changed friends tooltip message from "max streak" to "longest streak". Please close the PR if this was intentional wording. ### Checks - [ ] Adding quotes? - [ ] Make sure to include translations for the quotes in the description (or another comment) so we can verify their content. - [ ] Adding a language? - Make sure to follow the [languages documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LANGUAGES.md) - [ ] Add language to `packages/schemas/src/languages.ts` - [ ] Add language to exactly one group in `frontend/src/ts/constants/languages.ts` - [ ] Add language json file to `frontend/static/languages` - [ ] Adding a theme? - Make sure to follow the [themes documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/THEMES.md) - [ ] Add theme to `packages/schemas/src/themes.ts` - [ ] Add theme to `frontend/src/ts/constants/themes.ts` - [ ] Add theme css file to `frontend/static/themes` - [ ] Add some screenshot of the theme, especially with different test settings (colorful, flip colors) to your pull request - [ ] Adding a layout? - [ ] Make sure to follow the [layouts documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LAYOUTS.md) - [ ] Add layout to `packages/schemas/src/layouts.ts` - [ ] Add layout json file to `frontend/static/layouts` - [ ] Adding a font? - Make sure to follow the [themes documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/FONTS.md) - [ ] Add font file to `frontend/static/webfonts` - [ ] Add font to `packages/schemas/src/fonts.ts` - [ ] Add font to `frontend/src/ts/constants/fonts.ts` - [x] Check if any open issues are related to this PR; if so, be sure to tag them below. - [x] Make sure the PR title follows the Conventional Commits standard. (https://www.conventionalcommits.org for more info) - [x] Make sure to include your GitHub username prefixed with @ inside parentheses at the end of the PR title. Closes #7127 Co-authored-by: Ashiq Renju --- frontend/src/ts/pages/friends.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/pages/friends.ts b/frontend/src/ts/pages/friends.ts index 15f2eaccdafd..ae3739b4467c 100644 --- a/frontend/src/ts/pages/friends.ts +++ b/frontend/src/ts/pages/friends.ts @@ -302,7 +302,7 @@ function buildFriendRow(entry: Friend): HTMLTableRowElement { )} ${formatStreak(entry.streak?.length)} From 741ab7cb05654d6960e8d7ad550d6ed2468ce7f0 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 4 Dec 2025 17:11:52 +0100 Subject: [PATCH 2/4] refactor: cache often used elements --- frontend/src/ts/test/test-ui.ts | 129 +++++++++++++++----------------- 1 file changed, 59 insertions(+), 70 deletions(-) diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 972ece968389..253b9983cd1d 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -137,6 +137,11 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => { if (eventKey === "burstHeatmap") void applyBurstHeatmap(); }); +const wordsEl = document.querySelector(".pageTest #words") as HTMLElement; +const wordsWrapperEl = document.querySelector( + ".pageTest #wordsWrapper", +) as HTMLElement; + export let activeWordTop = 0; export let lineTransition = false; export let currentTestLine = 0; @@ -170,10 +175,9 @@ export function focusWords(force = false): void { export function keepWordsInputInTheCenter(force = false): void { const wordsInput = getInputElement(); - const wordsWrapper = document.querySelector("#wordsWrapper"); - if (wordsInput === null || wordsWrapper === null) return; + if (wordsInput === null || wordsWrapperEl === null) return; - const wordsWrapperHeight = wordsWrapper.offsetHeight; + const wordsWrapperHeight = wordsWrapperEl.offsetHeight; const windowHeight = window.innerHeight; // dont do anything if the wrapper can fit on screen @@ -191,8 +195,8 @@ export function keepWordsInputInTheCenter(force = false): void { } export function getWordElement(index: number): HTMLElement | null { - const el = document.querySelector( - `#words .word[data-wordindex='${index}']`, + const el = wordsEl.querySelector( + `.word[data-wordindex='${index}']`, ); return el; } @@ -205,7 +209,7 @@ export function updateActiveElement( backspace?: boolean, initial = false, ): void { - const active = document.querySelector("#words .active"); + const active = wordsEl.querySelector(".active"); if (!backspace) { active?.classList.add("typed"); } @@ -434,47 +438,46 @@ function buildWordHTML(word: string, wordIndex: number): string { function updateWordWrapperClasses(): void { if (Config.tapeMode !== "off") { - $("#words").addClass("tape"); - $("#wordsWrapper").addClass("tape"); + wordsEl.classList.add("tape"); + wordsWrapperEl.classList.add("tape"); } else { - $("#words").removeClass("tape"); - $("#wordsWrapper").removeClass("tape"); + wordsEl.classList.remove("tape"); + wordsWrapperEl.classList.remove("tape"); } if (Config.blindMode) { - $("#words").addClass("blind"); - $("#wordsWrapper").addClass("blind"); + wordsEl.classList.add("blind"); + wordsWrapperEl.classList.add("blind"); } else { - $("#words").removeClass("blind"); - $("#wordsWrapper").removeClass("blind"); + wordsEl.classList.remove("blind"); + wordsWrapperEl.classList.remove("blind"); } if (Config.indicateTypos === "below" || Config.indicateTypos === "both") { - $("#words").addClass("indicateTyposBelow"); - $("#wordsWrapper").addClass("indicateTyposBelow"); + wordsEl.classList.add("indicateTyposBelow"); + wordsWrapperEl.classList.add("indicateTyposBelow"); } else { - $("#words").removeClass("indicateTyposBelow"); - $("#wordsWrapper").removeClass("indicateTyposBelow"); + wordsEl.classList.remove("indicateTyposBelow"); + wordsWrapperEl.classList.remove("indicateTyposBelow"); } if (Config.hideExtraLetters) { - $("#words").addClass("hideExtraLetters"); - $("#wordsWrapper").addClass("hideExtraLetters"); + wordsEl.classList.add("hideExtraLetters"); + wordsWrapperEl.classList.add("hideExtraLetters"); } else { - $("#words").removeClass("hideExtraLetters"); - $("#wordsWrapper").removeClass("hideExtraLetters"); + wordsEl.classList.remove("hideExtraLetters"); + wordsWrapperEl.classList.remove("hideExtraLetters"); } const existing = - $("#words") - ?.attr("class") - ?.split(/\s+/) - ?.filter((it) => !it.startsWith("highlight-")) ?? []; + wordsEl?.className + .split(/\s+/) + .filter((className) => !className.startsWith("highlight-")) ?? []; if (Config.highlightMode !== null) { existing.push("highlight-" + Config.highlightMode.replaceAll("_", "-")); } - $("#words").attr("class", existing.join(" ")); + wordsEl.className = existing.join(" "); updateWordsWidth(); updateWordsWrapperHeight(true); @@ -485,9 +488,7 @@ function updateWordWrapperClasses(): void { } export function showWords(): void { - const words = $("#words"); - - words.empty(); + wordsEl.innerHTML = ""; if (Config.mode === "zen") { appendEmptyWordElement(); @@ -496,7 +497,7 @@ export function showWords(): void { for (let i = 0; i < TestWords.words.length; i++) { wordsHTML += buildWordHTML(TestWords.words.get(i), i); } - words.html(wordsHTML); + wordsEl.innerHTML = wordsHTML; } updateActiveElement(undefined, true); @@ -507,7 +508,8 @@ export function showWords(): void { export function appendEmptyWordElement( index = TestInput.input.getHistory().length, ): void { - $("#words").append( + wordsEl.insertAdjacentHTML( + "beforeend", `
`, ); } @@ -520,9 +522,8 @@ export function updateWordsInputPosition(): void { : TestState.isLanguageRightToLeft; const el = getInputElement(); - const wrapperElement = document.querySelector("#wordsWrapper"); - if (el === null || wrapperElement === null) return; + if (el === null) return; const activeWord = getActiveWordElement(); @@ -551,7 +552,7 @@ export function updateWordsInputPosition(): void { if (Config.tapeMode !== "off") { el.style.left = `${ - wrapperElement.offsetWidth * (Config.tapeMargin / 100) + wordsWrapperEl.offsetWidth * (Config.tapeMargin / 100) }px`; } else { if (activeWord.offsetWidth < letterHeight && isTestRightToLeft) { @@ -598,14 +599,13 @@ export async function centerActiveLine(): Promise { export function updateWordsWrapperHeight(force = false): void { if (ActivePage.get() !== "test" || TestState.resultVisible) return; if (!force && Config.mode !== "custom") return; - const wrapperEl = document.getElementById("wordsWrapper") as HTMLElement; const outOfFocusEl = document.querySelector( ".outOfFocusWarning", ) as HTMLElement; const activeWordEl = getActiveWordElement(); if (!activeWordEl) return; - wrapperEl.classList.remove("hidden"); + wordsWrapperEl.classList.remove("hidden"); const wordComputedStyle = window.getComputedStyle(activeWordEl); const wordMargin = @@ -622,15 +622,14 @@ export function updateWordsWrapperHeight(force = false): void { if (showAllLines) { //allow the wrapper to grow and shink with the words - wrapperEl.style.height = ""; + wordsWrapperEl.style.height = ""; } else if (Config.mode === "zen") { //zen mode, showAllLines off - wrapperEl.style.height = wordHeight * 2 + "px"; + wordsWrapperEl.style.height = wordHeight * 2 + "px"; } else { if (Config.tapeMode === "off") { //tape off, showAllLines off, non-zen mode - const wordElements = - document.querySelectorAll("#words .word"); + const wordElements = wordsEl.querySelectorAll(".word"); let lines = 0; let lastTop = 0; let wordIndex = 0; @@ -650,15 +649,14 @@ export function updateWordsWrapperHeight(force = false): void { if (lines < 3) wrapperHeight = wrapperHeight * (3 / lines); //limit to 3 lines - wrapperEl.style.height = wrapperHeight + "px"; + wordsWrapperEl.style.height = wrapperHeight + "px"; } else { //show 3 lines if tape mode is on and has newlines, otherwise use words height (because of indicate typos: below) if (TestWords.hasNewline) { - wrapperEl.style.height = wordHeight * 3 + "px"; + wordsWrapperEl.style.height = wordHeight * 3 + "px"; } else { - const wordsHeight = - document.getElementById("words")?.offsetHeight ?? wordHeight; - wrapperEl.style.height = wordsHeight + "px"; + const wordsHeight = wordsEl.offsetHeight ?? wordHeight; + wordsWrapperEl.style.height = wordsHeight + "px"; } } } @@ -670,8 +668,6 @@ function updateWordsMargin(): void { if (Config.tapeMode !== "off") { void scrollTape(true); } else { - const wordsEl = document.getElementById("words") as HTMLElement; - $(wordsEl).stop(true, false); const afterNewlineEls = @@ -688,22 +684,22 @@ export function addWord( word: string, wordIndex = TestWords.words.length - 1, ): void { - $("#words").append(buildWordHTML(word, wordIndex)); + wordsEl.insertAdjacentHTML("beforeend", buildWordHTML(word, wordIndex)); } export function flipColors(tf: boolean): void { if (tf) { - $("#words").addClass("flipped"); + wordsEl.classList.add("flipped"); } else { - $("#words").removeClass("flipped"); + wordsEl.classList.remove("flipped"); } } export function colorful(tc: boolean): void { if (tc) { - $("#words").addClass("colorfulMode"); + wordsEl.classList.add("colorfulMode"); } else { - $("#words").removeClass("colorfulMode"); + wordsEl.classList.remove("colorfulMode"); } } @@ -846,7 +842,8 @@ export async function updateActiveWordLetters( } if (newlineafter) - $("#words").append( + wordsEl.insertAdjacentHTML( + "beforeend", "
", ); if (Config.tapeMode !== "off") { @@ -887,10 +884,7 @@ export async function scrollTape(noAnimation = false): Promise { ? !TestState.isLanguageRightToLeft : TestState.isLanguageRightToLeft; - const wordsWrapperWidth = ( - document.querySelector("#wordsWrapper") as HTMLElement - ).offsetWidth; - const wordsEl = document.getElementById("words") as HTMLElement; + const wordsWrapperWidth = wordsWrapperEl.offsetWidth; const wordsChildrenArr = [...wordsEl.children] as HTMLElement[]; const activeWordEl = getActiveWordElement(); if (!activeWordEl) return; @@ -1096,7 +1090,7 @@ export function updatePremid(): void { } function removeTestElements(lastElementIndexToRemove: number): void { - const wordsChildren = document.getElementById("words")?.children; + const wordsChildren = wordsEl.children; if (wordsChildren === undefined) return; @@ -1118,7 +1112,6 @@ export async function lineJump( if (currentTestLine > 0 || force) { const hideBound = currentTop; - const wordsEl = document.getElementById("words") as HTMLElement; const activeWordEl = getActiveWordElement(); if (!activeWordEl) { resolve(); @@ -1195,11 +1188,11 @@ export async function lineJump( export function setRightToLeft(isEnabled: boolean): void { if (isEnabled) { - $("#words").addClass("rightToLeftTest"); + wordsEl.classList.add("rightToLeftTest"); $("#resultWordsHistory .words").addClass("rightToLeftTest"); $("#resultReplay .words").addClass("rightToLeftTest"); } else { - $("#words").removeClass("rightToLeftTest"); + wordsEl.classList.remove("rightToLeftTest"); $("#resultWordsHistory .words").removeClass("rightToLeftTest"); $("#resultReplay .words").removeClass("rightToLeftTest"); } @@ -1207,11 +1200,11 @@ export function setRightToLeft(isEnabled: boolean): void { export function setLigatures(isEnabled: boolean): void { if (isEnabled || Config.mode === "custom" || Config.mode === "zen") { - $("#words").addClass("withLigatures"); + wordsEl.classList.add("withLigatures"); $("#resultWordsHistory .words").addClass("withLigatures"); $("#resultReplay .words").addClass("withLigatures"); } else { - $("#words").removeClass("withLigatures"); + wordsEl.classList.remove("withLigatures"); $("#resultWordsHistory .words").removeClass("withLigatures"); $("#resultReplay .words").removeClass("withLigatures"); } @@ -1383,9 +1376,7 @@ export function toggleResultWords(noAnimation = false): void { //show if ($("#resultWordsHistory .words .word").length === 0) { - $("#words").html( - `
`, - ); + wordsEl.innerHTML = `
`; void loadWordsHistory().then(() => { if (Config.burstHeatmap) { void applyBurstHeatmap(); @@ -1785,9 +1776,7 @@ export async function afterTestWordChange( } } else if (direction === "back") { if (Config.mode === "zen") { - const wordsChildren = [ - ...(document.querySelector("#words")?.children ?? []), - ] as HTMLElement[]; + const wordsChildren = [...(wordsEl.children ?? [])] as HTMLElement[]; let deleteElements = false; for (const child of wordsChildren) { From b9924ff4930565e8e0514684240844dc2d0d8bcb Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 4 Dec 2025 17:23:15 +0100 Subject: [PATCH 3/4] perf: defer UI updates away from input logic (@miodec) (#7162) Together with #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. --- frontend/src/html/pages/test.html | 3 + frontend/src/index.html | 2 +- frontend/src/styles/animations.scss | 9 + frontend/src/styles/core.scss | 1 + frontend/src/styles/test.scss | 18 +- frontend/src/ts/elements/keymap.ts | 92 +-- frontend/src/ts/elements/loader.ts | 48 +- frontend/src/ts/elements/monkey-power.ts | 102 +-- .../ts/input/handlers/before-insert-text.ts | 16 +- frontend/src/ts/input/handlers/insert-text.ts | 2 - frontend/src/ts/sentry.ts | 7 +- frontend/src/ts/test/live-acc.ts | 81 +-- frontend/src/ts/test/live-burst.ts | 75 ++- frontend/src/ts/test/live-speed.ts | 79 ++- frontend/src/ts/test/monkey.ts | 40 +- frontend/src/ts/test/replay.ts | 5 - frontend/src/ts/test/result.ts | 70 +- frontend/src/ts/test/test-logic.ts | 14 +- frontend/src/ts/test/test-timer.ts | 133 ++-- frontend/src/ts/test/test-ui.ts | 600 ++++++++++-------- frontend/src/ts/test/timer-progress.ts | 355 ++++++----- 21 files changed, 984 insertions(+), 768 deletions(-) diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html index 777d1f7dadc7..95ad7beed8a8 100644 --- a/frontend/src/html/pages/test.html +++ b/frontend/src/html/pages/test.html @@ -192,6 +192,9 @@ +