Skip to content

Commit 5e91d3e

Browse files
committed
checkpoint
1 parent 5eaeeb4 commit 5e91d3e

File tree

7 files changed

+100
-90
lines changed

7 files changed

+100
-90
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ We expect all contributors to abide by the terms of our [Code of Conduct](https:
427427
- [ ] Drag and select areas of the screen to scan
428428
- [ ] Long task progress bar filter
429429
- [ ] Report should include all renders
430+
- [ ] ChatGPT / Claude video
430431
- [ ] [Runtime version guarding](https://github.com/lahmatiy/react-render-tracker/blob/229ad0e9c28853615300724d5dc86c140f250f60/src/publisher/react-integration/utils/getInternalReactConstants.ts#L28)
431432
- [ ] React as peer dependency (lock version to range)
432433
- [ ] Add a funny mascot, like the ["Stop I'm Changing" dude](https://www.youtube.com/shorts/FwOZdX7bDKI?app=desktop)

src/core/index.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -92,22 +92,14 @@ interface Internals {
9292
componentAllowList: WeakMap<React.ComponentType<any>, Options> | null;
9393
options: Options;
9494
scheduledOutlines: PendingOutline[];
95-
activeOutlinesMap: Map<string, ActiveOutline>;
95+
activeOutlines: ActiveOutline[];
9696
reportData: Record<
9797
string,
9898
{
9999
count: number;
100100
time: number;
101101
}
102102
>;
103-
fiberMap: WeakMap<
104-
Fiber,
105-
{
106-
count: number;
107-
time: number;
108-
lastUpdated: number;
109-
}
110-
>;
111103
}
112104

113105
export const ReactScanInternals: Internals = {
@@ -134,16 +126,8 @@ export const ReactScanInternals: Internals = {
134126
resetCountTimeout: 5000,
135127
},
136128
reportData: {},
137-
fiberMap: new WeakMap<
138-
Fiber,
139-
{
140-
count: number;
141-
time: number;
142-
lastUpdated: number;
143-
}
144-
>(),
145129
scheduledOutlines: [],
146-
activeOutlinesMap: new Map<string, ActiveOutline>(),
130+
activeOutlines: [],
147131
};
148132

149133
export const getReport = () => ReactScanInternals.reportData;

src/core/web/outline.ts

Lines changed: 88 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ReactScanInternals } from '../index';
55
import { getLabelText } from '../utils';
66
import { isOutlineUnstable, throttle } from './utils';
77
import { log } from './log';
8-
import { recalcOutlineColor } from './perf-observer';
8+
// import { recalcOutlineColor } from './perf-observer';
99

1010
export interface PendingOutline {
1111
rect: DOMRect;
@@ -31,13 +31,26 @@ export interface PaintedOutline {
3131

3232
export const MONO_FONT =
3333
'Menlo,Consolas,Monaco,Liberation Mono,Lucida Console,monospace';
34+
const DEFAULT_THROTTLE_TIME = 8; // 2 frames
3435
export const colorRef = { current: '115,97,230' };
3536

3637
export const getOutlineKey = (outline: PendingOutline): string => {
3738
return `${outline.rect.top}-${outline.rect.left}-${outline.rect.width}-${outline.rect.height}`;
3839
};
3940

41+
const getRectCache = new WeakMap<
42+
HTMLElement,
43+
{ rect: DOMRect; timestamp: number }
44+
>();
45+
4046
export const getRect = (domNode: HTMLElement): DOMRect | null => {
47+
const cachedRect = getRectCache.get(domNode);
48+
if (
49+
cachedRect &&
50+
performance.now() - cachedRect.timestamp < DEFAULT_THROTTLE_TIME
51+
) {
52+
return cachedRect.rect;
53+
}
4154
const style = window.getComputedStyle(domNode);
4255
if (
4356
style.display === 'none' ||
@@ -47,18 +60,19 @@ export const getRect = (domNode: HTMLElement): DOMRect | null => {
4760
return null;
4861
}
4962

50-
// if (!document.documentElement.contains(domNode)) return null;
51-
5263
const rect = domNode.getBoundingClientRect();
64+
5365
const isVisible =
5466
rect.top >= 0 ||
5567
rect.left >= 0 ||
5668
rect.bottom <= window.innerHeight ||
5769
rect.right <= window.innerWidth;
5870

59-
if (!isVisible) return null;
71+
if (!isVisible || !rect.width || !rect.height) {
72+
return null;
73+
}
6074

61-
if (!rect.height || !rect.width) return null;
75+
getRectCache.set(domNode, { rect, timestamp: performance.now() });
6276

6377
return rect;
6478
};
@@ -114,7 +128,7 @@ export const mergeOutlines = (outlines: PendingOutline[]) => {
114128
};
115129

116130
export const recalcOutlines = throttle(() => {
117-
const { scheduledOutlines, activeOutlinesMap } = ReactScanInternals;
131+
const { scheduledOutlines, activeOutlines } = ReactScanInternals;
118132

119133
for (let i = scheduledOutlines.length - 1; i >= 0; i--) {
120134
const outline = scheduledOutlines[i];
@@ -126,21 +140,18 @@ export const recalcOutlines = throttle(() => {
126140
outline.rect = rect;
127141
}
128142

129-
const activeOutlines = Array.from(activeOutlinesMap.values());
130-
131143
for (let i = activeOutlines.length - 1; i >= 0; i--) {
132144
const activeOutline = activeOutlines[i];
133145
if (!activeOutline) continue;
134146
const { outline } = activeOutline;
135147
const rect = getRect(outline.domNode);
136148
if (!rect) {
137-
activeOutlinesMap.delete(getOutlineKey(outline));
138-
activeOutline.resolve();
149+
activeOutlines.splice(i, 1);
139150
continue;
140151
}
141152
outline.rect = rect;
142153
}
143-
}, 16); // 1 frame
154+
}, DEFAULT_THROTTLE_TIME);
144155

145156
export const flushOutlines = (
146157
ctx: CanvasRenderingContext2D,
@@ -157,7 +168,7 @@ export const flushOutlines = (
157168

158169
requestAnimationFrame(() => {
159170
if (perfObserver) {
160-
recalcOutlineColor(perfObserver.takeRecords());
171+
// recalcOutlineColor(perfObserver.takeRecords());
161172
}
162173
recalcOutlines();
163174
void (async () => {
@@ -212,8 +223,7 @@ export const paintOutline = (
212223
) => {
213224
return new Promise<void>((resolve) => {
214225
const unstable = isOutlineUnstable(outline);
215-
const totalFrames = unstable ? 30 : 10;
216-
const frame = 0;
226+
const totalFrames = unstable ? 60 : 5;
217227
const alpha = 0.8;
218228

219229
const { options } = ReactScanInternals;
@@ -222,13 +232,12 @@ export const paintOutline = (
222232
log(outline.renders);
223233
}
224234

225-
const outlineKey = getOutlineKey(outline);
226-
const existingActiveOutline =
227-
ReactScanInternals.activeOutlinesMap.get(outlineKey);
235+
const key = getOutlineKey(outline);
236+
const existingActiveOutline = ReactScanInternals.activeOutlines.find(
237+
(activeOutline) => getOutlineKey(activeOutline.outline) === key,
238+
);
228239

229-
const renderCount = existingActiveOutline
230-
? existingActiveOutline.outline.renders.length + outline.renders.length
231-
: outline.renders.length;
240+
const renderCount = outline.renders.length;
232241
const maxRenders = ReactScanInternals.options.maxRenders;
233242
const t = Math.min(renderCount / (maxRenders ?? 20), 1);
234243

@@ -243,19 +252,17 @@ export const paintOutline = (
243252

244253
if (existingActiveOutline) {
245254
existingActiveOutline.outline.renders.push(...outline.renders);
246-
existingActiveOutline.frame = frame;
255+
existingActiveOutline.outline.rect = outline.rect;
256+
existingActiveOutline.frame = 0;
247257
existingActiveOutline.totalFrames = totalFrames;
248258
existingActiveOutline.alpha = alpha;
249259
existingActiveOutline.text = getLabelText(
250260
existingActiveOutline.outline.renders,
251261
);
252262
existingActiveOutline.color = color;
253-
existingActiveOutline.resolve = () => {
254-
resolve();
255-
options.onPaintFinish?.(existingActiveOutline.outline);
256-
};
257263
} else {
258-
ReactScanInternals.activeOutlinesMap.set(outlineKey, {
264+
const frame = 0;
265+
ReactScanInternals.activeOutlines.push({
259266
outline,
260267
alpha,
261268
frame,
@@ -270,50 +277,75 @@ export const paintOutline = (
270277
}
271278

272279
if (!animationFrameId) {
273-
fadeOutOutline(ctx);
280+
animationFrameId = requestAnimationFrame(() => fadeOutOutline(ctx));
274281
}
275282
});
276283
};
277284

278285
export const fadeOutOutline = (ctx: CanvasRenderingContext2D) => {
279-
const { activeOutlinesMap } = ReactScanInternals;
286+
const { activeOutlines } = ReactScanInternals;
280287

281-
ctx.save();
282-
ctx.resetTransform();
283288
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
284-
ctx.restore();
285289

286-
const activeOutlines = Array.from(activeOutlinesMap.values());
290+
const combinedPath = new Path2D();
287291

288-
for (let i = 0, len = activeOutlines.length; i < len; i++) {
292+
let maxStrokeAlpha = 0;
293+
let maxFillAlpha = 0;
294+
295+
const pendingLabeledOutlines: PaintedOutline[] = [];
296+
297+
for (let i = activeOutlines.length - 1; i >= 0; i--) {
289298
const activeOutline = activeOutlines[i];
290-
const { outline, frame, totalFrames, color } = activeOutline;
299+
if (!activeOutline) continue;
300+
const { outline, frame, totalFrames } = activeOutline;
301+
// const newRect = getRect(outline.domNode);
302+
// if (newRect) {
303+
// outline.rect = newRect;
304+
// }
291305
const { rect } = outline;
292306
const unstable = isOutlineUnstable(outline);
293307

294308
const alphaScalar = unstable ? 0.8 : 0.2;
295309

296310
activeOutline.alpha = alphaScalar * (1 - frame / totalFrames);
297311

298-
const strokeAlpha = activeOutline.alpha;
299-
const fillAlpha = activeOutline.alpha * 0.1;
312+
maxStrokeAlpha = Math.max(maxStrokeAlpha, activeOutline.alpha);
313+
maxFillAlpha = Math.max(maxFillAlpha, activeOutline.alpha * 0.1);
300314

301-
ctx.save();
302-
ctx.strokeStyle = `rgba(${color.r},${color.g},${color.b},${strokeAlpha})`;
303-
ctx.lineWidth = 1;
304-
ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${fillAlpha})`;
305-
306-
ctx.beginPath();
307-
ctx.rect(rect.x, rect.y, rect.width, rect.height);
308-
ctx.stroke();
309-
ctx.fill();
310-
ctx.closePath();
311-
ctx.restore();
315+
combinedPath.rect(rect.x, rect.y, rect.width, rect.height);
316+
317+
if (unstable) {
318+
pendingLabeledOutlines.push({
319+
alpha: activeOutline.alpha,
320+
outline,
321+
text: activeOutline.text,
322+
});
323+
}
324+
325+
activeOutline.frame++;
326+
327+
if (activeOutline.frame > activeOutline.totalFrames) {
328+
activeOutlines.splice(i, 1);
329+
}
330+
}
331+
332+
ctx.save();
333+
334+
ctx.strokeStyle = `rgba(${colorRef.current}, ${maxStrokeAlpha})`;
335+
ctx.lineWidth = 1;
336+
ctx.fillStyle = `rgba(${colorRef.current}, ${maxFillAlpha})`;
337+
338+
ctx.stroke(combinedPath);
339+
ctx.fill(combinedPath);
312340

313-
if (unstable && activeOutline.text) {
314-
const { text } = activeOutline;
315-
ctx.save();
341+
ctx.restore();
316342

343+
for (let i = 0, len = pendingLabeledOutlines.length; i < len; i++) {
344+
const { alpha, outline, text } = pendingLabeledOutlines[i];
345+
const { rect } = outline;
346+
ctx.save();
347+
348+
if (text) {
317349
ctx.font = `10px ${MONO_FONT}`;
318350
const textMetrics = ctx.measureText(text);
319351
const textWidth = textMetrics.width;
@@ -322,26 +354,19 @@ export const fadeOutOutline = (ctx: CanvasRenderingContext2D) => {
322354
const labelX: number = rect.x;
323355
const labelY: number = rect.y - textHeight - 4;
324356

325-
ctx.fillStyle = `rgba(${color.r},${color.g},${color.b},${strokeAlpha})`;
357+
ctx.fillStyle = `rgba(${colorRef.current},${alpha})`;
326358
ctx.fillRect(labelX, labelY, textWidth + 4, textHeight + 4);
327359

328-
ctx.fillStyle = `rgba(255,255,255,${strokeAlpha})`;
360+
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
329361
ctx.fillText(text, labelX + 2, labelY + textHeight);
330-
331-
ctx.restore();
332362
}
333363

334-
activeOutline.frame++;
335-
336-
if (activeOutline.frame > activeOutline.totalFrames) {
337-
activeOutlinesMap.delete(getOutlineKey(outline));
338-
activeOutline.resolve();
339-
}
364+
ctx.restore();
340365
}
341366

342-
if (activeOutlinesMap.size === 0) {
343-
animationFrameId = null;
344-
} else {
367+
if (activeOutlines.length) {
345368
animationFrameId = requestAnimationFrame(() => fadeOutOutline(ctx));
369+
} else {
370+
animationFrameId = null;
346371
}
347372
};

src/core/web/toolbar.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const createToolbar = () => {
1919
status.textContent = isHidden ? 'start ►' : 'stop ⏹';
2020
ReactScanInternals.isPaused = isHidden;
2121
if (ReactScanInternals.isPaused) {
22-
ReactScanInternals.activeOutlinesMap.clear();
22+
ReactScanInternals.activeOutlines = [];
2323
ReactScanInternals.scheduledOutlines = [];
2424
}
2525
if ('localStorage' in globalThis) {

src/core/web/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const onIdle = (callback: () => void) => {
1212
return setTimeout(callback, 0);
1313
};
1414

15-
export const throttle = <T extends (...args: any[]) => void>(
15+
export const throttle = <T extends (...args: any[]) => any>(
1616
callback: T,
1717
delay: number,
1818
) => {
@@ -21,7 +21,7 @@ export const throttle = <T extends (...args: any[]) => void>(
2121
const now = Date.now();
2222
if (now - lastCall >= delay) {
2323
lastCall = now;
24-
callback(...args);
24+
return callback(...args);
2525
}
2626
};
2727
};

test/src/index.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,13 @@ export const App = () => {
159159
className={`tab-button ${activeTab === 'nextjs-pages' ? 'active' : ''}`}
160160
onClick={() => setActiveTab('nextjs-pages')}
161161
>
162-
Next.js (Pages)
162+
Next.js (pages)
163163
</button>
164164
<button
165165
className={`tab-button ${activeTab === 'nextjs-app' ? 'active' : ''}`}
166166
onClick={() => setActiveTab('nextjs-app')}
167167
>
168-
Next.js (App)
168+
Next.js (app)
169169
</button>
170170
<button
171171
className={`tab-button ${activeTab === 'vite' ? 'active' : ''}`}
@@ -200,7 +200,7 @@ export default function Document() {
200200
return (
201201
<Html lang="en">
202202
<Head>
203-
+ <script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
203+
<script src="https://unpkg.com/react-scan/dist/auto.global.js" async></script>
204204
{/* rest of your scripts go under */}
205205
</Head>
206206
<body>
@@ -243,7 +243,7 @@ export default function Document() {
243243
return (
244244
<html lang="en">
245245
<head>
246-
<script src="https://unpkg.com/react-scan/dist/auto.global.js"></script>
246+
<script src="https://unpkg.com/react-scan/dist/auto.global.js" async></script>
247247
{/* rest of your scripts go under */}
248248
</head>
249249
<body>{children}</body>

0 commit comments

Comments
 (0)