Skip to content

Commit

Permalink
perf: remove unnecessary conditions compare
Browse files Browse the repository at this point in the history
  • Loading branch information
sastan committed Jan 28, 2022
1 parent edbd1c7 commit b526c88
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 84 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-doors-flash.md
@@ -0,0 +1,5 @@
---
'twind': patch
---

perf: remove unnecessary conditions compare
5 changes: 5 additions & 0 deletions packages/preset-tailwind/src/index.ts
Expand Up @@ -37,8 +37,13 @@ export default function presetTailwind({
}

function hashVars(value: string, { h }: Context<TailwindTheme>): string {
// PERF: check for --tw before running the regexp
// if (value.includes('--tw')) {
return value.replace(
/--(tw-[\w-]+)\b/g,
(_, property: string) => '--' + h(property).replace('#', ''),
)
// }

// return value
}
4 changes: 2 additions & 2 deletions packages/preset-tailwind/src/rules.ts
Expand Up @@ -1094,8 +1094,8 @@ function filter(prefix = ''): Rule<TailwindTheme>[] {
[
// hue-rotate can be negated
`${key[0] == 'h' ? '-?' : ''}(${prefix}${key})(?:-|$)`,
fromTheme<TailwindTheme, 'hueRotate'>(
key as 'hueRotate',
fromTheme<TailwindTheme, 'hueRotate' | 'dropShadow'>(
key as 'hueRotate' | 'dropShadow',
({ 1: $1, _ }) =>
({
[`--tw-${$1}`]: asArray(_)
Expand Down
47 changes: 32 additions & 15 deletions packages/twind/src/internal/context.ts
Expand Up @@ -143,27 +143,27 @@ function getRuleResolver<Theme extends BaseTheme = BaseTheme>(
}

function createVariantFunction<Theme extends BaseTheme = BaseTheme>(
condition: MaybeArray<string | RegExp>,
patterns: MaybeArray<string | RegExp>,

resolve: string | VariantResolver<Theme>,
): VariantFunction<Theme> {
return createResolve(condition, typeof resolve == 'function' ? resolve : () => resolve)
return createResolve(patterns, typeof resolve == 'function' ? resolve : () => resolve)
}

function createResolveFunction<Theme extends BaseTheme = BaseTheme>(
condition: MaybeArray<string | RegExp> | Shortcuts<Theme>,
patterns: MaybeArray<string | RegExp> | Shortcuts<Theme>,

resolve?: keyof CSSProperties | string | CSSObject | RuleResolver<Theme>,

convert?: MatchConverter<Theme>,
): ResolveFunction<Theme> {
// This is a shortcuts object
if (Object.getPrototypeOf(condition) === Object.prototype) {
if (Object.getPrototypeOf(patterns) === Object.prototype) {
return createExecutor(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
Object.keys(condition).map((key) => {
Object.keys(patterns).map((key) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
const value = (condition as Shortcuts<Theme>)[key]
const value = (patterns as Shortcuts<Theme>)[key]
return createResolveFunction(key, typeof value == 'function' ? value : () => value)
}),
(value, resolver, context) => {
Expand All @@ -183,7 +183,7 @@ function createResolveFunction<Theme extends BaseTheme = BaseTheme>(
}

return createResolve(
condition as MaybeArray<string | RegExp>,
patterns as MaybeArray<string | RegExp>,
!resolve
? (match) =>
({
Expand All @@ -210,21 +210,21 @@ function maybeNegate($_: string, value: string): string {
}

function createResolve<Result, Theme extends BaseTheme = BaseTheme>(
condition: MaybeArray<string | RegExp>,
patterns: MaybeArray<string | RegExp>,
resolve: (match: MatchResult, context: Context<Theme>) => Result,
): (value: string, context: Context<Theme>) => Result | undefined {
return createRegExpExecutor(condition, (value, condition, context) =>
return createRegExpExecutor(patterns, (value, condition, context) =>
exec(value, condition, resolve, context),
)
}

function exec<Result, Theme extends BaseTheme = BaseTheme>(
value: string,
condition: RegExp,
condition: Condition,
resolve: (match: MatchResult, context: Context<Theme>) => Result,
context: Context<Theme>,
): Result | undefined {
const match = condition.exec(value) as MatchResult | null
const match = condition.exec(value) as MatchResult | Falsey

if (match) {
// MATCH.$_ = value
Expand All @@ -235,10 +235,10 @@ function exec<Result, Theme extends BaseTheme = BaseTheme>(
}

function createRegExpExecutor<Result, Theme extends BaseTheme = BaseTheme>(
condition: MaybeArray<string | RegExp>,
run: (value: string, condition: RegExp, context: Context<Theme>) => Result,
patterns: MaybeArray<string | RegExp>,
run: (value: string, condition: Condition, context: Context<Theme>) => Result,
): (value: string, context: Context<Theme>) => Result | undefined {
return createExecutor(asArray(condition).map(asRegExp), run)
return createExecutor(asArray(patterns).map(toCondition), run)
}

function createExecutor<Condition, Result, Theme extends BaseTheme = BaseTheme>(
Expand All @@ -254,12 +254,29 @@ function createExecutor<Condition, Result, Theme extends BaseTheme = BaseTheme>(
}
}

function asRegExp(value: string | RegExp): RegExp {
/**
* Executes a search on a string using a regular expression pattern, and returns an array containing the results of that search.
* @param string The String object or string literal on which to perform the search.
*/
// type Condition = (string: string) => RegExpExecArray | Falsey
type Condition = RegExp

function toCondition(value: string | RegExp): Condition {
// "visible" -> /^visible$/
// "(float)-(left|right|none)" -> /^(float)-(left|right|none)$/
// "auto-rows-" -> /^auto-rows-/
// "gap(-|$)" -> /^gap(-|$)/

// PERF: try to detect if we can skip the regex execution
// if (typeof value == 'string') {
// const prefix = /^[\w-#~@]+(?!\?)/.exec(value)?.[0]
// value = new RegExp('^' + value + (value.includes('$') || value.slice(-1) == '-' ? '' : '$'))
// if (prefix) {
// return (string) => string.startsWith(prefix) && (value as RegExp).exec(string)
// }
// }
// return (string) => (value as RegExp).exec(string)

return typeof value == 'string'
? new RegExp('^' + value + (value.includes('$') || value.slice(-1) == '-' ? '' : '$'))
: value
Expand Down
133 changes: 68 additions & 65 deletions packages/twind/src/internal/serialize.ts
Expand Up @@ -40,80 +40,77 @@ function serialize$<Theme extends BaseTheme = BaseTheme>(
const value = (style as Record<string, unknown>)[key]

if (key[0] == '@') {
// at rules: https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
if (!value) continue

// at rules: https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
switch (key[1]) {
// @apply ...;
case 'a': {
// @apply ...;
if (key[1] == 'a') {
rules.push(
...translateWith(
name as string,
precedence,
parse(value as string),
context,
precedence,
conditions,
important,
true /* useOrderOfRules */,
),
)
continue
}

// @layer <layer>
if (key[1] == 'l') {
for (const css of asArray(value as MaybeArray<CSSObject>)) {
rules.push(
...translateWith(
name as string,
precedence,
parse(value as string),
...serialize$(
css,
{
n: name,
p: moveToLayer(precedence, Layer[key[7] as 'b']),
r: conditions,
i: important,
},
context,
precedence,
conditions,
important,
true /* useOrderOfRules */,
),
)
continue
}

// @layer <layer>
case 'l': {
for (const css of asArray(value as MaybeArray<CSSObject>)) {
rules.push(
...serialize$(
css,
{
n: name,
p: moveToLayer(precedence, Layer[key[7] as 'b']),
r: conditions,
i: important,
},
context,
),
)
}

continue
}
continue
}

// @import
case 'i': {
rules.push({
// before all layers
p: -1,
o: 0,
r: [],
d: asArray(value)
.filter(Boolean)
.map((value) => key + ' ' + (value as string))
.join(';'),
})
continue
}
// @import
if (key[1] == 'i') {
rules.push({
// before all layers
p: -1,
o: 0,
r: [],
d: asArray(value)
.filter(Boolean)
.map((value) => key + ' ' + (value as string))
.join(';'),
})
continue
}

// @keyframes
// @font-face
// TODO @font-feature-values
case 'k':
case 'f': {
// Use base layer
rules.push({
p: Layer.d,
o: 0,
r: [key],
d: serialize$(value as CSSObject, { p: Layer.d }, context)
.map(stringify)
.join(''),
})
continue
}
// -> All other are handled below; same as selector
// @keyframes
// @font-face
// TODO @font-feature-values
if (key[1] == 'k' || key[1] == 'f') {
// Use base layer
rules.push({
p: Layer.d,
o: 0,
r: [key],
d: serialize$(value as CSSObject, { p: Layer.d }, context)
.map(stringify)
.join(''),
})
continue
}
// -> All other are handled below; same as selector
}

// @media
Expand Down Expand Up @@ -159,7 +156,7 @@ function serialize$<Theme extends BaseTheme = BaseTheme>(
name = (value as string) + hash(JSON.stringify([precedence, important, style]))
} else if (value || value === 0) {
// property -> hyphenate
key = key.replace(/[A-Z]/g, '-$&').toLowerCase()
key = key.replace(/[A-Z]/g, (_) => '-' + _.toLowerCase())

// Update precedence
numberOfDeclarations += 1
Expand All @@ -181,6 +178,7 @@ function serialize$<Theme extends BaseTheme = BaseTheme>(
}
}

// PERF: prevent unshift using `rules = [{}]` above and then `rules[0] = {...}`
rules.unshift({
n: name && context.h(name),

Expand All @@ -200,7 +198,6 @@ function serialize$<Theme extends BaseTheme = BaseTheme>(
d: declarations,
})

// only keep layer bits for merging
return rules.sort(compareTwindRules)
}

Expand All @@ -211,8 +208,14 @@ export function resolveThemeFunction<Theme extends BaseTheme = BaseTheme>(
// support theme(...) function in values
// calc(100vh - theme('spacing.12'))
// theme('borderColor.DEFAULT', 'currentColor')

// PERF: check for theme before running the regexp
// if (value.includes('theme')) {
return value.replace(
/theme\((["'`])?(.+?)\1(?:\s*,\s*(["'`])?(.+?)\3)?\)/g,
(_, __, key, ___, value) => context.theme(key, value) as string,
)
// }

// return value
}
3 changes: 2 additions & 1 deletion packages/twind/src/internal/sorted-insertion-index.ts
Expand Up @@ -37,7 +37,8 @@ export function compareTwindRules(a: TwindRule, b: TwindRule): number {
return (
a.p - b.p ||
a.o - b.o ||
collator.compare(a.r as unknown as string, b.r as unknown as string) ||
// XXX: should we compare the conditions as welll — already included in precedence
// collator.compare(a.r as unknown as string, b.r as unknown as string) ||
collator.compare(a.n as string, b.n as string)
)
}
2 changes: 1 addition & 1 deletion packages/twind/src/tests/inject-global.test.ts
Expand Up @@ -125,7 +125,7 @@ test('layers', () => {
'h1{font-size:1.5rem;line-height:2rem}',
'h2{font-size:1.25rem;line-height:1.75rem}',
'.select2-dropdown{border-bottom-left-radius:0.5rem;border-bottom-right-radius:0.5rem;--tw-shadow:0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}',
'.select2-results__group{font-size:1.125rem;line-height:1.75rem;font-weight:700;--tw-text-opacity:1;color:rgba(17,24,39,var(--tw-text-opacity))}',
'.select2-search{border-width:1px;--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity));border-radius:0.25rem}',
'.select2-results__group{font-size:1.125rem;line-height:1.75rem;font-weight:700;--tw-text-opacity:1;color:rgba(17,24,39,var(--tw-text-opacity))}',
])
})

0 comments on commit b526c88

Please sign in to comment.