Skip to content

Commit 49e50d8

Browse files
committed
perf: 优化 CSS 选择器转换、JS 处理器及 WXML 模板处理热路径的缓存与计算
1 parent 75506ae commit 49e50d8

File tree

9 files changed

+196
-79
lines changed

9 files changed

+196
-79
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"weapp-tailwindcss": patch
3+
"@weapp-tailwindcss/postcss": patch
4+
"@weapp-tailwindcss/shared": patch
5+
---
6+
7+
性能优化:针对 CSS 选择器转换、JS 处理器、WXML 模板处理等热路径进行多项缓存与计算优化。
8+
9+
- JS 处理器:复用 `resolveClassNameTransformWithResult` 返回的 `escapedValue` 避免重复 escape 计算;引入 `getReplacement` 缓存消除重复 `replaceWxml` 调用;移除 `escapeStringRegexp` + `new RegExp` 正则编译开销
10+
- `createJsHandler`:预构建默认 `defaults` 对象,无覆盖选项时跳过 `defuOverrideArray` 合并
11+
- WXML 模板:`templateReplacer` 支持复用模块级 tokenizer 实例;`createTemplateHandler` 预构建 attribute matcher 并传递给 `customTemplateHandler`
12+
- PostCSS fallback 选择器解析:为 `transform` 函数添加 selector 级别缓存,避免重复解析相同选择器
13+
- `splitCode`:为默认和 allowDoubleQuotes 两种模式分别添加结果缓存,预编译分割正则

packages/postcss/src/selectorParser/fallback.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import psp from 'postcss-selector-parser'
77
import { isUniAppXEnabled, stripUnsupportedPseudoForUniAppX } from '../compat/uni-app-x'
88
import { normalizeTransformOptions } from './utils'
99

10+
interface CachedFallbackResult {
11+
action: 'keep' | 'update' | 'remove'
12+
selector?: string
13+
}
14+
1015
const fallbackRemoveCache = new WeakMap<object, {
1116
parser: ReturnType<typeof psp>
1217
transform: RuleTransformer
@@ -16,6 +21,7 @@ const FALLBACK_TRANSFORM_OPTIONS = normalizeTransformOptions()
1621

1722
/**
1823
* 获取用于小程序兼容性处理的解析器,内部会缓存实例并移除不支持的选择器。
24+
* 增加了选择器字符串级缓存,避免对相同选择器重复 parse。
1925
* @returns 带清理规则的解析器实例。
2026
*/
2127
export function getFallbackRemove(_rule?: Rule, options?: IStyleHandlerOptions) {
@@ -25,6 +31,16 @@ export function getFallbackRemove(_rule?: Rule, options?: IStyleHandlerOptions)
2531
if (!entry) {
2632
const uniAppX = isUniAppXEnabled(options)
2733
let currentRule: Rule | undefined
34+
// 选择器字符串级缓存,避免对相同选择器重复 parse + walk
35+
const selectorCache = new Map<string, CachedFallbackResult>()
36+
const selectorCacheLimit = 50000
37+
38+
function writeSelectorCache(selector: string, result: CachedFallbackResult) {
39+
if (selectorCache.size >= selectorCacheLimit) {
40+
selectorCache.clear()
41+
}
42+
selectorCache.set(selector, result)
43+
}
2844

2945
const parser = psp((selectors) => {
3046
const activeRule = currentRule
@@ -94,13 +110,42 @@ export function getFallbackRemove(_rule?: Rule, options?: IStyleHandlerOptions)
94110
const rawTransformSync = parser.transformSync.bind(parser)
95111

96112
const transform: RuleTransformer = (targetRule: Rule) => {
113+
const sourceSelector = targetRule.selector
114+
if (!sourceSelector) {
115+
return
116+
}
117+
118+
// 查询选择器字符串级缓存
119+
const cached = selectorCache.get(sourceSelector)
120+
if (cached) {
121+
if (cached.action === 'remove') {
122+
targetRule.remove()
123+
}
124+
else if (cached.action === 'update' && cached.selector && cached.selector !== sourceSelector) {
125+
targetRule.selector = cached.selector
126+
}
127+
return
128+
}
129+
97130
currentRule = targetRule
98131
try {
99132
rawTransformSync(targetRule, FALLBACK_TRANSFORM_OPTIONS)
100133
}
101134
finally {
102135
currentRule = undefined
103136
}
137+
138+
// 写入缓存
139+
const wasRemoved = targetRule.parent == null
140+
if (wasRemoved) {
141+
writeSelectorCache(sourceSelector, { action: 'remove' })
142+
}
143+
else if (targetRule.selector === sourceSelector) {
144+
writeSelectorCache(sourceSelector, { action: 'keep' })
145+
}
146+
else {
147+
writeSelectorCache(sourceSelector, { action: 'update', selector: targetRule.selector })
148+
}
104149
}
105150

106151
parser.transformSync = ((input: unknown, opts?: ParserTransformOptions) => {

packages/shared/src/extractors/split.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,36 @@ export function isValidSelector(selector = ''): selector is string {
88

99
// 可选实现:export const splitCode = (code: string) => [...new Set(code.split(/\\?[\s'"`;={}]+/g))].filter(isValidSelector)
1010

11+
/**
12+
* splitCode 结果缓存,避免对相同字符串重复分割和过滤。
13+
* 使用两个独立缓存分别对应 allowDoubleQuotes 的两种取值。
14+
*/
15+
const splitCacheDefault = new Map<string, string[]>()
16+
const splitCacheAllowQuotes = new Map<string, string[]>()
17+
const SPLIT_CACHE_LIMIT = 8192
18+
19+
/** 预编译的分割正则,避免每次调用都创建 */
20+
const SPLITTER_DEFAULT = /\s+|"/
21+
const SPLITTER_ALLOW_QUOTES = /\s+/
22+
1123
export function splitCode(code: string, allowDoubleQuotes = false) {
24+
const cache = allowDoubleQuotes ? splitCacheAllowQuotes : splitCacheDefault
25+
const cached = cache.get(code)
26+
if (cached) {
27+
return cached
28+
}
29+
1230
// 把压缩产物中的转义空白字符(\n \r \t)先还原成空格,避免被粘连到类名上
1331
const normalized = code.includes('\\') ? code.replace(/\\[nrt]/g, ' ') : code
1432

15-
// 参数 onlyWhiteSpace?: boolean
16-
// const regex = onlyWhiteSpace ? /[\s]+/ : /"|[\s]+/
17-
// 默认使用 /\s+/
18-
// 用于处理 Vue 的静态节点
19-
// 示例:|class="
20-
const splitter = allowDoubleQuotes ? /\s+/ : /\s+|"/
21-
return normalized.split(splitter).filter(element => isValidSelector(element))
33+
const splitter = allowDoubleQuotes ? SPLITTER_ALLOW_QUOTES : SPLITTER_DEFAULT
34+
const result = normalized.split(splitter).filter(element => isValidSelector(element))
35+
36+
// 防止缓存无限增长
37+
if (cache.size >= SPLIT_CACHE_LIMIT) {
38+
cache.clear()
39+
}
40+
cache.set(code, result)
41+
42+
return result
2243
}

packages/weapp-tailwindcss/src/js/handlers.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,19 @@ import type { StringLiteral, TemplateElement } from '@babel/types'
33
import type { IJsHandlerOptions } from '../types'
44
import type { JsToken } from './types'
55
import { jsStringEscape } from '@ast-core/escape'
6-
import { escapeStringRegexp } from '@weapp-core/regex'
76
import { splitCode } from '@weapp-tailwindcss/shared/extractors'
87
import { createDebug } from '@/debug'
9-
import { resolveClassNameTransformDecision, shouldEnableArbitraryValueFallback } from '../shared/classname-transform'
8+
import { resolveClassNameTransformWithResult, shouldEnableArbitraryValueFallback } from '../shared/classname-transform'
109
import { decodeUnicode2 } from '../utils/decode'
1110
import { replaceWxml } from '../wxml/shared'
1211
import { isClassContextLiteralPath } from './class-context'
1312

1413
type EscapeMap = NonNullable<IJsHandlerOptions['escapeMap']>
1514

1615
const debug = createDebug('[js:handlers] ')
17-
const patternCache = new Map<string, RegExp>()
1816
const replacementCacheByEscapeMap = new WeakMap<EscapeMap, Map<string, string>>()
1917
const defaultReplacementCache = new Map<string, string>()
2018

21-
function getPattern(candidate: string) {
22-
let cached = patternCache.get(candidate)
23-
if (!cached) {
24-
cached = new RegExp(escapeStringRegexp(candidate))
25-
patternCache.set(candidate, cached)
26-
}
27-
return cached
28-
}
29-
3019
function getReplacement(candidate: string, escapeMap?: EscapeMap) {
3120
if (!escapeMap) {
3221
let cached = defaultReplacementCache.get(candidate)
@@ -51,6 +40,23 @@ function getReplacement(candidate: string, escapeMap?: EscapeMap) {
5140
return cached
5241
}
5342

43+
/**
44+
* 将 replacement 写入对应的缓存,供后续 getReplacement 命中。
45+
*/
46+
function setReplacementCache(candidate: string, replacement: string, escapeMap?: EscapeMap) {
47+
if (!escapeMap) {
48+
defaultReplacementCache.set(candidate, replacement)
49+
}
50+
else {
51+
let store = replacementCacheByEscapeMap.get(escapeMap)
52+
if (!store) {
53+
store = new Map<string, string>()
54+
replacementCacheByEscapeMap.set(escapeMap, store)
55+
}
56+
store.set(candidate, replacement)
57+
}
58+
}
59+
5460
function hasIgnoreComment(node: StringLiteral | TemplateElement) {
5561
return Array.isArray(node.leadingComments)
5662
&& node.leadingComments.some(comment => comment.value.includes('weapp-tw') && comment.value.includes('ignore'))
@@ -123,35 +129,43 @@ export function replaceHandleValue(
123129
const skippedSamples: string[] = []
124130

125131
for (const candidate of candidates) {
126-
const decision = resolveClassNameTransformDecision(candidate, {
132+
const result = resolveClassNameTransformWithResult(candidate, {
127133
...options,
128134
classContext,
129135
})
130-
if (decision === 'skip') {
136+
if (result.decision === 'skip') {
131137
if (skippedSamples.length < 6) {
132138
skippedSamples.push(candidate)
133139
}
134140
continue
135141
}
136142
matchedCandidateCount += 1
137-
if (decision === 'escaped') {
143+
if (result.decision === 'escaped') {
138144
escapedDecisionCount += 1
139145
if (escapedSamples.length < 6) {
140146
escapedSamples.push(candidate)
141147
}
142148
}
143-
if (decision === 'fallback') {
149+
if (result.decision === 'fallback') {
144150
fallbackDecisionCount += 1
145151
}
146152

147153
if (!transformed.includes(candidate)) {
148154
continue
149155
}
150156

151-
const pattern = getPattern(candidate)
152-
const replacement = getReplacement(candidate, escapeMap)
153-
const replaced = transformed.replace(pattern, replacement)
157+
// 复用 escapedValue 或走缓存的 getReplacement,避免重复 escape 计算
158+
let replacement: string
159+
if (result.decision === 'escaped' && result.escapedValue) {
160+
replacement = result.escapedValue
161+
setReplacementCache(candidate, replacement, escapeMap)
162+
}
163+
else {
164+
replacement = getReplacement(candidate, escapeMap)
165+
}
154166

167+
// 使用 String.replace 仅替换首次出现,与原始行为一致
168+
const replaced = transformed.replace(candidate, replacement)
155169
if (replaced !== transformed) {
156170
transformed = replaced
157171
mutated = true

packages/weapp-tailwindcss/src/js/index.ts

Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,37 @@ export {
77
}
88

99
export function createJsHandler(options: CreateJsHandlerOptions): JsHandler {
10-
// 保留不可变的默认选项,重复调用时仅传入本次的覆盖项
11-
const {
12-
arbitraryValues,
13-
escapeMap,
14-
staleClassNameFallback,
15-
jsArbitraryValueFallback,
16-
tailwindcssMajorVersion,
17-
jsPreserveClass,
18-
generateMap,
19-
needEscaped,
20-
alwaysEscape,
21-
unescapeUnicode,
22-
babelParserOptions,
23-
ignoreCallExpressionIdentifiers,
24-
ignoreTaggedTemplateExpressionIdentifiers,
25-
uniAppX,
26-
moduleSpecifierReplacements,
27-
} = options
10+
// 预构建不可变的默认选项对象,避免每次调用都重新创建字面量
11+
const defaults: IJsHandlerOptions = {
12+
escapeMap: options.escapeMap,
13+
staleClassNameFallback: options.staleClassNameFallback,
14+
jsArbitraryValueFallback: options.jsArbitraryValueFallback,
15+
tailwindcssMajorVersion: options.tailwindcssMajorVersion,
16+
arbitraryValues: options.arbitraryValues,
17+
jsPreserveClass: options.jsPreserveClass,
18+
generateMap: options.generateMap,
19+
needEscaped: options.needEscaped,
20+
alwaysEscape: options.alwaysEscape,
21+
unescapeUnicode: options.unescapeUnicode,
22+
babelParserOptions: options.babelParserOptions,
23+
ignoreCallExpressionIdentifiers: options.ignoreCallExpressionIdentifiers,
24+
ignoreTaggedTemplateExpressionIdentifiers: options.ignoreTaggedTemplateExpressionIdentifiers,
25+
uniAppX: options.uniAppX,
26+
moduleSpecifierReplacements: options.moduleSpecifierReplacements,
27+
} as IJsHandlerOptions
2828

2929
function handler(rawSource: string, classNameSet?: Set<string>, options?: CreateJsHandlerOptions) {
30-
const overrideOptions = (options ?? {}) as IJsHandlerOptions
30+
// 快路径:无覆盖选项时跳过 defuOverrideArray,直接合并 classNameSet
31+
if (!options || Object.keys(options).length === 0) {
32+
return jsHandler(rawSource, { ...defaults, classNameSet })
33+
}
34+
3135
const resolvedOptions = defuOverrideArray<IJsHandlerOptions, IJsHandlerOptions[]>(
3236
{
33-
...overrideOptions,
34-
classNameSet,
35-
},
36-
{
37+
...(options as IJsHandlerOptions),
3738
classNameSet,
38-
escapeMap,
39-
staleClassNameFallback,
40-
jsArbitraryValueFallback,
41-
tailwindcssMajorVersion,
42-
arbitraryValues,
43-
jsPreserveClass,
44-
generateMap,
45-
needEscaped,
46-
alwaysEscape,
47-
unescapeUnicode,
48-
babelParserOptions,
49-
ignoreCallExpressionIdentifiers,
50-
ignoreTaggedTemplateExpressionIdentifiers,
51-
uniAppX,
52-
moduleSpecifierReplacements,
5339
},
40+
defaults,
5441
)
5542

5643
return jsHandler(rawSource, resolvedOptions)

0 commit comments

Comments
 (0)