perf(site): use lazy iteration in sliceAtGraphemeBoundary (#23671)

Array.from(graphemeSegmenter.segment(text)) materializes the
entire text into an array before iterating, even though the loop
breaks early at the visible prefix length. During streaming at
60fps, this makes each frame O(full text) instead of O(prefix).

Benchmark on 5000-char text with 200-char prefix: 22.6x faster
(1.44ms to 0.06ms per call, saving 8.3% of the frame budget).
The fallback codepoint path had the same issue with Array.from.
This commit is contained in:
Mathias Fredriksson
2026-03-26 16:33:48 +02:00
committed by GitHub
parent 87aafd4ae2
commit b23c07cf23
@@ -327,9 +327,12 @@ function sliceAtGraphemeBoundary(
if (graphemeSegmenter) {
let safeEnd = 0;
const segments = Array.from(graphemeSegmenter.segment(text));
for (const segment of segments) {
// Iterate the segmenter lazily instead of materializing
// with Array.from(). The early break makes this O(prefix)
// instead of O(full text), which matters at 60fps during
// streaming where the visible prefix is much shorter than
// the full accumulated text.
for (const segment of graphemeSegmenter.segment(text)) {
const segmentEnd = segment.index + segment.segment.length;
if (segmentEnd > maxCodeUnitLength) {
break;
@@ -342,9 +345,10 @@ function sliceAtGraphemeBoundary(
// Fallback: iterate by codepoint to avoid splitting surrogate
// pairs. This is less precise than grapheme segmentation but
// still safe for rendering.
// still safe for rendering. Iterating the string directly with
// for...of is lazy and avoids the O(n) Array.from() cost.
let safeEnd = 0;
for (const codePoint of Array.from(text)) {
for (const codePoint of text) {
const codePointEnd = safeEnd + codePoint.length;
if (codePointEnd > maxCodeUnitLength) {
break;