渲染优化

This commit is contained in:
CN-JS-HuiBai
2026-04-07 00:26:00 +08:00
parent c94b697319
commit 09887b52d0
2 changed files with 130 additions and 90 deletions

View File

@@ -137,6 +137,7 @@
let siteThemeQuery = null; // For media query cleanup let siteThemeQuery = null; // For media query cleanup
let siteThemeHandler = null; let siteThemeHandler = null;
let backgroundIntervals = []; // To track setIntervals let backgroundIntervals = []; // To track setIntervals
let lastMapDataHash = ''; // Cache for map rendering optimization
// Load sort state from localStorage or use default // Load sort state from localStorage or use default
let currentSort = { column: 'up', direction: 'desc' }; let currentSort = { column: 'up', direction: 'desc' };
@@ -882,12 +883,46 @@
} }
}); });
} }
function updateMap2D(servers) { // Debounced version of the map update to avoid rapid succession calls
if (!myMap2D) return; // Optimized map update with change detection and performance tuning
const updateMap2D = debounce(function (servers) {
if (!myMap2D || !servers) return;
// Shift longitude for Pacific-centered view // Shift longitude for Pacific-centered view
const shiftLng = (lng) => (lng < -20 ? lng + 360 : lng); const shiftLng = (lng) => (lng < -20 ? lng + 360 : lng);
// Create a data fingerprint to avoid redundant renders
// We only care about geographical positions and latency connectivity for the map
const dataFingerprint = JSON.stringify({
serverCount: servers.length,
latencyCount: currentLatencies.length,
// Sample critical connectivity data (first/last items)
lHash: currentLatencies.length > 0 ? (currentLatencies[0].id + currentLatencies[currentLatencies.length-1].id) : '',
// Include theme to ensure theme toggle still triggers redraw
theme: document.documentElement.classList.contains('light-theme') ? 'light' : 'dark'
});
if (dataFingerprint === lastMapDataHash) {
return;
}
lastMapDataHash = dataFingerprint;
// 1. Pre-build coord lookup for O(1) route lookup
const serverCoords = new Map();
servers.forEach(s => {
if (s.lat && s.lng) {
const coords = [shiftLng(s.lng), s.lat];
const city = (s.city || '').toLowerCase().trim();
const country = (s.country || '').toLowerCase().trim();
const countryName = (s.countryName || '').toLowerCase().trim();
if (city) serverCoords.set(city, coords);
if (country) serverCoords.set(country, coords);
if (countryName) serverCoords.set(countryName, coords);
}
});
// 2. Prepare geoData for scatter points
const geoData = servers const geoData = servers
.filter(s => s.lat && s.lng) .filter(s => s.lat && s.lng)
.map(s => ({ .map(s => ({
@@ -901,9 +936,10 @@
netTx: s.netTx netTx: s.netTx
})); }));
// Combine all series // Start with the scatter series
const finalSeries = [ const finalSeries = [
{ {
id: 'nodes-scatter',
type: 'scatter', type: 'scatter',
coordinateSystem: 'geo', coordinateSystem: 'geo',
geoIndex: 0, geoIndex: 0,
@@ -914,11 +950,12 @@
shadowColor: 'rgba(6, 182, 212, 0.5)' shadowColor: 'rgba(6, 182, 212, 0.5)'
}, },
data: geoData, data: geoData,
zlevel: 1 zlevel: 1,
animation: false // Performance optimization
} }
]; ];
// Add latency routes if configured // 3. Process latency routes with grouping
if (currentLatencies && currentLatencies.length > 0) { if (currentLatencies && currentLatencies.length > 0) {
const countryCoords = { const countryCoords = {
'china': [116.4074, 39.9042], 'china': [116.4074, 39.9042],
@@ -950,130 +987,114 @@
'seoul': [126.9780, 37.5665] 'seoul': [126.9780, 37.5665]
}; };
const getCoords = (name) => {
const lowerName = (name || '').toLowerCase().trim();
if (countryCoords[lowerName]) return countryCoords[lowerName];
const s = servers.find(sv =>
(sv.countryName || '').toLowerCase() === lowerName ||
(sv.country || '').toLowerCase() === lowerName ||
(sv.city || '').toLowerCase() === lowerName
);
if (s && s.lng && s.lat) return [shiftLng(s.lng), s.lat];
return null;
};
const getShiftedCoords = (name) => { const getShiftedCoords = (name) => {
const c = countryCoords[(name || '').toLowerCase().trim()]; const lower = (name || '').toLowerCase().trim();
if (c) return [shiftLng(c[0]), c[1]]; if (countryCoords[lower]) return [shiftLng(countryCoords[lower][0]), countryCoords[lower][1]];
const raw = getCoords(name); const sCoord = serverCoords.get(lower);
if (raw) return [shiftLng(raw[0]), raw[1]]; if (sCoord) return sCoord;
return null; return null;
}; };
// Group latency routes by path to handle overlap visually
const routeGroups = {}; const routeGroups = {};
currentLatencies.forEach(route => { currentLatencies.forEach(route => {
const start = getShiftedCoords(route.source); const start = getShiftedCoords(route.source);
const end = getShiftedCoords(route.dest); const end = getShiftedCoords(route.dest);
if (start && end) { if (start && end) {
// Canonical points for grouping (independent of direction)
// Sort by longitude then latitude
const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
const key = `${pts[0][0].toFixed(4)},${pts[0][1].toFixed(4)}_${pts[1][0].toFixed(4)},${pts[1][1].toFixed(4)}`; const key = `${pts[0][0].toFixed(2)},${pts[0][1].toFixed(2)}_${pts[1][0].toFixed(2)},${pts[1][1].toFixed(2)}`;
if (!routeGroups[key]) routeGroups[key] = []; if (!routeGroups[key]) routeGroups[key] = [];
routeGroups[key].push({ route, start, end }); routeGroups[key].push({ route, start, end });
} }
}); });
const styleGroupedSeries = new Map();
Object.keys(routeGroups).forEach(key => { Object.keys(routeGroups).forEach(key => {
const routes = routeGroups[key]; const routesInGroup = routeGroups[key];
const count = routes.length; const count = routesInGroup.length;
routes.forEach((item, i) => { routesInGroup.forEach((item, i) => {
const { route, start, end } = item; const { route, start, end } = item;
// Identify if the route is in the "canonical" direction
const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
const isForward = (start === pts[0]);
// Normalization and special routing rules
const lowS = (route.source || '').toLowerCase().trim(); const lowS = (route.source || '').toLowerCase().trim();
const lowD = (route.dest || '').toLowerCase().trim(); const lowD = (route.dest || '').toLowerCase().trim();
const isSeattleJapan = ( (lowS === 'seattle' || lowS === 'us seattle') && lowD === 'japan' ) ||
( (lowD === 'seattle' || lowD === 'us seattle') && lowS === 'japan' ); const pts = [start, end].slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
const isForward = (start === pts[0]);
const isSeattleJapan = ((lowS === 'seattle' || lowS === 'us seattle') && lowD === 'japan') ||
((lowD === 'seattle' || lowD === 'us seattle') && lowS === 'japan');
// Canonical hash for deterministic randomization (period, etc)
const names = [lowS, lowD].sort(); const names = [lowS, lowD].sort();
const routeHash = names.join('').split('').reduce((acc, c) => acc + c.charCodeAt(0), 0); const routeHash = names.join('').split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
let finalCurve = 0; let finalCurve = 0.12;
if (isSeattleJapan) { if (isSeattleJapan) {
// Special rule: Seattle ↔ Japan route curves upward (North) finalCurve = (lowS === 'seattle' || lowS === 'us seattle') ? -0.2 : 0.2;
// Based on user feedback that previous signs resulted in a downward curve, } else if (count > 1) {
// we flip them to ensure the intended upward arc.
const isWestbound = (lowS === 'seattle' || lowS === 'us seattle');
finalCurve = isWestbound ? -0.2 : 0.2;
} else if (count === 1) {
// Subtle consistent curve for single routes to maintain parallelism
finalCurve = 0.12;
} else {
// Symmetrical fan-out for multiple lines between the same points
const magnitude = 0.12 + Math.floor(i / 2) * 0.12; const magnitude = 0.12 + Math.floor(i / 2) * 0.12;
const spread = (i % 2 === 0) ? magnitude : -magnitude; const spread = (i % 2 === 0) ? magnitude : -magnitude;
finalCurve = isForward ? spread : -spread; finalCurve = isForward ? spread : -spread;
} }
finalSeries.push({ const period = 3 + ((routeHash + i) % 5);
type: 'lines', const styleKey = `${finalCurve.toFixed(2)}_${period}`;
coordinateSystem: 'geo',
zlevel: 2, if (!styleGroupedSeries.has(styleKey)) {
latencyLine: true, styleGroupedSeries.set(styleKey, {
effect: { id: 'latency-group-' + styleKey,
show: true, type: 'lines',
period: 3 + ((routeHash + i) % 5), // Deterministic random period: 3-7s to vary start/speed coordinateSystem: 'geo',
trailLength: 0.1, zlevel: 2,
color: 'rgba(99, 102, 241, 0.8)', effect: {
symbol: 'arrow', show: true,
symbolSize: 6 period: period,
}, trailLength: 0.1,
lineStyle: { color: 'rgba(99, 102, 241, 0.8)',
color: 'rgba(99, 102, 241, 0.3)', symbol: 'arrow',
width: 2, symbolSize: 6
curveness: finalCurve },
}, lineStyle: {
tooltip: { color: 'rgba(99, 102, 241, 0.3)',
formatter: () => { width: 2,
const latVal = (route.latency !== null && route.latency !== undefined) ? `${route.latency.toFixed(2)} ms` : '测量中...'; curveness: finalCurve
return ` },
<div style="padding: 4px;"> progressive: 50, // Progressive rendering for GPU offload
<div style="font-weight: 700;">${route.source}${route.dest}</div> animation: false,
<div style="font-size: 0.75rem; color: var(--accent-indigo); margin-top: 4px;">延时: ${latVal}</div> tooltip: {
</div> formatter: (params) => {
`; const r = params.data.meta;
} if (!r) return '';
}, const latVal = (r.latency !== null && r.latency !== undefined) ? `${r.latency.toFixed(2)} ms` : '测量中...';
data: [{ return `
fromName: route.source, <div style="padding: 4px;">
toName: route.dest, <div style="font-weight: 700;">${r.source}${r.dest}</div>
coords: [start, end] <div style="font-size: 0.75rem; color: var(--accent-indigo); margin-top: 4px;">延时: ${latVal}</div>
}] </div>
`;
}
},
data: []
});
}
styleGroupedSeries.get(styleKey).data.push({
fromName: route.source,
toName: route.dest,
coords: [start, end],
meta: route
}); });
}); });
}); });
styleGroupedSeries.forEach(s => finalSeries.push(s));
} }
myMap2D.setOption({ myMap2D.setOption({ series: finalSeries }, { replaceMerge: ['series'] });
series: finalSeries
});
// Update footer stats
if (dom.globeTotalNodes) dom.globeTotalNodes.textContent = geoData.length; if (dom.globeTotalNodes) dom.globeTotalNodes.textContent = geoData.length;
if (dom.globeTotalRegions) { if (dom.globeTotalRegions) {
const regions = new Set(geoData.map(d => d.country)).size; dom.globeTotalRegions.textContent = new Set(geoData.map(d => d.country)).size;
dom.globeTotalRegions.textContent = regions;
} }
} }, 800);
// ---- Update Dashboard ---- // ---- Update Dashboard ----
function updateDashboard(data) { function updateDashboard(data) {

View File

@@ -18,6 +18,7 @@ class AreaChart {
this.prevMaxVal = 0; this.prevMaxVal = 0;
this.currentMaxVal = 0; this.currentMaxVal = 0;
this.lastDataHash = ''; // Fingerprint for optimization
// Use debounced resize for performance and safety // Use debounced resize for performance and safety
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this); this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
@@ -40,6 +41,14 @@ class AreaChart {
setData(data) { setData(data) {
if (!data || !data.timestamps) return; if (!data || !data.timestamps) return;
// 1. Data Fingerprinting: Skip redundant updates to save GPU/CPU
const fingerprint = data.timestamps.length + '_' +
(data.rx.length > 0 ? data.rx[data.rx.length - 1] : 0) + '_' +
(data.tx.length > 0 ? data.tx[data.tx.length - 1] : 0);
if (fingerprint === this.lastDataHash) return;
this.lastDataHash = fingerprint;
// Store old data for smooth transition before updating this.data // Store old data for smooth transition before updating this.data
// Only clone if there is data to clone; otherwise use empty set // Only clone if there is data to clone; otherwise use empty set
if (this.data && this.data.timestamps && this.data.timestamps.length > 0) { if (this.data && this.data.timestamps && this.data.timestamps.length > 0) {
@@ -335,6 +344,7 @@ class MetricChart {
this.prevMaxVal = 0; this.prevMaxVal = 0;
this.currentMaxVal = 0; this.currentMaxVal = 0;
this.lastDataHash = ''; // Fingerprint for optimization
// Use debounced resize for performance and safety // Use debounced resize for performance and safety
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this); this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
@@ -358,6 +368,15 @@ class MetricChart {
} }
setData(data) { setData(data) {
if (!data || !data.timestamps) return;
// 1. Simple fingerprinting to avoid constant re-animation of same data
const lastVal = data.values && data.values.length > 0 ? data.values[data.values.length - 1] : 0;
const fingerprint = data.timestamps.length + '_' + lastVal + '_' + (data.series ? 's' : 'v');
if (fingerprint === this.lastDataHash) return;
this.lastDataHash = fingerprint;
if (this.data && this.data.values && this.data.values.length > 0) { if (this.data && this.data.values && this.data.values.length > 0) {
this.prevData = JSON.parse(JSON.stringify(this.data)); this.prevData = JSON.parse(JSON.stringify(this.data));
} else { } else {