From b46f634b251973b05f04a18125beda2241213a5c Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Sat, 14 Nov 2026 20:01:53 -0500 Subject: [PATCH 1/3] perf(web): batch middle truncate fits MiddleTruncate fitted each head in its own layout effect and resize callback: read scrollWidth, toggle data-overflow, binary-search with range rects, write clip-path. Each read followed the previous head's write, so a list of overflowing labels forced a style and layout pass per row. Heads queued in one commit or resize delivery now fit together in a microtask before paint: all reads, then all writes. --- .../ui/middle-truncate.browser.test.tsx | 18 +++++ .../web/src/components/ui/middle-truncate.tsx | 73 ++++++++++++------- 2 files changed, 64 insertions(+), 27 deletions(-) diff --git a/apps/web/src/components/ui/middle-truncate.browser.test.tsx b/apps/web/src/components/ui/middle-truncate.browser.test.tsx index 3fbf36523254..f547c76ea93c 100644 --- a/apps/web/src/components/ui/middle-truncate.browser.test.tsx +++ b/apps/web/src/components/ui/middle-truncate.browser.test.tsx @@ -24,6 +24,24 @@ describe("MiddleTruncate", () => { expect(label()).toHaveStyle({ clipPath: "none" }); }); + it("fits every head mounted in the same commit", async () => { + const values = ["t3/investigate-release-build-times", "t3/remove-stale-worktree-branches"]; + await using screen = await render( + <> + {values.map((value) => ( +
{narrow(value)}
+ ))} + , + ); + const heads = () => [ + ...screen.container.querySelectorAll("[title] > span:first-child"), + ]; + + await expect + .poll(() => heads().filter((head) => head.hasAttribute("data-overflow")).length) + .toBe(2); + }); + it.each([ ["Fix the sidebar", 7], ["report\u2003\u2003draft.txt", 9], diff --git a/apps/web/src/components/ui/middle-truncate.tsx b/apps/web/src/components/ui/middle-truncate.tsx index faf8ab6b3f39..767963229310 100644 --- a/apps/web/src/components/ui/middle-truncate.tsx +++ b/apps/web/src/components/ui/middle-truncate.tsx @@ -25,8 +25,8 @@ export function MiddleTruncate({ const element = headRef.current; // A new head can keep the same box size, so refit here, not only on resize. if (!element || head === undefined) return; - fitHead(element); - return observeResize(element, () => fitHead(element)); + scheduleFit(element); + return observeResize(element, () => scheduleFit(element)); }, [head]); return ( head.clientWidth && text instanceof Text; - head.toggleAttribute("data-overflow", overflowing); - let slack = 0; - if (overflowing) { - const range = document.createRange(); - const left = head.getBoundingClientRect().left; - const widthTo = (end: number) => { - range.setStart(text, 0); - range.setEnd(text, end); - return range.getBoundingClientRect().right - left; - }; - let fits = 0; - let over = text.length; - while (over - fits > 1) { - const mid = (fits + over) >> 1; - if (widthTo(mid) <= head.clientWidth) fits = mid; - else over = mid; - } - if (/[\uD800-\uDBFF]/.test(text.data[fits - 1] ?? "")) fits -= 1; - slack = Math.max(0, head.clientWidth - widthTo(fits)); +// Fitting heads one by one forces a layout per row, since each read follows the previous +// head's write. Heads queued in one commit or resize delivery fit together in a microtask, +// still before paint: all reads, then all writes. +const pendingFits = new Set(); + +function scheduleFit(head: HTMLElement) { + if (pendingFits.size === 0) queueMicrotask(fitPendingHeads); + pendingFits.add(head); +} + +function fitPendingHeads() { + const heads = [...pendingFits].filter((head) => head.isConnected); + pendingFits.clear(); + const overflowing = heads.map( + (head) => head.scrollWidth > head.clientWidth && head.firstChild instanceof Text, + ); + heads.forEach((head, index) => head.toggleAttribute("data-overflow", overflowing[index])); + const slacks = heads.map((head, index) => (overflowing[index] ? measureSlack(head) : 0)); + heads.forEach((head, index) => { + const slack = slacks[index]!; + head.style.clipPath = slack > 0 ? `inset(-0.25em ${slack}px -0.25em 0)` : ""; + const tail = head.nextElementSibling; + if (tail instanceof HTMLElement) tail.style.translate = slack > 0 ? `-${slack}px 0` : ""; + }); +} + +function measureSlack(head: HTMLElement) { + const text = head.firstChild as Text; + const range = document.createRange(); + const left = head.getBoundingClientRect().left; + const width = head.clientWidth; + const widthTo = (end: number) => { + range.setStart(text, 0); + range.setEnd(text, end); + return range.getBoundingClientRect().right - left; + }; + let fits = 0; + let over = text.length; + while (over - fits > 1) { + const mid = (fits + over) >> 1; + if (widthTo(mid) <= width) fits = mid; + else over = mid; } - head.style.clipPath = slack > 0 ? `inset(-0.25em ${slack}px -0.25em 0)` : ""; - const tail = head.nextElementSibling; - if (tail instanceof HTMLElement) tail.style.translate = slack > 0 ? `-${slack}px 0` : ""; + if (/[\uD800-\uDBFF]/.test(text.data[fits - 1] ?? "")) fits -= 1; + return Math.max(0, width - widthTo(fits)); } // The segments render pre so the space at the cut survives, so collapse here what CSS From b34c763c44245757798c1b25d6601866dc61eaf7 Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Sat, 14 Nov 2026 20:10:47 -0500 Subject: [PATCH 2/3] test(web): wait for middle truncate fits with vi.waitFor --- .../src/components/ui/middle-truncate.browser.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/ui/middle-truncate.browser.test.tsx b/apps/web/src/components/ui/middle-truncate.browser.test.tsx index f547c76ea93c..91de6b169f64 100644 --- a/apps/web/src/components/ui/middle-truncate.browser.test.tsx +++ b/apps/web/src/components/ui/middle-truncate.browser.test.tsx @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vite-plus/test"; +import { describe, expect, it, vi } from "vite-plus/test"; import { render } from "../../../test/render"; import { MiddleTruncate } from "./middle-truncate"; @@ -37,9 +37,9 @@ describe("MiddleTruncate", () => { ...screen.container.querySelectorAll("[title] > span:first-child"), ]; - await expect - .poll(() => heads().filter((head) => head.hasAttribute("data-overflow")).length) - .toBe(2); + await vi.waitFor(() => + expect(heads().filter((head) => head.hasAttribute("data-overflow"))).toHaveLength(2), + ); }); it.each([ From 5be92c9ec07ed83a9a3e6d7865dbeb46d696c7ab Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Nov 2026 01:43:44 +0000 Subject: [PATCH 3/3] refactor(web): keep the clip comment beside the code that clips Co-Authored-By: Claude Opus 5.5 --- apps/web/src/components/ui/middle-truncate.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/ui/middle-truncate.tsx b/apps/web/src/components/ui/middle-truncate.tsx index 767963229310..a5213209f0ed 100644 --- a/apps/web/src/components/ui/middle-truncate.tsx +++ b/apps/web/src/components/ui/middle-truncate.tsx @@ -60,10 +60,6 @@ export function MiddleTruncate({ ); } -// An overflowing head is cut after its last whole character: clip the partial glyph -// and slide the tail left over the slack. Neither changes layout, so the observed -// size stays the true available width. - // Fitting heads one by one forces a layout per row, since each read follows the previous // head's write. Heads queued in one commit or resize delivery fit together in a microtask, // still before paint: all reads, then all writes. @@ -74,6 +70,9 @@ function scheduleFit(head: HTMLElement) { pendingFits.add(head); } +// An overflowing head is cut after its last whole character: clip the partial glyph +// and slide the tail left over the slack. Neither changes layout, so the observed +// size stays the true available width. function fitPendingHeads() { const heads = [...pendingFits].filter((head) => head.isConnected); pendingFits.clear();