Skip to content

Commit 5db92a4

Browse files
Speed up segmentation (#13415)
* Speed up segmentation * Add segment benchmark * Add and move comments * Update * Tweak comments * Tweak variable name * Tweak --------- Co-authored-by: Jordan Pittman <[email protected]>
1 parent be94e21 commit 5db92a4

File tree

2 files changed

+58
-22
lines changed

2 files changed

+58
-22
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { bench } from 'vitest'
2+
import { segment } from './segment'
3+
4+
const values = [
5+
['hover:focus:underline', ':'],
6+
['var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0)', ','],
7+
['var(--some-value,env(safe-area-inset-top,var(--some-other-value,env(safe-area-inset))))', ','],
8+
]
9+
10+
bench('segment', () => {
11+
for (let [value, sep] of values) {
12+
segment(value, sep)
13+
}
14+
})

packages/tailwindcss/src/utils/segment.ts

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1+
// This is a shared buffer that is used to keep track of the current nesting level
2+
// of parens, brackets, and braces. It is used to determine if a character is at
3+
// the top-level of a string. This is a performance optimization to avoid memory
4+
// allocations on every call to `segment`.
5+
const closingBracketStack = new Uint8Array(256)
6+
7+
// All numbers are equivalent to the value returned by `String#charCodeAt(0)`
8+
const BACKSLASH = 0x5c
9+
const OPEN_PAREN = 0x28
10+
const OPEN_BRACKET = 0x5b
11+
const OPEN_CURLY = 0x7b
12+
const CLOSE_PAREN = 0x29
13+
const CLOSE_BRACKET = 0x5d
14+
const CLOSE_CURLY = 0x7d
15+
116
/**
217
* This splits a string on a top-level character.
318
*
4-
* Regex doesn't support recursion (at least not the JS-flavored version).
5-
* So we have to use a tiny state machine to keep track of paren placement.
19+
* Regex doesn't support recursion (at least not the JS-flavored version),
20+
* so we have to use a tiny state machine to keep track of paren placement.
621
*
722
* Expected behavior using commas:
823
* var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0)
@@ -11,43 +26,50 @@
1126
* ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens
1227
*/
1328
export function segment(input: string, separator: string) {
14-
// Stack of characters to close open brackets. Appending to a string because
15-
// it's faster than an array of strings.
16-
let closingBracketStack = ''
29+
// SAFETY: We can use an index into a shared buffer because this function is
30+
// synchronous, non-recursive, and runs in a single-threaded envionment.
31+
let stackPos = 0
1732
let parts: string[] = []
1833
let lastPos = 0
1934

35+
let separatorCode = separator.charCodeAt(0)
36+
2037
for (let idx = 0; idx < input.length; idx++) {
21-
let char = input[idx]
38+
let char = input.charCodeAt(idx)
2239

23-
if (closingBracketStack.length === 0 && char === separator) {
40+
if (stackPos === 0 && char === separatorCode) {
2441
parts.push(input.slice(lastPos, idx))
2542
lastPos = idx + 1
2643
continue
2744
}
2845

2946
switch (char) {
30-
case '\\':
47+
case BACKSLASH:
3148
// The next character is escaped, so we skip it.
3249
idx += 1
3350
break
34-
case '(':
35-
closingBracketStack += ')'
51+
case OPEN_PAREN:
52+
closingBracketStack[stackPos] = CLOSE_PAREN
53+
stackPos++
3654
break
37-
case '[':
38-
closingBracketStack += ']'
55+
case OPEN_BRACKET:
56+
closingBracketStack[stackPos] = CLOSE_BRACKET
57+
stackPos++
3958
break
40-
case '{':
41-
closingBracketStack += '}'
59+
case OPEN_CURLY:
60+
closingBracketStack[stackPos] = CLOSE_CURLY
61+
stackPos++
4262
break
43-
case ')':
44-
case ']':
45-
case '}':
46-
if (
47-
closingBracketStack.length > 0 &&
48-
char === closingBracketStack[closingBracketStack.length - 1]
49-
) {
50-
closingBracketStack = closingBracketStack.slice(0, closingBracketStack.length - 1)
63+
case CLOSE_BRACKET:
64+
case CLOSE_CURLY:
65+
case CLOSE_PAREN:
66+
if (stackPos > 0 && char === closingBracketStack[stackPos - 1]) {
67+
// SAFETY: The buffer does not need to be mutated because the stack is
68+
// only ever read from or written to its current position. Its current
69+
// position is only ever incremented after writing to it. Meaning that
70+
// the buffer can be dirty for the next use and still be correct since
71+
// reading/writing always starts at position `0`.
72+
stackPos--
5173
}
5274
break
5375
}

0 commit comments

Comments
 (0)