/** * Профиль перерисовок React через минимальный __REACT_DEVTOOLS_GLOBAL_HOOK__ (Task 7). * Нужна профилирующая сборка (react-dom/profiling): только в ней fiber'ы несут actualDuration. * Init-script REACT_HOOK_SCRIPT ставится ДО загрузки приложения (ctx.addInitScript). * profileStart(page) / profileStop(page) -> {commits, top: [{component, renders, self_ms}] (топ-20 по self_ms), ...}. * * Правило «компонент отрисовался в коммите»: fiber.flags & PerformedWork (1) для function/class/forwardRef/ * simple-memo (теги 0, 1, 11, 15). Обход как у DevTools: в потомков идём, только если fiber новый (нет alternate) * или fiber.child !== alternate.child (иначе поддерево пропущено bailout'ом, а его флаги устарели). * self_ms = actualDuration - сумма actualDuration обойдённых детей. */ export const REACT_HOOK_SCRIPT = `(() => { if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) return; const COMPONENT_TAGS = { 0: 1, 1: 1, 11: 1, 15: 1 }; const st = { active: false, renderers: 0, commitsTotal: 0, commits: 0, sawActualDuration: false, stats: new Map(), }; const nameOf = (f) => { const t = f.type; if (typeof t === 'function') return t.displayName || t.name || '(anonymous)'; if (t && typeof t === 'object') { const r = t.render; const n = t.displayName || (r && (r.displayName || r.name)); return n ? (f.tag === 11 ? 'ForwardRef(' + n + ')' : n) : '(anonymous)'; } return '(anonymous)'; }; const visit = (root) => { const stack = [root]; while (stack.length) { const f = stack.pop(); const alt = f.alternate; const descend = alt === null || alt === undefined || f.child !== alt.child; if (COMPONENT_TAGS[f.tag] && (f.flags & 1) === 1) { let kids = 0; if (descend) for (let c = f.child; c; c = c.sibling) kids += c.actualDuration || 0; const self = Math.max((f.actualDuration || 0) - kids, 0); const name = nameOf(f); const s = st.stats.get(name) || { renders: 0, self_ms: 0 }; s.renders += 1; s.self_ms += self; st.stats.set(name, s); } if (descend) for (let c = f.child; c; c = c.sibling) stack.push(c); } }; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { supportsFiber: true, isDisabled: false, renderers: new Map(), inject(r) { st.renderers += 1; this.renderers.set(st.renderers, r); return st.renderers; }, checkDCE() {}, onScheduleFiberRoot() {}, onCommitFiberUnmount() {}, onPostCommitFiberRoot() {}, onCommitFiberRoot(id, root) { st.commitsTotal += 1; // профилирующая сборка: у HostRoot есть числовой actualDuration (в prod-сборке поля нет) if (!st.sawActualDuration) { const cur = root.current; if (cur && typeof cur.actualDuration === 'number' && cur.actualDuration >= 0) st.sawActualDuration = true; } if (!st.active) return; st.commits += 1; try { visit(root.current); } catch (e) { st.error = String(e); } }, }; window.__reactProfile = st; })();`; export interface RenderRow { component: string; renders: number; self_ms: number } export interface ProfileResult { commits: number; total_renders: number; total_self_ms: number; top: RenderRow[]; error?: string; } export interface HookInfo { renderers: number; commits_total: number; saw_actual_duration: boolean } export async function profileStart(page: Page_): Promise { await page.evaluate(() => { const st = (window as any).__reactProfile; if (!st) throw new Error('React hook is not installed (REACT_HOOK_SCRIPT)'); st.stats = new Map(); st.commits = 0; st.error = undefined; st.active = true; }); } export async function profileStop(page: Page_, topN = 20): Promise { return page.evaluate((n) => { const st = (window as any).__reactProfile; st.active = false; const rows = Array.from(st.stats.entries() as Iterable<[string, { renders: number; self_ms: number }]>) .map(([component, v]) => ({ component, renders: v.renders, self_ms: v.self_ms })) .sort((a, b) => b.self_ms - a.self_ms || b.renders - a.renders); return { commits: st.commits as number, total_renders: rows.reduce((a, r) => a + r.renders, 0), total_self_ms: rows.reduce((a, r) => a + r.self_ms, 0), top: rows.slice(0, n), ...(st.error ? { error: st.error as string } : {}), }; }, topN); } export async function hookInfo(page: Page_): Promise { return page.evaluate(() => { const st = (window as any).__reactProfile; return { renderers: st?.renderers ?? 0, commits_total: st?.commitsTotal ?? 0, saw_actual_duration: !!st?.sawActualDuration }; }); } import type { Page as Page_ } from '@playwright/test';