Skip to content

Commit e66ffcc

Browse files
authored
perf(web): stop measuring table cells while replies stream (#26152)
1 parent 0637f87 commit e66ffcc

1 file changed

Lines changed: 18 additions & 68 deletions

File tree

‎apps/web/src/components/chat/MarkdownTable.tsx‎

Lines changed: 18 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useRef, useState, type ComponentProps } from "react";
33

44
import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
55
import { getClientSettings } from "../../hooks/useSettings";
6-
import { observeResize } from "../../lib/observeResize";
76
import {
87
serializeTableElementToCsv,
98
serializeTableElementToMarkdown,
@@ -15,35 +14,9 @@ import { ScrollArea } from "../ui/scroll-area";
1514
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
1615
import { reportMarkdownActionFailure } from "./markdownActionFailure";
1716

18-
/**
19-
* Whether any cell is clipped in the unwrapped layout. Only then does the wrap
20-
* toggle change anything, so short tables hide it. Measures with the container
21-
* briefly unwrapped and restores it before the browser paints.
22-
*/
23-
function tableHasClippedCells(container: HTMLElement, table: HTMLTableElement): boolean {
24-
const viewport = table.closest<HTMLElement>('[data-slot="scroll-area-viewport"]');
25-
const scrollLeft = viewport?.scrollLeft ?? 0;
26-
const previous = container.dataset.expanded;
27-
container.dataset.expanded = "false";
28-
try {
29-
for (const row of table.rows) {
30-
for (const cell of row.cells) {
31-
if (cell.scrollWidth > cell.clientWidth) return true;
32-
}
33-
}
34-
return false;
35-
} finally {
36-
if (previous === undefined) delete container.dataset.expanded;
37-
else container.dataset.expanded = previous;
38-
// The unwrapped layout can be narrower and clamp a sideways scroll.
39-
if (viewport && scrollLeft > 0) viewport.scrollLeft = scrollLeft;
40-
}
41-
}
42-
4317
export function MarkdownTable({ children, ...props }: ComponentProps<"table">) {
4418
const containerRef = useRef<HTMLDivElement | null>(null);
4519
const [expanded, setExpanded] = useState(() => getClientSettings().wordWrap);
46-
const [hasClippedCells, setHasClippedCells] = useState(false);
4720
const { copyToClipboard, isCopied: copied } = useCopyToClipboard<"markdown" | "csv">({
4821
timeout: 1200,
4922
onError: (cause, format) =>
@@ -52,25 +25,6 @@ export function MarkdownTable({ children, ...props }: ComponentProps<"table">) {
5225
const expandLabel = expanded ? "Collapse table cells" : "Expand table cells";
5326
const copyLabel = copied ? "Copied" : "Copy table";
5427

55-
// React calls this with null when a previous attach returned no cleanup.
56-
const attachContainer = (container: HTMLDivElement | null) => {
57-
containerRef.current = container;
58-
const table = container?.querySelector("table");
59-
if (!container || !table) return;
60-
const measure = () => setHasClippedCells(tableHasClippedCells(container, table));
61-
measure();
62-
// A streamed cell can grow until it clips while the table keeps its size, and a
63-
// narrower column can clip a cell without this component rendering.
64-
const mutations = new MutationObserver(measure);
65-
mutations.observe(table, { childList: true, characterData: true, subtree: true });
66-
const stopResize = observeResize(table, measure);
67-
return () => {
68-
mutations.disconnect();
69-
stopResize();
70-
containerRef.current = null;
71-
};
72-
};
73-
7428
const handleCopy = (format: "markdown" | "csv") => {
7529
const table = containerRef.current?.querySelector("table");
7630
if (!table) return;
@@ -84,7 +38,7 @@ export function MarkdownTable({ children, ...props }: ComponentProps<"table">) {
8438

8539
return (
8640
<div
87-
ref={attachContainer}
41+
ref={containerRef}
8842
className="chat-markdown-table-container"
8943
data-expanded={expanded ? "true" : "false"}
9044
>
@@ -93,27 +47,23 @@ export function MarkdownTable({ children, ...props }: ComponentProps<"table">) {
9347
</ScrollArea>
9448
{/* Stays within the row's px-1: the chat lane clips sideways overflow. */}
9549
<div className="mt-0.5 -mx-1 flex items-center justify-between select-none">
96-
{hasClippedCells ? (
97-
<Tooltip>
98-
<TooltipTrigger
99-
render={
100-
<Button
101-
type="button"
102-
variant={expanded ? "secondary" : "ghost-muted"}
103-
size="icon-xs"
104-
aria-pressed={expanded}
105-
onClick={() => setExpanded((value) => !value)}
106-
aria-label={expandLabel}
107-
/>
108-
}
109-
>
110-
<MorphIcon className="size-3" icon={expanded ? Minimize2 : Maximize2} />
111-
</TooltipTrigger>
112-
<TooltipPopup side="top">{expandLabel}</TooltipPopup>
113-
</Tooltip>
114-
) : (
115-
<span />
116-
)}
50+
<Tooltip>
51+
<TooltipTrigger
52+
render={
53+
<Button
54+
type="button"
55+
variant={expanded ? "secondary" : "ghost-muted"}
56+
size="icon-xs"
57+
aria-pressed={expanded}
58+
onClick={() => setExpanded((value) => !value)}
59+
aria-label={expandLabel}
60+
/>
61+
}
62+
>
63+
<MorphIcon className="size-3" icon={expanded ? Minimize2 : Maximize2} />
64+
</TooltipTrigger>
65+
<TooltipPopup side="top">{expandLabel}</TooltipPopup>
66+
</Tooltip>
11767
<Menu>
11868
<Tooltip>
11969
<TooltipTrigger

0 commit comments

Comments
 (0)