Optimize frontend rendering: delta updates, rAF debouncing, cached DOM refs
- Disable Chart.js animations on real-time FPS and perf charts - Dashboard: delta-update profile badges on state changes instead of full DOM rebuild - Dashboard: cache querySelector results in Map for metrics update loop - Dashboard: debounce poll interval slider restart (300ms) - Calibration: debounce ResizeObserver and span drag via requestAnimationFrame - Calibration: batch updateCalibrationPreview canvas render into rAF Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,9 @@ class CalibrationModal extends Modal {
|
|||||||
|
|
||||||
const calibModal = new CalibrationModal();
|
const calibModal = new CalibrationModal();
|
||||||
|
|
||||||
|
let _dragRaf = null;
|
||||||
|
let _previewRaf = null;
|
||||||
|
|
||||||
/* ── Public API (exported names unchanged) ────────────────────── */
|
/* ── Public API (exported names unchanged) ────────────────────── */
|
||||||
|
|
||||||
export async function showCalibration(deviceId) {
|
export async function showCalibration(deviceId) {
|
||||||
@@ -117,8 +120,12 @@ export async function showCalibration(deviceId) {
|
|||||||
|
|
||||||
if (!window._calibrationResizeObserver) {
|
if (!window._calibrationResizeObserver) {
|
||||||
window._calibrationResizeObserver = new ResizeObserver(() => {
|
window._calibrationResizeObserver = new ResizeObserver(() => {
|
||||||
updateSpanBars();
|
if (window._calibrationResizeRaf) return;
|
||||||
renderCalibrationCanvas();
|
window._calibrationResizeRaf = requestAnimationFrame(() => {
|
||||||
|
window._calibrationResizeRaf = null;
|
||||||
|
updateSpanBars();
|
||||||
|
renderCalibrationCanvas();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
window._calibrationResizeObserver.observe(preview);
|
window._calibrationResizeObserver.observe(preview);
|
||||||
@@ -202,8 +209,12 @@ export function updateCalibrationPreview() {
|
|||||||
if (toggleEl) toggleEl.classList.toggle('edge-disabled', count === 0);
|
if (toggleEl) toggleEl.classList.toggle('edge-disabled', count === 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSpanBars();
|
if (_previewRaf) cancelAnimationFrame(_previewRaf);
|
||||||
renderCalibrationCanvas();
|
_previewRaf = requestAnimationFrame(() => {
|
||||||
|
_previewRaf = null;
|
||||||
|
updateSpanBars();
|
||||||
|
renderCalibrationCanvas();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderCalibrationCanvas() {
|
export function renderCalibrationCanvas() {
|
||||||
@@ -509,11 +520,19 @@ function initSpanDrag() {
|
|||||||
if (handleType === 'start') span.start = Math.min(fraction, span.end - MIN_SPAN);
|
if (handleType === 'start') span.start = Math.min(fraction, span.end - MIN_SPAN);
|
||||||
else span.end = Math.max(fraction, span.start + MIN_SPAN);
|
else span.end = Math.max(fraction, span.start + MIN_SPAN);
|
||||||
|
|
||||||
updateSpanBars();
|
if (!_dragRaf) {
|
||||||
renderCalibrationCanvas();
|
_dragRaf = requestAnimationFrame(() => {
|
||||||
|
_dragRaf = null;
|
||||||
|
updateSpanBars();
|
||||||
|
renderCalibrationCanvas();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseUp() {
|
function onMouseUp() {
|
||||||
|
if (_dragRaf) { cancelAnimationFrame(_dragRaf); _dragRaf = null; }
|
||||||
|
updateSpanBars();
|
||||||
|
renderCalibrationCanvas();
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
document.removeEventListener('mouseup', onMouseUp);
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
}
|
}
|
||||||
@@ -548,11 +567,19 @@ function initSpanDrag() {
|
|||||||
span.start = newStart;
|
span.start = newStart;
|
||||||
span.end = newStart + spanWidth;
|
span.end = newStart + spanWidth;
|
||||||
|
|
||||||
updateSpanBars();
|
if (!_dragRaf) {
|
||||||
renderCalibrationCanvas();
|
_dragRaf = requestAnimationFrame(() => {
|
||||||
|
_dragRaf = null;
|
||||||
|
updateSpanBars();
|
||||||
|
renderCalibrationCanvas();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseUp() {
|
function onMouseUp() {
|
||||||
|
if (_dragRaf) { cancelAnimationFrame(_dragRaf); _dragRaf = null; }
|
||||||
|
updateSpanBars();
|
||||||
|
renderCalibrationCanvas();
|
||||||
document.removeEventListener('mousemove', onMouseMove);
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
document.removeEventListener('mouseup', onMouseUp);
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ let _fpsCharts = {}; // { targetId: Chart }
|
|||||||
let _lastRunningIds = []; // sorted target IDs from previous render
|
let _lastRunningIds = []; // sorted target IDs from previous render
|
||||||
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
|
let _uptimeBase = {}; // { targetId: { seconds, timestamp } }
|
||||||
let _uptimeTimer = null;
|
let _uptimeTimer = null;
|
||||||
|
let _metricsElements = new Map();
|
||||||
|
|
||||||
function _loadFpsHistory() {
|
function _loadFpsHistory() {
|
||||||
try {
|
try {
|
||||||
@@ -99,7 +100,7 @@ function _createFpsChart(canvasId, history, fpsTarget) {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
animation: true,
|
animation: false,
|
||||||
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
||||||
scales: {
|
scales: {
|
||||||
x: { display: false },
|
x: { display: false },
|
||||||
@@ -124,6 +125,18 @@ function _initFpsCharts(runningTargetIds) {
|
|||||||
_fpsCharts[id] = _createFpsChart(`dashboard-fps-${id}`, history, fpsTarget);
|
_fpsCharts[id] = _createFpsChart(`dashboard-fps-${id}`, history, fpsTarget);
|
||||||
}
|
}
|
||||||
_saveFpsHistory();
|
_saveFpsHistory();
|
||||||
|
_cacheMetricsElements(runningTargetIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cacheMetricsElements(runningIds) {
|
||||||
|
_metricsElements.clear();
|
||||||
|
for (const id of runningIds) {
|
||||||
|
_metricsElements.set(id, {
|
||||||
|
fps: document.querySelector(`[data-fps-text="${id}"]`),
|
||||||
|
errors: document.querySelector(`[data-errors-text="${id}"]`),
|
||||||
|
row: document.querySelector(`[data-target-id="${id}"]`),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update running target metrics in-place (no HTML rebuild). */
|
/** Update running target metrics in-place (no HTML rebuild). */
|
||||||
@@ -152,17 +165,18 @@ function _updateRunningMetrics(enrichedRunning) {
|
|||||||
_setUptimeBase(target.id, metrics.uptime_seconds);
|
_setUptimeBase(target.id, metrics.uptime_seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update text values
|
// Update text values (use cached refs, fallback to querySelector)
|
||||||
const fpsEl = document.querySelector(`[data-fps-text="${target.id}"]`);
|
const cached = _metricsElements.get(target.id);
|
||||||
|
const fpsEl = cached?.fps || document.querySelector(`[data-fps-text="${target.id}"]`);
|
||||||
if (fpsEl) fpsEl.innerHTML = `${fpsActual}<span class="dashboard-fps-target">/${fpsTarget}</span>`;
|
if (fpsEl) fpsEl.innerHTML = `${fpsActual}<span class="dashboard-fps-target">/${fpsTarget}</span>`;
|
||||||
|
|
||||||
const errorsEl = document.querySelector(`[data-errors-text="${target.id}"]`);
|
const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`);
|
||||||
if (errorsEl) errorsEl.textContent = `${errors > 0 ? '⚠️' : '✅'} ${errors}`;
|
if (errorsEl) errorsEl.textContent = `${errors > 0 ? '⚠️' : '✅'} ${errors}`;
|
||||||
|
|
||||||
// Update health dot
|
// Update health dot
|
||||||
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
||||||
if (isLed) {
|
if (isLed) {
|
||||||
const row = document.querySelector(`[data-target-id="${target.id}"]`);
|
const row = cached?.row || document.querySelector(`[data-target-id="${target.id}"]`);
|
||||||
if (row) {
|
if (row) {
|
||||||
const dot = row.querySelector('.health-dot');
|
const dot = row.querySelector('.health-dot');
|
||||||
if (dot && state.device_last_checked != null) {
|
if (dot && state.device_last_checked != null) {
|
||||||
@@ -174,19 +188,55 @@ function _updateRunningMetrics(enrichedRunning) {
|
|||||||
_saveFpsHistory();
|
_saveFpsHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _updateProfilesInPlace(profiles) {
|
||||||
|
for (const p of profiles) {
|
||||||
|
const card = document.querySelector(`[data-profile-id="${p.id}"]`);
|
||||||
|
if (!card) continue;
|
||||||
|
const badge = card.querySelector('.dashboard-badge-active, .dashboard-badge-stopped');
|
||||||
|
if (badge) {
|
||||||
|
if (!p.enabled) {
|
||||||
|
badge.className = 'dashboard-badge-stopped';
|
||||||
|
badge.textContent = t('profiles.status.disabled');
|
||||||
|
} else if (p.is_active) {
|
||||||
|
badge.className = 'dashboard-badge-active';
|
||||||
|
badge.textContent = t('profiles.status.active');
|
||||||
|
} else {
|
||||||
|
badge.className = 'dashboard-badge-stopped';
|
||||||
|
badge.textContent = t('profiles.status.inactive');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const metricVal = card.querySelector('.dashboard-metric-value');
|
||||||
|
if (metricVal) {
|
||||||
|
const cnt = p.target_ids.length;
|
||||||
|
const active = (p.active_target_ids || []).length;
|
||||||
|
metricVal.textContent = p.is_active ? `${active}/${cnt}` : `${cnt}`;
|
||||||
|
}
|
||||||
|
const btn = card.querySelector('.dashboard-target-actions .btn');
|
||||||
|
if (btn) {
|
||||||
|
btn.className = `btn btn-icon ${p.enabled ? 'btn-warning' : 'btn-success'}`;
|
||||||
|
btn.setAttribute('onclick', `dashboardToggleProfile('${p.id}', ${!p.enabled})`);
|
||||||
|
btn.textContent = p.enabled ? '⏸' : '▶';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _renderPollIntervalSelect() {
|
function _renderPollIntervalSelect() {
|
||||||
const sec = Math.round(dashboardPollInterval / 1000);
|
const sec = Math.round(dashboardPollInterval / 1000);
|
||||||
return `<span class="dashboard-poll-wrap" onclick="event.stopPropagation()"><input type="range" class="dashboard-poll-slider" min="1" max="10" value="${sec}" oninput="changeDashboardPollInterval(this.value)" title="${t('dashboard.poll_interval')}"><span class="dashboard-poll-value">${sec}s</span></span>`;
|
return `<span class="dashboard-poll-wrap" onclick="event.stopPropagation()"><input type="range" class="dashboard-poll-slider" min="1" max="10" value="${sec}" oninput="changeDashboardPollInterval(this.value)" title="${t('dashboard.poll_interval')}"><span class="dashboard-poll-value">${sec}s</span></span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _pollDebounce = null;
|
||||||
export function changeDashboardPollInterval(value) {
|
export function changeDashboardPollInterval(value) {
|
||||||
const ms = parseInt(value, 10) * 1000;
|
|
||||||
setDashboardPollInterval(ms);
|
|
||||||
startAutoRefresh();
|
|
||||||
stopPerfPolling();
|
|
||||||
startPerfPolling();
|
|
||||||
const label = document.querySelector('.dashboard-poll-value');
|
const label = document.querySelector('.dashboard-poll-value');
|
||||||
if (label) label.textContent = `${value}s`;
|
if (label) label.textContent = `${value}s`;
|
||||||
|
clearTimeout(_pollDebounce);
|
||||||
|
_pollDebounce = setTimeout(() => {
|
||||||
|
const ms = parseInt(value, 10) * 1000;
|
||||||
|
setDashboardPollInterval(ms);
|
||||||
|
startAutoRefresh();
|
||||||
|
stopPerfPolling();
|
||||||
|
startPerfPolling();
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getCollapsedSections() {
|
function _getCollapsedSections() {
|
||||||
@@ -289,11 +339,18 @@ export async function loadDashboard(forceFullRender = false) {
|
|||||||
const newRunningIds = running.map(t => t.id).sort().join(',');
|
const newRunningIds = running.map(t => t.id).sort().join(',');
|
||||||
const prevRunningIds = [..._lastRunningIds].sort().join(',');
|
const prevRunningIds = [..._lastRunningIds].sort().join(',');
|
||||||
const hasExistingDom = !!container.querySelector('.dashboard-perf-persistent');
|
const hasExistingDom = !!container.querySelector('.dashboard-perf-persistent');
|
||||||
if (!forceFullRender && hasExistingDom && newRunningIds === prevRunningIds && newRunningIds !== '') {
|
const structureUnchanged = hasExistingDom && newRunningIds === prevRunningIds;
|
||||||
|
if (structureUnchanged && !forceFullRender && running.length > 0) {
|
||||||
_updateRunningMetrics(running);
|
_updateRunningMetrics(running);
|
||||||
set_dashboardLoading(false);
|
set_dashboardLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (structureUnchanged && forceFullRender) {
|
||||||
|
if (running.length > 0) _updateRunningMetrics(running);
|
||||||
|
_updateProfilesInPlace(profiles);
|
||||||
|
set_dashboardLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (profiles.length > 0) {
|
if (profiles.length > 0) {
|
||||||
const activeProfiles = profiles.filter(p => p.is_active);
|
const activeProfiles = profiles.filter(p => p.is_active);
|
||||||
@@ -475,7 +532,7 @@ function renderDashboardProfile(profile) {
|
|||||||
const activeCount = (profile.active_target_ids || []).length;
|
const activeCount = (profile.active_target_ids || []).length;
|
||||||
const targetsInfo = isActive ? `${activeCount}/${targetCount}` : `${targetCount}`;
|
const targetsInfo = isActive ? `${activeCount}/${targetCount}` : `${targetCount}`;
|
||||||
|
|
||||||
return `<div class="dashboard-target dashboard-profile">
|
return `<div class="dashboard-target dashboard-profile" data-profile-id="${profile.id}">
|
||||||
<div class="dashboard-target-info">
|
<div class="dashboard-target-info">
|
||||||
<span class="dashboard-target-icon">📋</span>
|
<span class="dashboard-target-icon">📋</span>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function _createChart(canvasId, color, fillColor) {
|
|||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
animation: true,
|
animation: false,
|
||||||
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
||||||
scales: {
|
scales: {
|
||||||
x: { display: false },
|
x: { display: false },
|
||||||
|
|||||||
Reference in New Issue
Block a user