Skip to content

Commit da72e34

Browse files
gibsondanDagster Devtools
authored andcommitted
[ui] Fix MiddleTruncate canvas text-measurement perf regression (#25566)
## Summary Minimal fix for the perf regression introduced by #25178. **Scoped to the regression only** — a follow-up PR handles the pre-existing forced-reflow cost. #25178 began copying `letterSpacing`, `textRendering`, `fontKerning`, `wordSpacing`, and `fontStretch` onto the canvas 2D context used to measure text for middle truncation. Assigning text-shaping properties like `letterSpacing`/`textRendering` forces the browser **off its fast text-measurement path**, making every `measureText` call dramatically slower. That cost is multiplied by the per-instance binary search in `calculateMiddleTruncation` and by the number of rendered rows. A profiled asset-catalog load (group-by-Group, expanding groups) showed a single ~25s main-thread task with **~14s** of self-time in `measureText`. ### Origin The header tracking that exposed this came from #25091 (`Heading` component → `letter-spacing: -0.02em`). #25178 then made the measuring canvas honor that tracking — correct, but on the slow path. ### Fix Assign only `font` (which carries font-stretch via the shorthand) to the context, and account for `letterSpacing` **arithmetically** in the measure callback. This preserves #25178's correctness fix (headers no longer mis-wrap) while restoring the fast `measureText` path. The canvas create/append/remove lifecycle is **intentionally unchanged** here. The remaining ~6s of forced-reflow cost (`document.body` append/remove per measurement) predates #25178 and is addressed in a stacked follow-up. ## Test Plan - `yarn tsgo`, `yarn lint`, and the `calculateMiddleTruncation` tests pass. - Re-profiling the affected catalog load shows the `measureText` self-time collapse. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Internal-RevId: 23bc1cf34fc7e4419f99c63a4ad58100c294a1da
1 parent 988f668 commit da72e34

1 file changed

Lines changed: 15 additions & 3 deletions

File tree

js_modules/ui-components/src/components/MiddleTruncate.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ type MeasurementConfig = {
8888
textString: string;
8989
};
9090

91+
const parsePixels = (value: string) => {
92+
const parsed = parseFloat(value);
93+
return Number.isFinite(parsed) ? parsed : 0;
94+
};
95+
9196
/**
9297
* Given a font style and a container width, use a canvas to determine the longest possible
9398
* middle-truncated string that will fit within the container.
@@ -108,16 +113,23 @@ const calculateMiddleTruncatedText = ({styles, width, textString}: MeasurementCo
108113

109114
const targetWidth = width;
110115

111-
const {font, letterSpacing, wordSpacing, fontKerning, fontStretch, textRendering} = styles;
112-
Object.assign(ctx, {font, letterSpacing, wordSpacing, fontKerning, fontStretch, textRendering});
116+
// Only assign `font` (which carries font-stretch via the shorthand) to the
117+
// context. Assigning text-shaping properties such as `letterSpacing` or
118+
// `textRendering` forces the browser off its fast text-measurement path,
119+
// making each `measureText` call dramatically slower — a cost multiplied by
120+
// the binary search below and by every rendered row. `letterSpacing` still
121+
// affects layout width, so we account for it arithmetically instead.
122+
ctx.font = styles.font;
123+
const letterSpacing = parsePixels(styles.letterSpacing);
113124
body.appendChild(canvas);
114125

115126
// Search for the largest possible middle-truncated string that will fit within
116127
// the allotted width.
117128
const truncated = calculateMiddleTruncation(
118129
textString,
119130
targetWidth,
120-
(value: string) => ctx.measureText(value).width,
131+
(value: string) =>
132+
ctx.measureText(value).width + (letterSpacing ? letterSpacing * value.length : 0),
121133
);
122134

123135
body.removeChild(canvas);

0 commit comments

Comments
 (0)