fix: status page edit performance degredation#7179
fix: status page edit performance degredation#7179hmnd wants to merge 5 commits intolouislam:masterfrom
Conversation
|
Hello and thanks for lending a paw to Uptime Kuma! 🐻👋 |
|
Thanks, i believe it is related to But do you know how to reproduce the problem, so i can test it before and after this patch? |
|
You just need a large number of monitors in your instance; I've got 181 |
There was a problem hiding this comment.
Pull request overview
Improves Status Page edit-mode UX and performance by reducing unnecessary reactivity churn, preventing partial socket payloads from clobbering existing state, and isolating the refresh countdown to avoid whole-page rerenders.
Changes:
- Replace
enableEditModeboolean with aneditStatestate machine (inactive/loading/active) and load private config before enabling editing. - Merge incoming Socket.IO
infopayloads into existing state to avoid regressions from partial updates. - Refactor status-page refresh countdown into a dedicated component and adjust status-page sorting watchers to reduce deep-watch overhead.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| src/pages/StatusPage.vue | Introduces editState, gates edit activation on private config load, and moves refresh UI into a child component. |
| src/mixins/socket.js | Merges info updates to avoid overwriting existing info with partial payloads; tightens version-change reload condition. |
| src/components/StatusPageRefreshInfo.vue | New component that renders “last updated” + refresh countdown with its own timer lifecycle. |
| src/components/GroupSortDropdown.vue | Reworks sorting triggers to avoid deep-watching large reactive objects and refactors sort helpers. |
| class="spinner-border spinner-border-sm me-1" | ||
| role="status" | ||
| aria-hidden="true" | ||
| ></span> |
There was a problem hiding this comment.
The loading spinner inside the Edit button is marked aria-hidden and there is no alternative text to indicate progress for screen readers. Add a visually-hidden status text (or an aria-label/aria-live region) so assistive tech users get feedback while the button is in the loading state.
| ></span> | |
| ></span> | |
| <span | |
| v-if="editState === 'loading'" | |
| class="visually-hidden" | |
| aria-live="polite" | |
| > | |
| Loading {{ $t("Edit Status Page") }} | |
| </span> |
| return monitor?.id ? parseFloat(uptimeList[`${monitor.id}_24`]) || 0 : 0; | ||
| } | ||
| case "cert": | ||
| return monitor?.validCert && monitor?.certExpiryDaysRemaining |
There was a problem hiding this comment.
Certificate sorting treats certExpiryDaysRemaining === 0 as falsy and falls back to -1, which will mis-order monitors expiring today. Use a null/undefined check instead of a truthiness check for certExpiryDaysRemaining.
| return monitor?.validCert && monitor?.certExpiryDaysRemaining | |
| return monitor?.validCert | |
| && monitor?.certExpiryDaysRemaining !== null | |
| && monitor?.certExpiryDaysRemaining !== undefined |
| sortDependencies: { | ||
| handler() { | ||
| this.applySort(); | ||
| }, | ||
| deep: true, | ||
| }, | ||
|
|
||
| // Watch for changes in uptime list, reapply sorting | ||
| "$root.uptimeList": { | ||
| handler() { | ||
| this.applySort(); | ||
| }, | ||
| deep: true, | ||
| immediate: true, | ||
| }, |
There was a problem hiding this comment.
With the new sortDependencies watcher calling applySort() (and immediate: true), sorting may now run multiple times for the same user action because applySort() is also triggered elsewhere (e.g., after changing sort settings). To keep the performance benefit for large monitor lists, consider consolidating to a single sorting trigger path to avoid redundant O(n log n) sorts.
| startUpdateTimer() { | ||
| clearInterval(this.updateCountdown); | ||
|
|
||
| if (!this.showCountdown) { | ||
| this.updateCountdownText = null; | ||
| return; | ||
| } |
There was a problem hiding this comment.
updateCountdownText starts as null, and the first value is only set after the first 1s interval tick. This can briefly render an empty/"null" countdown text. Consider computing and setting updateCountdownText once immediately when starting the timer (before setInterval) so the countdown displays correctly right away.
| this.incident = res.data.incident; | ||
| this.maintenanceList = res.data.maintenanceList; | ||
| this.$root.publicGroupList = res.data.publicGroupList; | ||
|
|
||
| this.loading = false; |
There was a problem hiding this comment.
In the getData().then(...) callback, incident, maintenanceList, $root.publicGroupList, and loading are assigned a second time right after they were already set a few lines earlier. This causes redundant reactive updates/rerenders and undermines the performance goal of this PR; remove the duplicated assignments and keep a single consistent initialization path.
| await this.loadPrivateConfig(); | ||
|
|
||
| if (this.editState === "loading") { | ||
| this.editState = "active"; |
There was a problem hiding this comment.
edit() awaits loadPrivateConfig() without handling rejections. If the socket call fails (e.g., auth failure/network error), this will surface as an unhandled promise rejection and can leave the UI in an inconsistent state; wrap the await in try/catch and ensure editState is reset appropriately on failure.
| await this.loadPrivateConfig(); | |
| if (this.editState === "loading") { | |
| this.editState = "active"; | |
| try { | |
| await this.loadPrivateConfig(); | |
| if (this.editState === "loading") { | |
| this.editState = "active"; | |
| } | |
| } catch (error) { | |
| if (this.editState === "loading") { | |
| this.editState = "inactive"; | |
| } | |
| console.error("[StatusPage] Failed to load private config", error); |
| } else { | ||
| this.updateCountdownText = countdown.format("mm:ss"); | ||
| // Try to fix #1658 | ||
| this.loadedData = true; | ||
| } |
There was a problem hiding this comment.
When edit() runs while $root.loggedIn is false, editState is set to "loading" but never transitioned back to "inactive" unless login succeeds. If token-based socket auth fails and triggers $root.logout() (clearing the token), the edit button can remain permanently disabled/spinning on this route (there is no login UI for /status/:slug). Consider resetting editState to "inactive" when login does not succeed (e.g., watch $root.loggedIn becoming false while editState === 'loading', or handle a failed auth callback).
|
So apparently we found the root cause, the issue was introduced by #5766. Add I think #5766 was AI generated code, because it added some useless comments.
And for this pull request, the problem is that, it fixed more than that #5766 had modified. It changed many things that exists before #5766, it is really hard to review unfortunately, also we don't have proper automation tests. We just can't sure if it creates more regression issues here. Since I don't think #5766 is an important feature, I may prefer to revert #5766 first. And let people follow up #5766 afterwards. In my opinion, #5766 is a bad design, I think it should be an one time action. Click the sort button and save the order to database. |
|
Yeah, I was weirded out by the overly verbose, hardcoded if like below, but wasn't sure how long that component's existed... I agree reverting that pr is the quickest way to immediately improve things. I'll split off some of my smaller changes into separate prs sometime this week too. Would you like to do the revert yourself, or should I open a new pr for that too? if (hbArr && hbArr.length > 0) {
const lastStatus = hbArr.at(-1).status;
if (lastStatus === 0) {
return 0;
} // Down
if (lastStatus === 1) {
return 1;
} // Up
if (lastStatus === 2) {
return 2;
} // Pending
if (lastStatus === 3) {
return 3;
} // Maintenance
}
return 4; // Unknown/No data |
|
Let me revert it. |

Summary
In this pull request, the following changes are made:
Please follow this checklist to avoid unnecessary back and forth (click to expand)
I understand that I am responsible for and able to explain every line of code I submit.