Skip to content

Commit 4e761c6

Browse files
authored
feat: 10% perf improvements in the core lib (#729)
3.27ms → 2.95ms <img width="639" height="1040" alt="image" src="https://github.com/user-attachments/assets/4f3adc53-d1f6-4596-ba68-ed5eb84efa5c" />
1 parent c78869b commit 4e761c6

File tree

4 files changed

+77
-41
lines changed

4 files changed

+77
-41
lines changed

src/handler/compute.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ export default async function compute(
2929
const Yoga = await getYoga()
3030

3131
// Extend the default style with defined and inherited styles.
32-
const style: SerializedStyle = {
33-
...inheritedStyle,
34-
...expand(presets[type], inheritedStyle),
35-
...expand(definedStyle, inheritedStyle),
36-
}
32+
const style: SerializedStyle = Object.assign(
33+
{},
34+
inheritedStyle,
35+
expand(presets[type], inheritedStyle),
36+
expand(definedStyle, inheritedStyle)
37+
)
3738

3839
if (type === 'img') {
3940
let [resolvedSrc, imageWidth, imageHeight] = await resolveImageData(

src/handler/image.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,27 @@ function parsePNG(buf: ArrayBuffer) {
6161
import { createLRU, parseViewBox } from '../utils.js'
6262

6363
type ResolvedImageData = [string, number?, number?] | readonly []
64-
export const cache = createLRU<ResolvedImageData>(100)
64+
export const cache = createLRU<ResolvedImageData>(500)
6565
export const inflightRequests = new Map<string, Promise<ResolvedImageData>>()
6666

6767
const ALLOWED_IMAGE_TYPES = [PNG, APNG, JPEG, GIF, SVG]
6868

69+
// Pre-compiled regex patterns for SVG parsing
70+
const SVG_ATTRS_REGEX = /<svg[^>]*>/i
71+
const VIEWBOX_REGEX = /viewBox=['"]([^'"]+)['"]/
72+
const WIDTH_REGEX = /width=['"](\d*\.?\d+)['"]/
73+
const HEIGHT_REGEX = /height=['"](\d*\.?\d+)['"]/
74+
6975
function arrayBufferToBase64(buffer) {
70-
let binary = ''
7176
const bytes = new Uint8Array(buffer)
72-
for (let i = 0; i < bytes.byteLength; i++) {
73-
binary += String.fromCharCode(bytes[i])
77+
const CHUNK_SIZE = 0x8000 // 32KB chunks
78+
let binary = ''
79+
80+
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
81+
const chunk = bytes.subarray(i, Math.min(i + CHUNK_SIZE, bytes.length))
82+
binary += String.fromCharCode(...chunk)
7483
}
84+
7585
return btoa(binary)
7686
}
7787

@@ -87,28 +97,32 @@ function base64ToArrayBuffer(base64: string): ArrayBuffer {
8797

8898
function parseSvgImageSize(src: string, data: string) {
8999
// Parse the SVG image size
90-
const svgTag = data.match(/<svg[^>]*>/)[0]
100+
const svgMatch = data.match(SVG_ATTRS_REGEX)
101+
if (!svgMatch) throw new Error(`Failed to parse SVG from ${src}`)
91102

92-
const viewBoxStr = svgTag.match(/viewBox=['"](.+)['"]/)
93-
let viewBox = viewBoxStr ? parseViewBox(viewBoxStr[1]) : null
103+
const svgTag = svgMatch[0]
104+
const viewBoxMatch = VIEWBOX_REGEX.exec(svgTag)
105+
const widthMatch = WIDTH_REGEX.exec(svgTag)
106+
const heightMatch = HEIGHT_REGEX.exec(svgTag)
94107

95-
const width = svgTag.match(/width=['"](\d*\.\d+|\d+)['"]/)
96-
const height = svgTag.match(/height=['"](\d*\.\d+|\d+)['"]/)
108+
let viewBox = viewBoxMatch ? parseViewBox(viewBoxMatch[1]) : null
97109

98-
if (!viewBox && (!width || !height)) {
110+
if (!viewBox && (!widthMatch || !heightMatch)) {
99111
throw new Error(`Failed to parse SVG from ${src}: missing "viewBox"`)
100112
}
101113

102-
const size = viewBox ? [viewBox[2], viewBox[3]] : [+width[1], +height[1]]
114+
const size = viewBox
115+
? [viewBox[2], viewBox[3]]
116+
: [+widthMatch[1], +heightMatch[1]]
103117

104118
const ratio = size[0] / size[1]
105119
const imageSize: [number, number] =
106-
width && height
107-
? [+width[1], +height[1]]
108-
: width
109-
? [+width[1], +width[1] / ratio]
110-
: height
111-
? [+height[1] * ratio, +height[1]]
120+
widthMatch && heightMatch
121+
? [+widthMatch[1], +heightMatch[1]]
122+
: widthMatch
123+
? [+widthMatch[1], +widthMatch[1] / ratio]
124+
: heightMatch
125+
? [+heightMatch[1] * ratio, +heightMatch[1]]
112126
: [size[0], size[1]]
113127

114128
return imageSize

src/text/measurer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ export function genMeasurer(
1818
const cache = new Map<string, number>()
1919

2020
function measureGrapheme(grapheme: string): number {
21-
if (cache.has(grapheme)) {
22-
return cache.get(grapheme)
23-
}
21+
let width = cache.get(grapheme)
2422

25-
const width = engine.measure(grapheme, { fontSize, letterSpacing })
26-
cache.set(grapheme, width)
23+
if (width === undefined) {
24+
width = engine.measure(grapheme, { fontSize, letterSpacing })
25+
cache.set(grapheme, width)
26+
}
2727

2828
return width
2929
}

src/utils.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,19 @@ export const wordSeparators = [
172172
0x0020, 0x00a0, 0x1361, 0x10100, 0x10101, 0x1039, 0x1091, 0xa,
173173
].map((point) => String.fromCodePoint(point))
174174

175+
const segmentCache = new Map<string, string[]>()
176+
const MAX_SEGMENT_CACHE_SIZE = 500
177+
175178
export function segment(
176179
content: string,
177180
granularity: 'word' | 'grapheme',
178181
locale?: string
179182
): string[] {
183+
const cacheKey = `${granularity}:${locale || ''}:${content}`
184+
185+
if (segmentCache.has(cacheKey)) {
186+
return segmentCache.get(cacheKey)!
187+
}
180188
if (!wordSegmenter || !graphemeSegmenter) {
181189
if (!(typeof Intl !== 'undefined' && 'Segmenter' in Intl)) {
182190
// https://caniuse.com/mdn-javascript_builtins_intl_segments
@@ -191,8 +199,10 @@ export function segment(
191199
})
192200
}
193201

202+
let result: string[]
203+
194204
if (granularity === 'grapheme') {
195-
return [...graphemeSegmenter.segment(content)].map((seg) => seg.segment)
205+
result = [...graphemeSegmenter.segment(content)].map((seg) => seg.segment)
196206
} else {
197207
const segmented = [...wordSegmenter.segment(content)].map(
198208
(seg) => seg.segment
@@ -218,8 +228,16 @@ export function segment(
218228
}
219229
}
220230

221-
return output
231+
result = output
222232
}
233+
234+
if (segmentCache.size >= MAX_SEGMENT_CACHE_SIZE) {
235+
const firstKey = segmentCache.keys().next().value
236+
segmentCache.delete(firstKey)
237+
}
238+
239+
segmentCache.set(cacheKey, result)
240+
return result
223241
}
224242

225243
export function buildXMLString(
@@ -243,21 +261,24 @@ export function buildXMLString(
243261

244262
export function createLRU<T>(max = 20) {
245263
const store: Map<string, T> = new Map()
246-
function set(key: string, value: T) {
247-
if (store.size >= max) {
248-
const keyToDelete = store.keys().next().value
249-
store.delete(keyToDelete)
250-
}
251-
store.set(key, value)
252-
}
253264
function get(key: string): T | undefined {
254-
const hasKey = store.has(key)
255-
if (!hasKey) return undefined
265+
const value = store.get(key)
266+
if (value === undefined) return undefined
256267

257-
const entry = store.get(key)!
268+
// Move to end (most recently used)
258269
store.delete(key)
259-
store.set(key, entry)
260-
return entry
270+
store.set(key, value)
271+
return value
272+
}
273+
function set(key: string, value: T) {
274+
if (store.has(key)) {
275+
store.delete(key)
276+
} else if (store.size >= max) {
277+
const firstKey = store.keys().next().value
278+
store.delete(firstKey)
279+
}
280+
281+
store.set(key, value)
261282
}
262283
function clear() {
263284
store.clear()

0 commit comments

Comments
 (0)