渲染优化

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 siteThemeHandler = null;
let backgroundIntervals = []; // To track setIntervals
let lastMapDataHash = ''; // Cache for map rendering optimization
// Load sort state from localStorage or use default
let currentSort = { column: 'up', direction: 'desc' };
@@ -882,12 +883,46 @@
}
});
}
function updateMap2D(servers) {
if (!myMap2D) return;
// Debounced version of the map update to avoid rapid succession calls
// Optimized map update with change detection and performance tuning
const updateMap2D = debounce(function (servers) {
if (!myMap2D || !servers) return;
// Shift longitude for Pacific-centered view
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
.filter(s => s.lat && s.lng)
.map(s => ({
@@ -901,9 +936,10 @@
netTx: s.netTx
}));
// Combine all series
// Start with the scatter series
const finalSeries = [
{
id: 'nodes-scatter',
type: 'scatter',
coordinateSystem: 'geo',
geoIndex: 0,
@@ -914,11 +950,12 @@
shadowColor: 'rgba(6, 182, 212, 0.5)'
},
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) {
const countryCoords = {
'china': [116.4074, 39.9042],
@@ -950,130 +987,114 @@
'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 c = countryCoords[(name || '').toLowerCase().trim()];
if (c) return [shiftLng(c[0]), c[1]];
const raw = getCoords(name);
if (raw) return [shiftLng(raw[0]), raw[1]];
const lower = (name || '').toLowerCase().trim();
if (countryCoords[lower]) return [shiftLng(countryCoords[lower][0]), countryCoords[lower][1]];
const sCoord = serverCoords.get(lower);
if (sCoord) return sCoord;
return null;
};
// Group latency routes by path to handle overlap visually
const routeGroups = {};
currentLatencies.forEach(route => {
const start = getShiftedCoords(route.source);
const end = getShiftedCoords(route.dest);
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 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] = [];
routeGroups[key].push({ route, start, end });
}
});
const styleGroupedSeries = new Map();
Object.keys(routeGroups).forEach(key => {
const routes = routeGroups[key];
const count = routes.length;
const routesInGroup = routeGroups[key];
const count = routesInGroup.length;
routes.forEach((item, i) => {
routesInGroup.forEach((item, i) => {
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 lowD = (route.dest || '').toLowerCase().trim();
const isSeattleJapan = ( (lowS === 'seattle' || lowS === 'us seattle') && lowD === 'japan' ) ||
( (lowD === 'seattle' || lowD === 'us seattle') && lowS === 'japan' );
// Canonical hash for deterministic randomization (period, etc)
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');
const names = [lowS, lowD].sort();
const routeHash = names.join('').split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
let finalCurve = 0;
let finalCurve = 0.12;
if (isSeattleJapan) {
// Special rule: Seattle ↔ Japan route curves upward (North)
// Based on user feedback that previous signs resulted in a downward curve,
// 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
finalCurve = (lowS === 'seattle' || lowS === 'us seattle') ? -0.2 : 0.2;
} else if (count > 1) {
const magnitude = 0.12 + Math.floor(i / 2) * 0.12;
const spread = (i % 2 === 0) ? magnitude : -magnitude;
finalCurve = isForward ? spread : -spread;
}
finalSeries.push({
type: 'lines',
coordinateSystem: 'geo',
zlevel: 2,
latencyLine: true,
effect: {
show: true,
period: 3 + ((routeHash + i) % 5), // Deterministic random period: 3-7s to vary start/speed
trailLength: 0.1,
color: 'rgba(99, 102, 241, 0.8)',
symbol: 'arrow',
symbolSize: 6
},
lineStyle: {
color: 'rgba(99, 102, 241, 0.3)',
width: 2,
curveness: finalCurve
},
tooltip: {
formatter: () => {
const latVal = (route.latency !== null && route.latency !== undefined) ? `${route.latency.toFixed(2)} ms` : '测量中...';
return `
<div style="padding: 4px;">
<div style="font-weight: 700;">${route.source}${route.dest}</div>
<div style="font-size: 0.75rem; color: var(--accent-indigo); margin-top: 4px;">延时: ${latVal}</div>
</div>
`;
}
},
data: [{
fromName: route.source,
toName: route.dest,
coords: [start, end]
}]
const period = 3 + ((routeHash + i) % 5);
const styleKey = `${finalCurve.toFixed(2)}_${period}`;
if (!styleGroupedSeries.has(styleKey)) {
styleGroupedSeries.set(styleKey, {
id: 'latency-group-' + styleKey,
type: 'lines',
coordinateSystem: 'geo',
zlevel: 2,
effect: {
show: true,
period: period,
trailLength: 0.1,
color: 'rgba(99, 102, 241, 0.8)',
symbol: 'arrow',
symbolSize: 6
},
lineStyle: {
color: 'rgba(99, 102, 241, 0.3)',
width: 2,
curveness: finalCurve
},
progressive: 50, // Progressive rendering for GPU offload
animation: false,
tooltip: {
formatter: (params) => {
const r = params.data.meta;
if (!r) return '';
const latVal = (r.latency !== null && r.latency !== undefined) ? `${r.latency.toFixed(2)} ms` : '测量中...';
return `
<div style="padding: 4px;">
<div style="font-weight: 700;">${r.source}${r.dest}</div>
<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({
series: finalSeries
});
myMap2D.setOption({ series: finalSeries }, { replaceMerge: ['series'] });
// Update footer stats
if (dom.globeTotalNodes) dom.globeTotalNodes.textContent = geoData.length;
if (dom.globeTotalRegions) {
const regions = new Set(geoData.map(d => d.country)).size;
dom.globeTotalRegions.textContent = regions;
dom.globeTotalRegions.textContent = new Set(geoData.map(d => d.country)).size;
}
}
}, 800);
// ---- Update Dashboard ----
function updateDashboard(data) {

View File

@@ -18,6 +18,7 @@ class AreaChart {
this.prevMaxVal = 0;
this.currentMaxVal = 0;
this.lastDataHash = ''; // Fingerprint for optimization
// Use debounced resize for performance and safety
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
@@ -40,6 +41,14 @@ class AreaChart {
setData(data) {
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
// Only clone if there is data to clone; otherwise use empty set
if (this.data && this.data.timestamps && this.data.timestamps.length > 0) {
@@ -335,6 +344,7 @@ class MetricChart {
this.prevMaxVal = 0;
this.currentMaxVal = 0;
this.lastDataHash = ''; // Fingerprint for optimization
// Use debounced resize for performance and safety
this._resize = typeof debounce === 'function' ? debounce(this.resize.bind(this), 100) : this.resize.bind(this);
@@ -358,6 +368,15 @@ class MetricChart {
}
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) {
this.prevData = JSON.parse(JSON.stringify(this.data));
} else {