Skip to content

Commit cecadad

Browse files
feat(chrome-ext): use scroll deltas to minimize perceived lag (#1906)
1 parent 7be3b1f commit cecadad

File tree

1 file changed

+63
-1
lines changed

1 file changed

+63
-1
lines changed

packages/lint-framework/src/lint/LintFramework.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { IgnorableLintBox } from './Box';
12
import computeLintBoxes from './computeLintBoxes';
23
import { isVisible } from './domUtils';
34
import Highlights from './Highlights';
@@ -17,12 +18,16 @@ export default class LintFramework {
1718
private popupHandler: PopupHandler;
1819
private targets: Set<Node>;
1920
private scrollableAncestors: Set<HTMLElement>;
21+
private scrollPositions: Map<HTMLElement, { left: number; top: number }>;
2022
private lintRequested = false;
2123
private renderRequested = false;
2224
private lastLints: { target: HTMLElement; lints: UnpackedLint[] }[] = [];
25+
private lastBoxes: IgnorableLintBox[] = [];
2326

2427
/** The function to be called to re-render the highlights. This is a variable because it is used to register/deregister event listeners. */
2528
private updateEventCallback: () => void;
29+
/** Scroll handler used to register/deregister scroll listeners. */
30+
private scrollEventCallback: (ev: Event) => void;
2631

2732
/** Function used to fetch lints for a given text/domain. */
2833
private lintProvider: (text: string, domain: string) => Promise<UnpackedLint[]>;
@@ -53,12 +58,18 @@ export default class LintFramework {
5358
});
5459
this.targets = new Set();
5560
this.scrollableAncestors = new Set();
61+
this.scrollPositions = new Map();
5662
this.lastLints = [];
63+
this.lastBoxes = [];
5764

5865
this.updateEventCallback = () => {
5966
this.update();
6067
};
6168

69+
this.scrollEventCallback = (ev: Event) => {
70+
this.onAncestorScroll(ev);
71+
};
72+
6273
const timeoutCallback = () => {
6374
this.update();
6475

@@ -159,7 +170,15 @@ export default class LintFramework {
159170
for (const el of scrollableAncestors) {
160171
if (!this.scrollableAncestors.has(el as HTMLElement)) {
161172
this.scrollableAncestors.add(el as HTMLElement);
162-
(el as HTMLElement).addEventListener('scroll', this.updateEventCallback, {
173+
// Initialize scroll position tracking
174+
const scroller = el as HTMLElement;
175+
this.scrollPositions.set(scroller, {
176+
left: scroller.scrollLeft,
177+
top: scroller.scrollTop,
178+
});
179+
180+
// Listen for scroll with immediate highlight shift
181+
(el as HTMLElement).addEventListener('scroll', this.scrollEventCallback, {
163182
capture: true,
164183
passive: true,
165184
});
@@ -179,6 +198,47 @@ export default class LintFramework {
179198
}
180199
}
181200

201+
/**
202+
* Handle scrolls on tracked ancestor elements by shifting the last-rendered
203+
* boxes immediately by the scroll delta, then schedule a full recompute.
204+
*/
205+
private onAncestorScroll(ev: Event) {
206+
const scroller = ev.target as HTMLElement | null;
207+
if (!scroller) return;
208+
209+
const prev = this.scrollPositions.get(scroller);
210+
const current = { left: scroller.scrollLeft, top: scroller.scrollTop };
211+
if (!prev) {
212+
this.scrollPositions.set(scroller, current);
213+
this.updateEventCallback();
214+
return;
215+
}
216+
217+
const dx = current.left - prev.left;
218+
const dy = current.top - prev.top;
219+
220+
// Update stored position immediately
221+
this.scrollPositions.set(scroller, current);
222+
223+
if ((dx !== 0 || dy !== 0) && this.lastBoxes.length > 0) {
224+
// Shift only boxes whose source is within this scroller
225+
const adjusted: IgnorableLintBox[] = this.lastBoxes.map((b) => {
226+
const sourceEl = b.source as any as HTMLElement;
227+
if (sourceEl && scroller.contains(sourceEl)) {
228+
return { ...b, x: b.x - dx, y: b.y - dy };
229+
}
230+
return b;
231+
});
232+
233+
// Render immediately so highlights track content without visible lag
234+
this.highlights.renderLintBoxes(adjusted);
235+
this.popupHandler.updateLintBoxes(adjusted);
236+
}
237+
238+
// Continue with normal update to recompute accurate layout
239+
this.updateEventCallback();
240+
}
241+
182242
private requestRender() {
183243
if (this.renderRequested) {
184244
return;
@@ -194,6 +254,8 @@ export default class LintFramework {
194254
)
195255
: [],
196256
);
257+
// Save for immediate scroll adjustments
258+
this.lastBoxes = boxes;
197259
this.highlights.renderLintBoxes(boxes);
198260
this.popupHandler.updateLintBoxes(boxes);
199261

0 commit comments

Comments
 (0)