perf(desktop): Dashboard-Aufbau in der Linux-App ohne Einblend-Animation, Melder gegen Fokus-Schuebe gedrosselt
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 1m45s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 4m35s

Gemessen in der echten Linux-App (WebKitGTK): Haenger beim Oeffnen des
Dashboards von 550-1100 ms auf 250-350 ms. Im alpha-Log kamen beim Oeffnen
bis zu 50 Abfragen je Sekunde von Erinnerungs- und Nextcloud-Melder; Fokus-/
Sichtbarkeitswechsel loesen jetzt hoechstens alle 10 s eine Abfrage aus.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-03 01:19:35 +02:00
parent 19340fcebd
commit 53a49a109f
7 changed files with 87 additions and 1 deletions
+7
View File
@@ -489,6 +489,13 @@ input[type="checkbox"], input[type="radio"] {
animation-delay: var(--enter-delay, 0ms);
}
}
/* Linux-Desktop-App (WebKitGTK, ohne Grafikbeschleunigung): keine
Einblend-Animation und kein Pulsieren der Ladeplatzhalter — beides liess den
Aufbau des Dashboards stocken (DesktopPlatformMarker setzt das Attribut). */
html[data-desktop-os="linux"] .widget-card,
html[data-desktop-os="linux"] .animate-pulse {
animation: none;
}
@keyframes widget-enter {
from {
opacity: 0;
+2
View File
@@ -4,6 +4,7 @@ import { getLocale, getMessages } from 'next-intl/server';
import { ThemeProvider } from 'next-themes';
import { DesktopContextMenuGuard } from '@/components/desktop/desktop-context-menu-guard';
import { DesktopExternalLinks } from '@/components/desktop/desktop-external-links';
import { DesktopPlatformMarker } from '@/components/desktop/desktop-platform-marker';
import { DesktopThemeSync } from '@/components/desktop/desktop-theme-sync';
import './globals.css';
@@ -29,6 +30,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<DesktopContextMenuGuard />
<DesktopExternalLinks />
<DesktopThemeSync />
<DesktopPlatformMarker />
{children}
</NextIntlClientProvider>
</ThemeProvider>
@@ -0,0 +1,22 @@
import { cleanup, render } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { DESKTOP_CLIENT_COOKIE_NAME } from '@/lib/desktop-client';
import { DesktopPlatformMarker } from './desktop-platform-marker';
afterEach(() => {
cleanup();
document.cookie = `${DESKTOP_CLIENT_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
});
describe('DesktopPlatformMarker', () => {
it('setzt das Betriebssystem der Desktop-App am html-Element', () => {
document.cookie = `${DESKTOP_CLIENT_COOKIE_NAME}=${encodeURIComponent('1.9.2|abc1234|linux')}; path=/`;
render(<DesktopPlatformMarker />);
expect(document.documentElement.dataset.desktopOs).toBe('linux');
});
it('setzt im Browser nichts', () => {
render(<DesktopPlatformMarker />);
expect(document.documentElement.dataset.desktopOs).toBeUndefined();
});
});
@@ -0,0 +1,25 @@
'use client';
import { useEffect } from 'react';
import { getDesktopClientInfo } from '@/lib/desktop-client';
/**
* DesktopPlatformMarker (quick-261002) — setzt `data-desktop-os` am
* `<html>` (z. B. `linux`), damit CSS die Desktop-App je Betriebssystem
* anpassen kann. Anlass: Die Linux-Webview (WebKitGTK) zeichnet ohne
* Grafikbeschleunigung; die gestaffelte Einblend-Animation der Kacheln liess
* den Aufbau des Dashboards spuerbar stocken (gemessen: Haenger von
* 550-1100 ms auf ~230 ms ohne Animation). Im Browser ohne Wirkung.
*/
export function DesktopPlatformMarker() {
useEffect(() => {
const os = getDesktopClientInfo()?.os;
if (!os) return;
document.documentElement.dataset.desktopOs = os;
return () => {
delete document.documentElement.dataset.desktopOs;
};
}, []);
return null;
}
@@ -9,6 +9,8 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
const MODULE_SLUG = 'nextcloud-status';
/** Abstand der Abfrage beim Server. */
const POLL_MS = 60_000;
/** Mindestabstand fuer Abfragen nach Fokus-/Sichtbarkeitswechsel. */
export const RESUME_MIN_MS = 10_000;
/** Rote Grundkennung der API -> Schluessel unter `nextcloudStatus.notify.down`. */
const DOWN_KEYS: Record<string, string> = {
@@ -110,7 +112,14 @@ export function NextcloudAlertNotifier() {
void start();
const interval = setInterval(() => void poll(), POLL_MS);
// Fokus/Sichtbarkeit loesen hoechstens alle RESUME_MIN_MS eine Abfrage aus:
// In der Linux-Desktop-App kamen diese Ereignisse beim Oeffnen des
// Dashboards in Schueben (bis zu 50 Abfragen je Sekunde im API-Log).
let lastResume = Date.now();
const resume = () => {
const now = Date.now();
if (now - lastResume < RESUME_MIN_MS) return;
lastResume = now;
pausedRef.current = false;
void start();
};
@@ -143,4 +143,16 @@ describe('ReminderNotifier', () => {
});
expect(mockList.mock.calls.length).toBe(after401 + 1);
});
it('viele Fokus-Ereignisse kurz hintereinander loesen nur eine Abfrage aus', async () => {
mockList.mockResolvedValue([]);
render(<ReminderNotifier />);
await advance(0);
await advance(15_000);
const before = mockList.mock.calls.length;
await act(async () => {
for (let i = 0; i < 50; i++) window.dispatchEvent(new Event('focus'));
await vi.advanceTimersByTimeAsync(0);
});
expect(mockList.mock.calls.length).toBe(before + 1);
});
});
@@ -9,14 +9,16 @@ import {
withNotifyLock,
} from '@/lib/reminder-notify';
import {
listReminders,
REMINDERS_CHANGED_EVENT,
type Reminder,
ReminderRequestError,
listReminders,
} from '@/lib/reminders-api';
/** Abstand der Abfrage beim Server. */
const POLL_MS = 60_000;
/** Mindestabstand fuer Abfragen nach Fokus-/Sichtbarkeitswechsel. */
export const RESUME_MIN_MS = 10_000;
/** Abstand der lokalen Faelligkeitspruefung gegen die geladene Liste (ohne Netz). */
const TICK_MS = 10_000;
const BODY_MAX = 200;
@@ -87,7 +89,14 @@ export function ReminderNotifier() {
const poll = setInterval(() => void load(), POLL_MS);
const tick = setInterval(() => void notifyDue(), TICK_MS);
// Fokus/Sichtbarkeit loesen hoechstens alle RESUME_MIN_MS eine Abfrage aus:
// In der Linux-Desktop-App kamen diese Ereignisse beim Oeffnen des
// Dashboards in Schueben (bis zu 50 Abfragen je Sekunde im API-Log).
let lastResume = Date.now();
const resume = () => {
const now = Date.now();
if (now - lastResume < RESUME_MIN_MS) return;
lastResume = now;
pausedRef.current = false;
void load();
};