perf(web): React.memo on list rows (EventRow, Row, HeldRow, FleetRow, EmptyRow, LogRow, QueueTable + badges), useMemo for filters/derived (Stream/Reports/Queue/Calendar/Dashboard), route code-split (lazy+Suspense in App) to cut main chunk; pure helpers unchanged
Some checks failed
CI / verify (push) Failing after 5m46s

This commit is contained in:
Natalie 2026-06-29 22:03:08 -04:00
parent 02a67a56ca
commit 1708effd57
11 changed files with 186 additions and 144 deletions

View file

@ -1,26 +1,27 @@
import { useEffect } from 'react';
import { lazy, Suspense, useEffect } from 'react';
import { navigate, useHashRoute } from './useHashRoute';
import { SectionLabel } from './ui';
import { ControlView } from './views/ControlView';
import { TriageView } from './views/TriageView';
import { DetailView } from './views/DetailView';
import { ReportsView } from './views/ReportsView';
import { MarketsView } from './views/MarketsView';
import { QueueView } from './views/QueueView';
import { PastebinView } from './views/PastebinView';
import { CampaignsView } from './views/CampaignsView';
import { IntroThreadView } from './views/IntroThreadView';
import { HostsView } from './views/HostsView';
import { SettingsView } from './views/SettingsView';
import { CalendarView } from './views/CalendarView';
import { ProspectsView } from './views/ProspectsView';
import { StreamView } from './views/StreamView';
import { DashboardView } from './views/DashboardView';
import { BackfillView } from './views/BackfillView';
import { VoiceView } from './views/VoiceView';
import { AutopilotView } from './views/AutopilotView';
import { ModelView } from './views/ModelView';
const ControlView = lazy(() => import('./views/ControlView').then((m) => ({ default: m.ControlView })));
const TriageView = lazy(() => import('./views/TriageView').then((m) => ({ default: m.TriageView })));
const DetailView = lazy(() => import('./views/DetailView').then((m) => ({ default: m.DetailView })));
const ReportsView = lazy(() => import('./views/ReportsView').then((m) => ({ default: m.ReportsView })));
const MarketsView = lazy(() => import('./views/MarketsView').then((m) => ({ default: m.MarketsView })));
const QueueView = lazy(() => import('./views/QueueView').then((m) => ({ default: m.QueueView })));
const PastebinView = lazy(() => import('./views/PastebinView').then((m) => ({ default: m.PastebinView })));
const CampaignsView = lazy(() => import('./views/CampaignsView').then((m) => ({ default: m.CampaignsView })));
const IntroThreadView = lazy(() => import('./views/IntroThreadView').then((m) => ({ default: m.IntroThreadView })));
const HostsView = lazy(() => import('./views/HostsView').then((m) => ({ default: m.HostsView })));
const SettingsView = lazy(() => import('./views/SettingsView').then((m) => ({ default: m.SettingsView })));
const CalendarView = lazy(() => import('./views/CalendarView').then((m) => ({ default: m.CalendarView })));
const ProspectsView = lazy(() => import('./views/ProspectsView').then((m) => ({ default: m.ProspectsView })));
const StreamView = lazy(() => import('./views/StreamView').then((m) => ({ default: m.StreamView })));
const DashboardView = lazy(() => import('./views/DashboardView').then((m) => ({ default: m.DashboardView })));
const BackfillView = lazy(() => import('./views/BackfillView').then((m) => ({ default: m.BackfillView })));
const VoiceView = lazy(() => import('./views/VoiceView').then((m) => ({ default: m.VoiceView })));
const AutopilotView = lazy(() => import('./views/AutopilotView').then((m) => ({ default: m.AutopilotView })));
const ModelView = lazy(() => import('./views/ModelView').then((m) => ({ default: m.ModelView })));
interface NavItem {
readonly view: string;
@ -151,49 +152,55 @@ export function App(): JSX.Element {
}
function ViewRouter({ view, param }: { view: string; param: string | null }): JSX.Element {
switch (view) {
// ── built ──
case 'triage':
return <TriageView />;
case 'prospect':
return param ? <DetailView handle={param} /> : <TriageView />;
case 'intro':
return param ? <IntroThreadView id={param} /> : <ReportsView />;
case 'queue':
return <QueueView />;
case 'campaigns':
return <CampaignsView />;
case 'reports':
return <ReportsView />;
case 'markets':
return <MarketsView />;
case 'pastebin':
return <PastebinView />;
case 'services':
case 'hosts':
return <HostsView />;
case 'control':
return <ControlView />;
// ── planned (P-1 shell parity; built in Waves A/B) ──
case 'dashboard':
return <DashboardView />;
case 'prospects':
return <ProspectsView />;
case 'stream':
return <StreamView />;
case 'backfill':
return <BackfillView />;
case 'calendar':
return <CalendarView />;
case 'model':
return <ModelView />;
case 'voice':
return <VoiceView />;
case 'settings':
return <SettingsView />;
case 'autopilot':
return <AutopilotView />;
default:
return <ControlView />;
}
return (
<Suspense fallback={<div style={{ padding: 20, color: '#6b6170' }}>Loading view</div>}>
{(() => {
switch (view) {
// ── built ──
case 'triage':
return <TriageView />;
case 'prospect':
return param ? <DetailView handle={param} /> : <TriageView />;
case 'intro':
return param ? <IntroThreadView id={param} /> : <ReportsView />;
case 'queue':
return <QueueView />;
case 'campaigns':
return <CampaignsView />;
case 'reports':
return <ReportsView />;
case 'markets':
return <MarketsView />;
case 'pastebin':
return <PastebinView />;
case 'services':
case 'hosts':
return <HostsView />;
case 'control':
return <ControlView />;
// ── planned (P-1 shell parity; built in Waves A/B) ──
case 'dashboard':
return <DashboardView />;
case 'prospects':
return <ProspectsView />;
case 'stream':
return <StreamView />;
case 'backfill':
return <BackfillView />;
case 'calendar':
return <CalendarView />;
case 'model':
return <ModelView />;
case 'voice':
return <VoiceView />;
case 'settings':
return <SettingsView />;
case 'autopilot':
return <AutopilotView />;
default:
return <ControlView />;
}
})()}
</Suspense>
);
}

View file

@ -1,7 +1,9 @@
import { memo } from 'react';
import { getActivity, type ActivityItem } from '../api';
import { formatTime, usePoll } from '../usePoll';
function Row({ item }: { item: ActivityItem }): JSX.Element {
const Row = memo(function Row({ item }: { item: ActivityItem }): JSX.Element {
return (
<div className="logrow">
<span className="logrow__time">{formatTime(item.createdAt)}</span>
@ -11,7 +13,7 @@ function Row({ item }: { item: ActivityItem }): JSX.Element {
{item.holdReason ? <span className="logrow__reason">{item.holdReason}</span> : null}
</div>
);
}
});
export function ActivityFeed(): JSX.Element {
const { data, error, loading } = usePoll(() => getActivity(50), 15_000);

View file

@ -1,7 +1,9 @@
import { memo } from 'react';
import { getHeldQueue, type ActivityItem } from '../api';
import { formatTime, usePoll } from '../usePoll';
function HeldRow({ item }: { item: ActivityItem }): JSX.Element {
const HeldRow = memo(function HeldRow({ item }: { item: ActivityItem }): JSX.Element {
return (
<div className="held">
<div className="held__top">
@ -14,7 +16,7 @@ function HeldRow({ item }: { item: ActivityItem }): JSX.Element {
</div>
</div>
);
}
});
export function HeldQueue(): JSX.Element {
const { data, error, loading } = usePoll(() => getHeldQueue(50), 30_000);

View file

@ -1,3 +1,5 @@
import { useMemo } from 'react';
import { usePoll } from '../usePoll';
import { getBookings, type BookingItem } from '../api';
import { navigate } from '../useHashRoute';
@ -13,10 +15,14 @@ export function CalendarView(): JSX.Element {
if (loading || !data) return <Card><Muted>Loading bookings</Muted></Card>;
// Group by day (already grouped in backend sample, but client can too)
const groups = data.items.reduce<Record<string, BookingItem[]>>((acc, b) => {
(acc[b.day] ||= []).push(b);
return acc;
}, {});
const groups = useMemo(
() =>
data.items.reduce<Record<string, BookingItem[]>>((acc, b) => {
(acc[b.day] ||= []).push(b);
return acc;
}, {}),
[data.items],
);
return (
<VStack $gap={12}>

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useMemo, useState } from 'react';
import {
Button,
@ -72,22 +72,30 @@ export function DashboardView(): JSX.Element {
const { kpis, density, insights, actions } = data;
const densityRows: readonly { key: string; count: number }[] = density.map((d) => ({
key: `${d.hour}h`,
count: d.count,
}));
const densityRows: readonly { key: string; count: number }[] = useMemo(
() => density.map((d) => ({ key: `${d.hour}h`, count: d.count })),
[density],
);
const kpiEntries = Object.entries(kpis);
const volumeRows: readonly { key: string; count: number }[] = kpiEntries
.filter(([, v]) => typeof v === 'number')
.map(([k, v]) => ({
key: k.replace(/7d|InRange/gi, '').trim() || k,
count: Number(v),
}));
const kpiEntries = useMemo(() => Object.entries(kpis), [kpis]);
const volumeRows: readonly { key: string; count: number }[] = useMemo(
() =>
kpiEntries
.filter(([, v]) => typeof v === 'number')
.map(([k, v]) => ({
key: k.replace(/7d|InRange/gi, '').trim() || k,
count: Number(v),
})),
[kpiEntries],
);
const peak = density.length
? density.reduce((a, b) => (b.count > a.count ? b : a))
: null;
const peak = useMemo(
() =>
density.length
? density.reduce((a, b) => (b.count > a.count ? b : a))
: null,
[density],
);
return (
<VStack $gap={14}>

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import {
getGpuStatus,
@ -333,7 +333,7 @@ export function HostsView(): JSX.Element {
);
}
function FleetRow({ label, ip, highlight }: { label: string; ip: string; highlight?: boolean }): JSX.Element {
const FleetRow = memo(function FleetRow({ label, ip, highlight }: { label: string; ip: string; highlight?: boolean }): JSX.Element {
return (
<div
style={{
@ -352,7 +352,7 @@ function FleetRow({ label, ip, highlight }: { label: string; ip: string; highlig
</span>
</div>
);
}
});
function CodeSnippet({ label, code }: { label: string; code: string }): JSX.Element {
const [copied, setCopied] = useState(false);

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
abortTask,
@ -82,9 +82,13 @@ export function QueueView(): JSX.Element {
// Handle search is client-side over the polled items (type/priority hit the API).
const needle = search.trim().toLowerCase();
const visible = needle
? tasks.filter((t) => `${t.handle ?? ''} ${t.prospectName ?? ''}`.toLowerCase().includes(needle))
: tasks;
const visible = useMemo(
() =>
needle
? tasks.filter((t) => `${t.handle ?? ''} ${t.prospectName ?? ''}`.toLowerCase().includes(needle))
: tasks,
[needle, tasks],
);
const toggle = (id: string): void =>
setSelected((prev) => {
@ -141,29 +145,29 @@ export function QueueView(): JSX.Element {
};
// ─── Row actions ────────────────────────────────────────────────────────────
const onRun = (t: ProspectorTask): void => {
const onRun = useCallback((t: ProspectorTask): void => {
if (!window.confirm(`Run ${t.id} now? A send-type task may dispatch a message.`)) return;
void guard(() => runTask(t.id));
};
const onEscalate = (t: ProspectorTask): void => {
}, [guard]);
const onEscalate = useCallback((t: ProspectorTask): void => {
if (!window.confirm(`Escalate ${t.id} to Quinn (human owner)?`)) return;
void guard(() => escalateTask(t.id));
};
const onCancel = (t: ProspectorTask): void => {
}, [guard]);
const onCancel = useCallback((t: ProspectorTask): void => {
if (!window.confirm(`Cancel ${t.id}? It is removed from the runner queue.`)) return;
void guard(() => cancelTask(t.id));
};
const onAbort = (t: ProspectorTask): void => {
}, [guard]);
const onAbort = useCallback((t: ProspectorTask): void => {
if (!window.confirm(`Abort ${t.id} mid-run?`)) return;
void guard(() => abortTask(t.id));
};
const onRequeue = (t: ProspectorTask): void => {
}, [guard]);
const onRequeue = useCallback((t: ProspectorTask): void => {
void guard(() => requeueTask(t.id));
};
const onViewProspect = (handle: string): void => {
}, [guard]);
const onViewProspect = useCallback((handle: string): void => {
setDetailTask(null);
navigate('prospect', handle);
};
}, []);
return (
<div className="view-stack">

View file

@ -1,4 +1,4 @@
import { useState, type CSSProperties, type ReactNode } from 'react';
import { memo, useMemo, useState, type CSSProperties, type ReactNode } from 'react';
import {
getProviderGraph,
@ -37,10 +37,14 @@ function ProviderGraphSection(): JSX.Element {
const min = minMr ? Number(minMr) : 0;
const text = q.trim().toLowerCase();
const nodes: GraphNode[] = data?.nodes ?? [];
const rows = nodes.filter(
(n) =>
(!text || n.handle.toLowerCase().includes(text)) &&
(!min || (n.compositeMrScore ?? 0) >= min),
const rows = useMemo(
() =>
nodes.filter(
(n) =>
(!text || n.handle.toLowerCase().includes(text)) &&
(!min || (n.compositeMrScore ?? 0) >= min),
),
[nodes, text, min],
);
return (
@ -85,7 +89,7 @@ function WarmIntrosSection(): JSX.Element {
const [statusFilter, setStatusFilter] = useState('');
const items: IntroThread[] = data?.items ?? [];
const rows = items.filter((i) => !statusFilter || i.status === statusFilter);
const rows = useMemo(() => items.filter((i) => !statusFilter || i.status === statusFilter), [items, statusFilter]);
return (
<section className="card">
@ -171,8 +175,11 @@ function MarketplaceSection(): JSX.Element {
const text = q.trim().toLowerCase();
const items: MarketplaceRouting[] = data?.items ?? [];
const rows = items.filter(
(r) => (!text || r.prospectHandle.toLowerCase().includes(text)) && (!status || r.status === status),
const rows = useMemo(
() => items.filter(
(r) => (!text || r.prospectHandle.toLowerCase().includes(text)) && (!status || r.status === status),
),
[items, text, status],
);
return (
@ -250,13 +257,13 @@ function Td({ children }: { children: ReactNode }): JSX.Element {
return <td style={CELL}>{children}</td>;
}
function EmptyRow({ span, text }: { span: number; text: string }): JSX.Element {
const EmptyRow = memo(function EmptyRow({ span, text }: { span: number; text: string }): JSX.Element {
return (
<tr>
<td colSpan={span} style={{ ...CELL, color: 'var(--muted)' }}>{text}</td>
</tr>
);
}
});
const STATUS_PILL: Record<string, string> = {
proposed: 'pill--hold',
@ -270,18 +277,18 @@ const STATUS_PILL: Record<string, string> = {
rejected: 'pill--bad',
};
function StatusBadge({ status }: { status: string }): JSX.Element {
const StatusBadge = memo(function StatusBadge({ status }: { status: string }): JSX.Element {
return <span className={`pill ${STATUS_PILL[status] ?? 'pill'}`}>{status}</span>;
}
});
function Stage({ label, value, accent }: { label: string; value: number; accent?: boolean }): JSX.Element {
const Stage = memo(function Stage({ label, value, accent }: { label: string; value: number; accent?: boolean }): JSX.Element {
return (
<div className="stage">
<div className={`stage__value${accent ? ' stage__value--accent' : ''}`}>{value}</div>
<div className="stage__label">{label}</div>
</div>
);
}
});
function VolumeChart({ points }: { points: readonly VolumePoint[] }): JSX.Element {
if (points.length === 0) return <div className="muted">No activity in range.</div>;
@ -302,7 +309,7 @@ function VolumeChart({ points }: { points: readonly VolumePoint[] }): JSX.Elemen
);
}
function ClassificationBars({ rows, bilingual }: { rows: readonly CountRow[]; bilingual: boolean }): JSX.Element {
const ClassificationBars = memo(function ClassificationBars({ rows, bilingual }: { rows: readonly CountRow[]; bilingual: boolean }): JSX.Element {
if (rows.length === 0) return <div className="muted">No classifications in range.</div>;
const max = Math.max(1, ...rows.map((r) => r.count));
return (
@ -318,4 +325,4 @@ function ClassificationBars({ rows, bilingual }: { rows: readonly CountRow[]; bi
))}
</div>
);
}
});

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { memo, useMemo, useState } from 'react';
import { getStream, type StreamEvent } from '../api';
import { navigate } from '../useHashRoute';
@ -57,7 +57,7 @@ function getDirGlyphAndColor(e: StreamEvent): { glyph: string; color: string } {
};
}
function EventRow({ event }: { event: StreamEvent }): JSX.Element {
const EventRow = memo(function EventRow({ event }: { event: StreamEvent }): JSX.Element {
const { glyph, color: dirColor } = getDirGlyphAndColor(event);
const badge = getKindBadge(event);
const tone = getPillTone(event);
@ -133,7 +133,7 @@ function EventRow({ event }: { event: StreamEvent }): JSX.Element {
</span>
</button>
);
}
});
export function StreamView(): JSX.Element {
const { data, error, loading } = usePoll(() => getStream(100), 20000);
@ -143,11 +143,11 @@ export function StreamView(): JSX.Element {
const items: StreamEvent[] = data?.items ?? [];
const filtered = items.filter((e) => {
const filtered = useMemo(() => items.filter((e) => {
const kindOk = streamKind === 'all' || e.kind === streamKind;
const dirOk = streamDir === 'all' || e.dir === streamDir;
return kindOk && dirOk;
});
}), [items, streamKind, streamDir]);
const showLoading = loading && items.length === 0;
const showError = !!error;

View file

@ -1,4 +1,4 @@
import type { CSSProperties } from 'react';
import { memo, type CSSProperties } from 'react';
import type { ProspectorTask, TaskPriority, TaskStatus, TaskType } from '../../api';
@ -25,7 +25,7 @@ export interface QueueTableProps {
* Empty states are real: the table head always renders and an in-table row
* states the clear/idle condition rather than hiding the surface.
*/
export function QueueTable(props: QueueTableProps): JSX.Element {
export const QueueTable = memo(function QueueTable(props: QueueTableProps): JSX.Element {
const { tab, tasks } = props;
const colCount = tab === 'queued' ? 8 : tab === 'processing' ? 6 : 6;
@ -145,7 +145,7 @@ export function QueueTable(props: QueueTableProps): JSX.Element {
</tbody>
</table>
);
}
});
// ─── Shared badges + time helpers (imported by TaskDetailModal too — DRY) ──────
@ -171,7 +171,7 @@ const STATUS_COLORS: Record<TaskStatus, { bg: string; fg: string }> = {
aborted: { bg: 'rgba(248, 113, 113, 0.18)', fg: '#f87171' },
};
export function Badge({ bg, fg, label }: { bg: string; fg: string; label: string }): JSX.Element {
export const Badge = memo(function Badge({ bg, fg, label }: { bg: string; fg: string; label: string }): JSX.Element {
return (
<span
style={{
@ -188,22 +188,22 @@ export function Badge({ bg, fg, label }: { bg: string; fg: string; label: string
{label}
</span>
);
}
});
export function TypeBadge({ type }: { type: TaskType }): JSX.Element {
export const TypeBadge = memo(function TypeBadge({ type }: { type: TaskType }): JSX.Element {
const c = TYPE_COLORS[type];
return <Badge bg={c.bg} fg={c.fg} label={type} />;
}
});
export function PriorityBadge({ priority }: { priority: TaskPriority }): JSX.Element {
export const PriorityBadge = memo(function PriorityBadge({ priority }: { priority: TaskPriority }): JSX.Element {
const c = PRIORITY_COLORS[priority];
return <Badge bg={c.bg} fg={c.fg} label={priority} />;
}
});
export function StatusBadge({ status }: { status: TaskStatus }): JSX.Element {
export const StatusBadge = memo(function StatusBadge({ status }: { status: TaskStatus }): JSX.Element {
const c = STATUS_COLORS[status];
return <Badge bg={c.bg} fg={c.fg} label={status} />;
}
});
export function shortId(id: string): string {
return id.length > 8 ? `${id.slice(0, 8)}` : id;

View file

@ -1,7 +1,16 @@
import { useEffect, useRef } from 'react';
import { memo, useEffect, useRef } from 'react';
import type { TaskLogEntry } from '../../api';
const LogRow = memo(function LogRow({ e }: { e: TaskLogEntry }): JSX.Element {
return (
<div className="logrow">
<span className="logrow__time">{clock(e.createdAt)}</span>
<span style={{ color: '#6ee7b7', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.message}</span>
</div>
);
});
/**
* Terminal-style runner activity panel. Renders the auto-runner poll-loop log
* (TaskLogEntry[]) newest-at-bottom and auto-scrolls to the tail on each update,
@ -25,10 +34,7 @@ export function RunnerLog({ entries }: { entries: TaskLogEntry[] }): JSX.Element
<div className="muted">Runner idle no recent activity.</div>
) : (
entries.map((e) => (
<div key={e.id} className="logrow">
<span className="logrow__time">{clock(e.createdAt)}</span>
<span style={{ color: '#6ee7b7', overflow: 'hidden', textOverflow: 'ellipsis' }}>{e.message}</span>
</div>
<LogRow key={e.id} e={e} />
))
)}
</div>