performance fixes

This commit is contained in:
Chris Amow 2026-08-17 00:50:20 -05:00
parent fc09da7053
commit 90f8a1b567
2 changed files with 89 additions and 45 deletions

View file

@ -1112,6 +1112,17 @@ made `.price` undefined and axis/price drag stayed dead. Use
the scale is still auto; a mostly-vertical mouse drag now sets
`manualPriceRange` the same way two-finger pan does.
### 2026-08-16 — frontend CPU was overlay work on every mouse pixel
`subscribeCrosshairMove` rebuilt context-label DOM every move; those labels
do not depend on the cursor. `visibleLogicalRangeChange` and every tick
redrew handles, bridges, comments and labels without coalescing. Three
`window` pointermove listeners ran while idle.
Context labels now update only with the view. All overlay redraws share
one rAF and skip if the logical/price range did not change. Pointermoves
attach only for an armed tool or an in-progress drag.
### 2026-08-16 — the rest of the same session, none of it subtle
Written because they were skipped the first time and then asked for.

View file

@ -66,7 +66,10 @@ class ConfluenceChart {
this.commentNodes = new Map();
this.commentLayer = null;
this.contextLabelLayer = null;
this.contextLabelFrame = null;
this.overlayFrame = null;
this.overlayForce = false;
this.overlayRangeKey = '';
this.pointerAttached = { tool: false, anchor: false, line: false };
this.onCommentToggle = null;
this.onCommentMove = null;
this.onCommentSelect = null;
@ -254,9 +257,7 @@ class ConfluenceChart {
this.chart.applyOptions({ width: el.clientWidth, height: el.clientHeight });
requestAnimationFrame(() => {
this.observePlotCanvas();
this.renderAnchorHandles();
this.renderComments();
this.renderContextLabels();
this.scheduleOverlays(true);
});
});
this.resizeObserver.observe(el);
@ -397,16 +398,10 @@ class ConfluenceChart {
this.toolMoveListener = event => this.moveToolGesture(event);
this.toolUpListener = event => this.finishToolGesture(event);
el.addEventListener('pointerdown', this.toolDownListener);
window.addEventListener('pointermove', this.toolMoveListener);
window.addEventListener('pointerup', this.toolUpListener);
this.anchorMoveListener = event => this.moveAnchor(event);
this.anchorUpListener = event => this.finishAnchorDrag(event);
window.addEventListener('pointermove', this.anchorMoveListener);
window.addEventListener('pointerup', this.anchorUpListener);
this.lineMoveListener = event => this.moveLine(event);
this.lineUpListener = event => this.finishLineDrag(event);
window.addEventListener('pointermove', this.lineMoveListener);
window.addEventListener('pointerup', this.lineUpListener);
this.contextListener = event => this.showContextMenu(event);
el.addEventListener('contextmenu', this.contextListener);
this.clickListener = event => {
@ -423,7 +418,6 @@ class ConfluenceChart {
};
el.addEventListener('click', this.clickListener);
this.chart.subscribeCrosshairMove(param => {
this.scheduleContextLabels();
if (!param.point || !param.time) {
this.tooltip.hidden = true;
this.hideOhlc();
@ -434,11 +428,7 @@ class ConfluenceChart {
});
requestAnimationFrame(() => this.observePlotCanvas());
this.chart.timeScale().subscribeVisibleLogicalRangeChange(() => {
this.renderAnchorHandles();
this.renderLineBridges();
this.renderProjectionDiagnostic();
this.renderComments();
this.renderContextLabels();
this.scheduleOverlays();
});
this.installPinchZoom();
}
@ -469,12 +459,7 @@ class ConfluenceChart {
};
this.chart.timeScale().setVisibleRange(this.pendingView);
}
requestAnimationFrame(() => this.renderAnchorHandles());
// The bar grid just changed underneath every pinned comment.
this.renderComments();
this.renderContextLabels();
this.renderLineBridges();
this.renderProjectionDiagnostic();
this.scheduleOverlays(true);
}
setTrendlineGeometry(geometry) {
@ -591,10 +576,7 @@ class ConfluenceChart {
if (this.bars.length && this.bars[this.bars.length - 1].t === bar.t) this.bars[this.bars.length - 1] = bar;
else this.bars.push(bar);
if (advanced) this.extendManualLineSeries(last, bar);
this.renderAnchorHandles();
if (advanced || this.lineBridgeLayer?.childElementCount) this.renderLineBridges();
this.renderContextLabels();
this.renderProjectionDiagnostic();
this.scheduleOverlays(true);
}
extendManualLineSeries(previous, bar) {
@ -623,7 +605,7 @@ class ConfluenceChart {
setComments(comments) {
this.comments = comments;
this.renderComments();
this.scheduleOverlays(true);
}
setCurrentPriceAnimation(enabled) {
@ -804,9 +786,7 @@ class ConfluenceChart {
this.candles.applyOptions({ autoscaleInfoProvider: this.candleAutoscaleProvider });
}
}
this.renderAnchorHandles();
this.renderComments();
this.renderContextLabels();
this.scheduleOverlays(true);
this.updateCurrentPrice(this.lastCurrentPrice);
};
this.touchEndListener = event => {
@ -1174,10 +1154,7 @@ class ConfluenceChart {
this.chart.timeScale().setVisibleRange(this.pendingView);
this.pendingView = null;
}
this.renderAnchorHandles();
this.renderLineBridges();
this.renderContextLabels();
this.renderProjectionDiagnostic();
this.scheduleOverlays(true);
}
renderContextLabels() {
@ -1215,14 +1192,61 @@ class ConfluenceChart {
}));
}
scheduleContextLabels() {
if (this.contextLabelFrame != null) return;
this.contextLabelFrame = requestAnimationFrame(() => {
this.contextLabelFrame = null;
overlayKey() {
const range = this.chart?.timeScale().getVisibleLogicalRange();
if (!range) return '';
const price = this.manualPriceRange;
return `${range.from.toFixed(4)}:${range.to.toFixed(4)}:${price?.minValue ?? ''}:${price?.maxValue ?? ''}`;
}
scheduleOverlays(force = false) {
this.overlayForce = this.overlayForce || force;
if (this.overlayFrame != null) return;
this.overlayFrame = requestAnimationFrame(() => {
this.overlayFrame = null;
const forceDraw = this.overlayForce;
this.overlayForce = false;
const key = this.overlayKey();
if (!forceDraw && key && key === this.overlayRangeKey) return;
this.overlayRangeKey = key;
this.renderAnchorHandles();
this.renderLineBridges();
this.renderProjectionDiagnostic();
this.renderComments();
this.renderContextLabels();
});
}
attachPointers(kind) {
if (this.pointerAttached[kind]) return;
this.pointerAttached[kind] = true;
if (kind === 'tool') {
window.addEventListener('pointermove', this.toolMoveListener);
window.addEventListener('pointerup', this.toolUpListener);
} else if (kind === 'anchor') {
window.addEventListener('pointermove', this.anchorMoveListener);
window.addEventListener('pointerup', this.anchorUpListener);
} else if (kind === 'line') {
window.addEventListener('pointermove', this.lineMoveListener);
window.addEventListener('pointerup', this.lineUpListener);
}
}
detachPointers(kind) {
if (!this.pointerAttached[kind]) return;
this.pointerAttached[kind] = false;
if (kind === 'tool') {
window.removeEventListener('pointermove', this.toolMoveListener);
window.removeEventListener('pointerup', this.toolUpListener);
} else if (kind === 'anchor') {
window.removeEventListener('pointermove', this.anchorMoveListener);
window.removeEventListener('pointerup', this.anchorUpListener);
} else if (kind === 'line') {
window.removeEventListener('pointermove', this.lineMoveListener);
window.removeEventListener('pointerup', this.lineUpListener);
}
}
// --- tool arming and placement gestures ---------------------------------
setToolCompleteHandler(handler) { this.onToolComplete = handler; }
@ -1255,6 +1279,8 @@ class ConfluenceChart {
this.pendingAnchor = null;
this.clearGesture();
this.clearLinePreview();
if (tool) this.attachPointers('tool');
else this.detachPointers('tool');
}
/**
@ -1292,11 +1318,7 @@ class ConfluenceChart {
this.observedPlotCanvas = canvas;
this.plotResizeObserver = new ResizeObserver(() => {
this.syncOverlayLayer();
this.renderAnchorHandles();
this.renderLineBridges();
this.renderProjectionDiagnostic();
this.renderComments();
this.renderContextLabels();
this.scheduleOverlays(true);
if (this.armedTool) this.renderGesture();
});
this.plotResizeObserver.observe(canvas);
@ -1377,7 +1399,10 @@ class ConfluenceChart {
const point = this.eventPoint(event);
if (!point) return;
if (!this.armedTool) {
if (this.withinPlot(point)) this.mousePan = { start: point };
if (this.withinPlot(point)) {
this.mousePan = { start: point };
this.attachPointers('tool');
}
return;
}
event.preventDefault();
@ -1546,6 +1571,7 @@ class ConfluenceChart {
finishToolGesture(event) {
this.mousePan = null;
if (!this.armedTool) this.detachPointers('tool');
if (!this.armedTool || !this.gesture) return;
const tool = this.armedTool;
const { start, end } = this.gesture;
@ -2242,6 +2268,7 @@ class ConfluenceChart {
original: { ...level },
moved: false,
};
this.attachPointers('line');
event.currentTarget.setPointerCapture?.(event.pointerId);
}
@ -2291,6 +2318,7 @@ class ConfluenceChart {
const level = this.levels.find(value => value.id === this.draggingLine.id);
const moved = this.draggingLine.moved;
this.draggingLine = null;
this.detachPointers('line');
if (moved && level && this.onLineChange) this.onLineChange({ ...level });
}
@ -2303,6 +2331,7 @@ class ConfluenceChart {
anchor: level && ConfluenceChart.isFlat(level) ? 'price' : anchor,
};
event.currentTarget.setPointerCapture?.(event.pointerId);
this.attachPointers('anchor');
}
moveAnchor(event) {
@ -2350,6 +2379,7 @@ class ConfluenceChart {
event.preventDefault();
const level = this.levels.find(value => value.id === this.draggingAnchor.id);
this.draggingAnchor = null;
this.detachPointers('anchor');
this.hideSnapDot();
if (level && this.onLineChange) this.onLineChange({ ...level });
}
@ -2425,7 +2455,10 @@ class ConfluenceChart {
destroy() {
if (this.resizeObserver) this.resizeObserver.disconnect();
if (this.plotResizeObserver) this.plotResizeObserver.disconnect();
if (this.contextLabelFrame != null) cancelAnimationFrame(this.contextLabelFrame);
if (this.overlayFrame != null) cancelAnimationFrame(this.overlayFrame);
this.detachPointers('tool');
this.detachPointers('anchor');
this.detachPointers('line');
clearTimeout(this.lineFocusTimer);
for (const node of this.commentNodes.values()) clearTimeout(node.focusTimer);
if (this.chartEl && this.clickListener) this.chartEl.removeEventListener('click', this.clickListener);