Skip to content

Commit 5eaeeb4

Browse files
committed
refactor(core): Optimize color calculation and outline rendering
1 parent f003213 commit 5eaeeb4

File tree

8 files changed

+82
-86
lines changed

8 files changed

+82
-86
lines changed

README.md

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

431434
## Acknowledgments

src/core/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ interface Options {
7070
*/
7171
resetCountTimeout?: number;
7272

73+
/**
74+
* Maximum number of renders for red indicator
75+
*
76+
* @default 20
77+
*/
78+
maxRenders?: number;
79+
7380
onCommitStart?: () => void;
7481
onRender?: (fiber: Fiber, render: Render) => void;
7582
onCommitFinish?: () => void;

src/core/instrumentation/fiber.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ export const didFiberRender = (fiber: Fiber): boolean => {
128128

129129
const prevProps = fiber.alternate?.memoizedProps || {};
130130
const flags = fiber.flags ?? (fiber as any).effectTag ?? 0;
131-
const subtreeFlags = fiber.subtreeFlags ?? 0;
132131

133132
switch (fiber.tag) {
134133
case ClassComponentTag:
@@ -137,10 +136,7 @@ export const didFiberRender = (fiber: Fiber): boolean => {
137136
case ForwardRefTag:
138137
case MemoComponentTag:
139138
case SimpleMemoComponentTag:
140-
return (
141-
(flags & PerformedWorkFlag) === PerformedWorkFlag &&
142-
(subtreeFlags & PerformedWorkFlag) === PerformedWorkFlag
143-
);
139+
return (flags & PerformedWorkFlag) === PerformedWorkFlag;
144140
default:
145141
// Host nodes (DOM, root, etc.)
146142
if (!fiber.alternate) return true;

src/core/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const getLabelText = (renders: Render[]) => {
3737
([, a], [, b]) => b.count - a.count,
3838
);
3939

40+
const parts: string[] = [];
4041
for (const [name, { count, trigger, forget }] of sortedComponents) {
4142
let text = name;
4243
if (count > 1) {
@@ -48,9 +49,11 @@ export const getLabelText = (renders: Render[]) => {
4849
if (forget) {
4950
text = `${text} ✨`;
5051
}
51-
labelText += text;
52+
parts.push(text);
5253
}
5354

55+
labelText = parts.join(' ');
56+
5457
if (!labelText.length) return null;
5558
if (labelText.length > 20) {
5659
labelText = `${labelText.slice(0, 20)}…`;

src/core/web/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ export const createOverlay = () => {
1313
const dpi = window.devicePixelRatio;
1414
canvas.width = dpi * window.innerWidth;
1515
canvas.height = dpi * window.innerHeight;
16-
ctx?.scale(dpi, dpi);
16+
17+
if (ctx) {
18+
ctx.resetTransform();
19+
ctx.scale(dpi, dpi);
20+
}
21+
1722
resizeScheduled = false;
1823
};
1924

src/core/web/outline.ts

Lines changed: 56 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export interface ActiveOutline {
2020
totalFrames: number;
2121
resolve: () => void;
2222
text: string | null;
23+
color: { r: number; g: number; b: number };
2324
}
2425

2526
export interface PaintedOutline {
@@ -114,6 +115,7 @@ export const mergeOutlines = (outlines: PendingOutline[]) => {
114115

115116
export const recalcOutlines = throttle(() => {
116117
const { scheduledOutlines, activeOutlinesMap } = ReactScanInternals;
118+
117119
for (let i = scheduledOutlines.length - 1; i >= 0; i--) {
118120
const outline = scheduledOutlines[i];
119121
const rect = getRect(outline.domNode);
@@ -126,13 +128,14 @@ export const recalcOutlines = throttle(() => {
126128

127129
const activeOutlines = Array.from(activeOutlinesMap.values());
128130

129-
for (let i = 0, len = activeOutlines.length; i < len; i++) {
131+
for (let i = activeOutlines.length - 1; i >= 0; i--) {
130132
const activeOutline = activeOutlines[i];
133+
if (!activeOutline) continue;
131134
const { outline } = activeOutline;
132135
const rect = getRect(outline.domNode);
133136
if (!rect) {
134-
activeOutlines.splice(i, 1);
135137
activeOutlinesMap.delete(getOutlineKey(outline));
138+
activeOutline.resolve();
136139
continue;
137140
}
138141
outline.rect = rect;
@@ -209,13 +212,10 @@ export const paintOutline = (
209212
) => {
210213
return new Promise<void>((resolve) => {
211214
const unstable = isOutlineUnstable(outline);
212-
213215
const totalFrames = unstable ? 30 : 10;
214216
const frame = 0;
215217
const alpha = 0.8;
216218

217-
const text: string | null = getLabelText(outline.renders);
218-
219219
const { options } = ReactScanInternals;
220220
options.onPaintStart?.(outline);
221221
if (options.log) {
@@ -226,6 +226,21 @@ export const paintOutline = (
226226
const existingActiveOutline =
227227
ReactScanInternals.activeOutlinesMap.get(outlineKey);
228228

229+
const renderCount = existingActiveOutline
230+
? existingActiveOutline.outline.renders.length + outline.renders.length
231+
: outline.renders.length;
232+
const maxRenders = ReactScanInternals.options.maxRenders;
233+
const t = Math.min(renderCount / (maxRenders ?? 20), 1);
234+
235+
const startColor = { r: 115, g: 97, b: 230 };
236+
const endColor = { r: 185, g: 49, b: 115 };
237+
238+
const r = Math.round(startColor.r + t * (endColor.r - startColor.r));
239+
const g = Math.round(startColor.g + t * (endColor.g - startColor.g));
240+
const b = Math.round(startColor.b + t * (endColor.b - startColor.b));
241+
242+
const color = { r, g, b };
243+
229244
if (existingActiveOutline) {
230245
existingActiveOutline.outline.renders.push(...outline.renders);
231246
existingActiveOutline.frame = frame;
@@ -234,6 +249,7 @@ export const paintOutline = (
234249
existingActiveOutline.text = getLabelText(
235250
existingActiveOutline.outline.renders,
236251
);
252+
existingActiveOutline.color = color;
237253
existingActiveOutline.resolve = () => {
238254
resolve();
239255
options.onPaintFinish?.(existingActiveOutline.outline);
@@ -248,7 +264,8 @@ export const paintOutline = (
248264
resolve();
249265
options.onPaintFinish?.(outline);
250266
},
251-
text,
267+
text: getLabelText(outline.renders),
268+
color,
252269
});
253270
}
254271

@@ -261,65 +278,42 @@ export const paintOutline = (
261278
export const fadeOutOutline = (ctx: CanvasRenderingContext2D) => {
262279
const { activeOutlinesMap } = ReactScanInternals;
263280

281+
ctx.save();
282+
ctx.resetTransform();
264283
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
265-
266-
const combinedPath = new Path2D();
267-
268-
let maxStrokeAlpha = 0;
269-
let maxFillAlpha = 0;
270-
271-
const pendingLabeledOutlines: PaintedOutline[] = [];
284+
ctx.restore();
272285

273286
const activeOutlines = Array.from(activeOutlinesMap.values());
274287

275288
for (let i = 0, len = activeOutlines.length; i < len; i++) {
276289
const activeOutline = activeOutlines[i];
277-
const { outline, frame, totalFrames } = activeOutline;
290+
const { outline, frame, totalFrames, color } = activeOutline;
278291
const { rect } = outline;
279292
const unstable = isOutlineUnstable(outline);
280293

281294
const alphaScalar = unstable ? 0.8 : 0.2;
282295

283296
activeOutline.alpha = alphaScalar * (1 - frame / totalFrames);
284297

285-
maxStrokeAlpha = Math.max(maxStrokeAlpha, activeOutline.alpha);
286-
maxFillAlpha = Math.max(maxFillAlpha, activeOutline.alpha * 0.1);
287-
288-
combinedPath.rect(rect.x, rect.y, rect.width, rect.height);
289-
290-
if (unstable) {
291-
pendingLabeledOutlines.push({
292-
alpha: activeOutline.alpha,
293-
outline,
294-
text: activeOutline.text,
295-
});
296-
}
297-
298-
activeOutline.frame++;
299-
300-
if (activeOutline.frame > activeOutline.totalFrames) {
301-
activeOutlinesMap.delete(getOutlineKey(outline));
302-
activeOutline.resolve();
303-
}
304-
}
305-
306-
ctx.save();
307-
308-
ctx.strokeStyle = `rgba(${colorRef.current}, ${maxStrokeAlpha})`;
309-
ctx.lineWidth = 1;
310-
ctx.fillStyle = `rgba(${colorRef.current}, ${maxFillAlpha})`;
298+
const strokeAlpha = activeOutline.alpha;
299+
const fillAlpha = activeOutline.alpha * 0.1;
311300

312-
ctx.stroke(combinedPath);
313-
ctx.fill(combinedPath);
314-
315-
ctx.restore();
316-
317-
for (let i = 0, len = pendingLabeledOutlines.length; i < len; i++) {
318-
const { alpha, outline, text } = pendingLabeledOutlines[i];
319-
const { rect } = outline;
320301
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();
312+
313+
if (unstable && activeOutline.text) {
314+
const { text } = activeOutline;
315+
ctx.save();
321316

322-
if (text) {
323317
ctx.font = `10px ${MONO_FONT}`;
324318
const textMetrics = ctx.measureText(text);
325319
const textWidth = textMetrics.width;
@@ -328,19 +322,26 @@ export const fadeOutOutline = (ctx: CanvasRenderingContext2D) => {
328322
const labelX: number = rect.x;
329323
const labelY: number = rect.y - textHeight - 4;
330324

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

334-
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
328+
ctx.fillStyle = `rgba(255,255,255,${strokeAlpha})`;
335329
ctx.fillText(text, labelX + 2, labelY + textHeight);
330+
331+
ctx.restore();
336332
}
337333

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

341-
if (activeOutlines.length) {
342-
animationFrameId = requestAnimationFrame(() => fadeOutOutline(ctx));
343-
} else {
342+
if (activeOutlinesMap.size === 0) {
344343
animationFrameId = null;
344+
} else {
345+
animationFrameId = requestAnimationFrame(() => fadeOutOutline(ctx));
345346
}
346347
};

src/core/web/perf-observer.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,14 @@ import { colorRef } from './outline';
55
export const createPerfObserver = () => {
66
const observer = new PerformanceObserver(NO_OP);
77

8-
observer.observe({ entryTypes: ['measure'] });
8+
observer.observe({ entryTypes: ['longtask'] });
99

1010
return observer;
1111
};
1212

1313
export const recalcOutlineColor = (entries: PerformanceEntryList) => {
1414
const { longTaskThreshold } = ReactScanInternals.options;
15-
const minDuration = longTaskThreshold ?? 1;
16-
const maxDuration = 1000;
15+
const minDuration = longTaskThreshold ?? 50;
1716

1817
let maxDurationFound = 0;
1918

@@ -24,24 +23,6 @@ export const recalcOutlineColor = (entries: PerformanceEntryList) => {
2423
}
2524
}
2625

27-
if (maxDurationFound > minDuration) {
28-
const t = Math.min(
29-
Math.max(
30-
(maxDurationFound - minDuration) / (maxDuration - minDuration),
31-
0,
32-
),
33-
1,
34-
);
35-
36-
const startColor = { r: 115, g: 97, b: 230 }; // Base color
37-
const endColor = { r: 185, g: 49, b: 115 }; // Color for longest tasks
38-
39-
const r = Math.round(startColor.r + t * (endColor.r - startColor.r));
40-
const g = Math.round(startColor.g + t * (endColor.g - startColor.g));
41-
const b = Math.round(startColor.b + t * (endColor.b - startColor.b));
42-
43-
colorRef.current = `${r},${g},${b}`;
44-
} else {
45-
colorRef.current = '115,97,230'; // Default color when there are no significant entries
46-
}
26+
colorRef.current =
27+
maxDurationFound > minDuration ? '185,49,115' : '115,97,230';
4728
};

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.activeOutlines = [];
22+
ReactScanInternals.activeOutlinesMap.clear();
2323
ReactScanInternals.scheduledOutlines = [];
2424
}
2525
if ('localStorage' in globalThis) {

0 commit comments

Comments
 (0)