Skip to content

Commit e5345fb

Browse files
committed
perf: optimize measurement methods to reduce reflows
1 parent 994205b commit e5345fb

1 file changed

Lines changed: 57 additions & 26 deletions

File tree

src/exporter/svg.ts

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -67,31 +67,38 @@ function parseCoordinate(value: string | null): number {
6767
return Number.isNaN(parsed) ? 0 : parsed;
6868
}
6969

70-
function measureSpanContentHeight(span: HTMLElement): number {
70+
interface MeasuredSpanContentDimensions {
71+
height: number;
72+
width: number;
73+
}
74+
75+
function measureSpanContentDimensions(
76+
span: HTMLElement,
77+
measureWidth: boolean,
78+
): MeasuredSpanContentDimensions {
7179
const prevHeight = span.style.height;
80+
const prevWidth = span.style.width;
7281
const prevOverflow = span.style.overflow;
7382
try {
7483
span.style.height = 'max-content';
7584
span.style.overflow = 'hidden';
7685
void span.offsetHeight; // force reflow
7786
const scrollHeight = span.scrollHeight;
7887
const rectHeight = span.getBoundingClientRect().height;
79-
return Math.max(scrollHeight, rectHeight);
80-
} finally {
81-
span.style.height = prevHeight;
82-
span.style.overflow = prevOverflow;
83-
}
84-
}
88+
let width = span.scrollWidth;
8589

86-
function measureSpanContentWidth(span: HTMLElement): number {
87-
const prevWidth = span.style.width;
88-
const prevOverflow = span.style.overflow;
89-
try {
90-
span.style.width = 'max-content';
91-
span.style.overflow = 'hidden';
92-
void span.offsetWidth; // force reflow
93-
return span.scrollWidth;
90+
if (measureWidth) {
91+
span.style.width = 'max-content';
92+
void span.offsetWidth; // force reflow
93+
width = span.scrollWidth;
94+
}
95+
96+
return {
97+
height: Math.max(scrollHeight, rectHeight),
98+
width,
99+
};
94100
} finally {
101+
span.style.height = prevHeight;
95102
span.style.width = prevWidth;
96103
span.style.overflow = prevOverflow;
97104
}
@@ -137,8 +144,16 @@ function createCoordConverter(
137144
// and horizontally aligned content can overflow as well.
138145
function getFOContentBoundsInSVG(
139146
fo: SVGForeignObjectElement,
140-
content: HTMLElement,
141147
toSVGCoord: (x: number, y: number) => SVGPoint,
148+
{
149+
contentHeight,
150+
contentWidth,
151+
keepForeignObjectWidth,
152+
}: {
153+
contentHeight: number;
154+
contentWidth: number;
155+
keepForeignObjectWidth: boolean;
156+
},
142157
): [number, number, number, number] {
143158
const foRect = fo.getBoundingClientRect();
144159
const foTopLeft = toSVGCoord(foRect.left, foRect.top);
@@ -156,19 +171,17 @@ function getFOContentBoundsInSVG(
156171
foRect.height > 0 ? foHeightSVG / foRect.height : 1;
157172
const svgUnitsPerClientPxX = foRect.width > 0 ? foWidthSVG / foRect.width : 1;
158173

159-
// Measure actual content dimensions
160-
const realScrollHeight = measureSpanContentHeight(content);
161174
const contentHeightSVG =
162-
realScrollHeight > 0
163-
? realScrollHeight * svgUnitsPerClientPxY
175+
contentHeight > 0
176+
? contentHeight * svgUnitsPerClientPxY
164177
: foHeightSVG;
165178

166-
const computedStyle = window.getComputedStyle(content);
179+
const computedStyle = window.getComputedStyle(fo.firstElementChild as Element);
167180
const alignItems = computedStyle.alignItems;
168181
const justifyContent = computedStyle.justifyContent;
169-
const contentWidthSVG = shouldKeepForeignObjectWidth(computedStyle)
182+
const contentWidthSVG = keepForeignObjectWidth
170183
? foWidthSVG
171-
: Math.max(foWidthSVG, measureSpanContentWidth(content) * svgUnitsPerClientPxX);
184+
: Math.max(foWidthSVG, contentWidth * svgUnitsPerClientPxX);
172185

173186
// Calculate vertical bounds
174187
let top: number, bottom: number;
@@ -214,18 +227,32 @@ function collectForeignObjectExportAdjustments(svg: SVGSVGElement) {
214227
).map((fo) => {
215228
const content = fo.firstElementChild as HTMLElement | null;
216229
if (!content) return null;
230+
const computedStyle = window.getComputedStyle(content);
231+
const keepForeignObjectWidth = shouldKeepForeignObjectWidth(computedStyle);
232+
const measuredContent = measureSpanContentDimensions(
233+
content,
234+
!keepForeignObjectWidth,
235+
);
217236

218237
const parent =
219238
fo.parentElement instanceof SVGGraphicsElement ? fo.parentElement : svg;
220239
const toParentCoord = createCoordConverter(svg, parent);
221240
const toLocalCoord = createCoordConverter(svg, fo);
222241
if (!toParentCoord) return null;
223242

224-
const parentBounds = getFOContentBoundsInSVG(fo, content, toParentCoord);
243+
const parentBounds = getFOContentBoundsInSVG(fo, toParentCoord, {
244+
contentHeight: measuredContent.height,
245+
contentWidth: measuredContent.width,
246+
keepForeignObjectWidth,
247+
});
225248
const originalX = parseCoordinate(fo.getAttribute('x'));
226249
const originalY = parseCoordinate(fo.getAttribute('y'));
227250
const localBounds = toLocalCoord
228-
? getFOContentBoundsInSVG(fo, content, toLocalCoord)
251+
? getFOContentBoundsInSVG(fo, toLocalCoord, {
252+
contentHeight: measuredContent.height,
253+
contentWidth: measuredContent.width,
254+
keepForeignObjectWidth,
255+
})
229256
: null;
230257
const hasTransform = fo.hasAttribute('transform');
231258
if (hasTransform && !localBounds) return null;
@@ -245,7 +272,11 @@ function collectForeignObjectExportAdjustments(svg: SVGSVGElement) {
245272
};
246273

247274
return {
248-
rootBounds: getFOContentBoundsInSVG(fo, content, toSVGCoord),
275+
rootBounds: getFOContentBoundsInSVG(fo, toSVGCoord, {
276+
contentHeight: measuredContent.height,
277+
contentWidth: measuredContent.width,
278+
keepForeignObjectWidth,
279+
}),
249280
exportBounds,
250281
} satisfies ForeignObjectExportAdjustment;
251282
});

0 commit comments

Comments
 (0)