From ab6e382aa454143e900ec9338401b33e076b1a2d Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 15 Apr 2026 09:02:53 -0500 Subject: [PATCH] Speed up issues page first paint (cherry picked from commit 9b12f59b1f605c80fda00fcfbd839fcf705e9954) --- ui/src/components/IssuesList.test.tsx | 30 ++++++++++++++++ ui/src/components/IssuesList.tsx | 51 ++++++++++++++++++++++++--- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/ui/src/components/IssuesList.test.tsx b/ui/src/components/IssuesList.test.tsx index c3073201668..a2b950522eb 100644 --- a/ui/src/components/IssuesList.test.tsx +++ b/ui/src/components/IssuesList.test.tsx @@ -391,6 +391,36 @@ describe("IssuesList", () => { }); }); + it("caps the first paint for large issue lists", async () => { + const manyIssues = Array.from({ length: 220 }, (_, index) => + createIssue({ + id: `issue-${index + 1}`, + identifier: `PAP-${index + 1}`, + title: `Issue ${index + 1}`, + }), + ); + + const { root } = renderWithQueryClient( + undefined} + />, + container, + ); + + await waitForAssertion(() => { + expect(container.querySelectorAll('[data-testid="issue-row"]')).toHaveLength(150); + expect(container.textContent).toContain("Rendering 150 of 220 issues..."); + }); + + act(() => { + root.unmount(); + }); + }); + it("uses context-scoped persisted column visibility", async () => { localStorage.setItem("paperclip:test-issues:company-1:issue-columns", JSON.stringify(["id", "assignee"])); diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 879dcc8a00c..ba8e7704edc 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -56,6 +56,9 @@ import { buildSubIssueDefaultsForViewer } from "../lib/subIssueDefaults"; import type { Issue, Project } from "@paperclipai/shared"; const ISSUE_SEARCH_DEBOUNCE_MS = 250; const ISSUE_SEARCH_RESULT_LIMIT = 200; +const INITIAL_ISSUE_ROW_RENDER_LIMIT = 150; +const ISSUE_ROW_RENDER_BATCH_SIZE = 150; +const ISSUE_ROW_RENDER_BATCH_DELAY_MS = 0; /* ── View state ── */ @@ -284,6 +287,7 @@ export function IssuesList({ const [assigneePickerIssueId, setAssigneePickerIssueId] = useState(null); const [assigneeSearch, setAssigneeSearch] = useState(""); const [issueSearch, setIssueSearch] = useState(initialSearch ?? ""); + const [renderedIssueRowLimit, setRenderedIssueRowLimit] = useState(INITIAL_ISSUE_ROW_RENDER_LIMIT); const [visibleIssueColumns, setVisibleIssueColumns] = useState(() => loadIssueColumns(scopedKey)); const deferredIssueSearch = useDeferredValue(issueSearch); const normalizedIssueSearch = deferredIssueSearch.trim().toLowerCase(); @@ -532,6 +536,26 @@ export function IssuesList({ })); }, [filtered, viewState.groupBy, agents, agentName, currentUserId, workspaceNameMap, issueTitleMap]); + useEffect(() => { + if (viewState.viewMode !== "list") return; + setRenderedIssueRowLimit(Math.min(filtered.length, INITIAL_ISSUE_ROW_RENDER_LIMIT)); + }, [filtered, viewState.viewMode]); + + useEffect(() => { + if (viewState.viewMode !== "list") return; + if (renderedIssueRowLimit >= filtered.length) return; + + const timeoutId = window.setTimeout(() => { + startTransition(() => { + setRenderedIssueRowLimit((current) => Math.min(filtered.length, current + ISSUE_ROW_RENDER_BATCH_SIZE)); + }); + }, ISSUE_ROW_RENDER_BATCH_DELAY_MS); + + return () => window.clearTimeout(timeoutId); + }, [filtered.length, renderedIssueRowLimit, viewState.viewMode]); + + const remainingIssueRowCount = Math.max(filtered.length - renderedIssueRowLimit, 0); + const newIssueDefaults = useCallback((groupKey?: string) => { const defaults: Record = { ...(baseCreateIssueDefaults ?? {}) }; if (projectId && defaults.projectId === undefined) defaults.projectId = projectId; @@ -581,6 +605,7 @@ export function IssuesList({ setAssigneeSearch(""); }, [onUpdateIssue]); + let remainingRowsToRender = viewState.viewMode === "list" ? renderedIssueRowLimit : Number.POSITIVE_INFINITY; return (
@@ -740,6 +765,11 @@ export function IssuesList({ Showing up to {ISSUE_SEARCH_RESULT_LIMIT} matches. Refine the search to narrow further.

)} + {viewState.viewMode === "list" && remainingIssueRowCount > 0 && ( +

+ Rendering {Math.min(renderedIssueRowLimit, filtered.length)} of {filtered.length} issues... +

+ )} {!isLoading && filtered.length === 0 && viewState.viewMode === "list" && ( ) : ( - groupedContent.map((group) => ( + groupedContent.map((group) => { + if (remainingRowsToRender <= 0) return null; + return ( () }; const renderIssueRow = (issue: Issue, depth: number) => { + if (remainingRowsToRender <= 0) return null; + remainingRowsToRender -= 1; + const children = childMap.get(issue.id) ?? []; const hasChildren = children.length > 0; const totalDescendants = hasChildren ? countDescendants(issue.id, childMap) : 0; @@ -818,7 +853,14 @@ export function IssuesList({ }; return ( -
0 ? { paddingLeft: `${depth * 16}px` } : undefined}> +
0 ? { paddingLeft: `${depth * 16}px` } : {}), + contentVisibility: "auto", + containIntrinsicSize: "44px", + }} + > renderIssueRow(issue, 0)); + return roots.map((issue) => renderIssueRow(issue, 0)).filter((node) => node !== null); })()} - )) + ); + }) )}
);