diff --git a/benchmark/index.ts b/benchmark/index.ts
index f921a3e..c6fa01c 100644
--- a/benchmark/index.ts
+++ b/benchmark/index.ts
@@ -1,4 +1,5 @@
// oxlint-disable no-unused-vars
+///
import { Bench } from 'tinybench'
import { parse, tokenize, walk } from '../dist/index.js'
import * as fs from 'node:fs'
@@ -7,7 +8,6 @@ import * as path from 'node:path'
import * as csstree from 'css-tree'
import * as postcss from 'postcss'
-// Sample CSS for benchmarking - realistic production-like CSS
const largeCSS = fs.readFileSync(path.resolve('benchmark/medium.css'), 'utf-8')
const bootstrapCSS = fs.readFileSync(
path.resolve('node_modules/bootstrap/dist/css/bootstrap.css'),
@@ -18,129 +18,306 @@ const tailwindCSS = fs.readFileSync(
'utf-8',
)
-const bench = new Bench({ time: 1000, warmup: true })
+type CSSFile = 'Large' | 'Bootstrap' | 'Tailwind'
-// Tokenizer benchmarks
-bench
- .add('Tokenizer - Large CSS', () => {
- for (const _token of tokenize(largeCSS)) {
- // Just iterate
- }
- })
- .add('Tokenizer - Bootstrap CSS', () => {
- for (const _token of tokenize(bootstrapCSS)) {
- // Just iterate
- }
- })
- .add('Tokenizer - Tailwind CSS', () => {
- for (const _token of tokenize(tailwindCSS)) {
- // Just iterate
+const files: CSSFile[] = ['Large', 'Bootstrap', 'Tailwind']
+
+const cssMap: Record = {
+ Large: largeCSS,
+ Bootstrap: bootstrapCSS,
+ Tailwind: tailwindCSS,
+}
+
+const fileSizes: Record = {
+ Large: largeCSS.length,
+ Bootstrap: bootstrapCSS.length,
+ Tailwind: tailwindCSS.length,
+}
+
+// Pre-parse once for walk-only benchmarks so parse time doesn't pollute walk timings
+const parsedMap = {
+ Large: parse(largeCSS),
+ Bootstrap: parse(bootstrapCSS),
+ Tailwind: parse(tailwindCSS),
+}
+
+const quick = process.argv.includes('--quick')
+
+// ─── Speed benchmarks ─────────────────────────────────────────────────────
+
+const bench = new Bench({ warmup: true })
+
+for (const file of files) {
+ const css = cssMap[file]
+ const parsed = parsedMap[file]
+
+ bench.add(`Tokenize|${file}`, () => {
+ for (const _token of tokenize(css)) {
+ // iterate
}
})
-// Parser benchmarks
-bench
- .add('Parser - Large CSS', () => {
- parse(largeCSS)
+ bench.add(`Parse|${file}`, () => {
+ parse(css)
})
- .add('Parser - Bootstrap CSS', () => {
- parse(bootstrapCSS)
- })
- .add('Parser - Tailwind CSS', () => {
- parse(tailwindCSS)
+
+ bench.add(`Walk|${file}`, () => {
+ walk(parsed, (node, _depth) => {
+ void node.type
+ void node.line
+ })
})
-bench
- .add('Parse/walk - Wallace - Bootstrap CSS', () => {
- let ast = parse(bootstrapCSS)
- let count = 0
+ bench.add(`Parse+Walk|${file}`, () => {
+ let ast = parse(css)
walk(ast, (node, _depth) => {
- let type = node.type
- let line = node.line
- count++
+ void node.type
+ void node.line
})
})
- .add('Parse/walk - CSSTree - Bootstrap CSS', () => {
- let ast = csstree.parse(bootstrapCSS, { positions: true })
- let count = 0
- // @ts-expect-error: no type definitions for css-tree
- csstree.walk(ast, (node) => {
- let type = node.type
- let line = node.loc?.start.line
- count++
+
+ if (!quick) {
+ bench.add(`Fair-Wallace|${file}`, () => {
+ let ast = parse(css, {
+ parse_selectors: false,
+ parse_values: false,
+ parse_atrule_preludes: false,
+ })
+ walk(ast, (node, _depth) => {
+ void node.type
+ void node.line
+ })
})
- })
- .add('Parse/walk - PostCSS - Bootstrap CSS', () => {
- let root = postcss.parse(bootstrapCSS)
- let count = 0
- root.walk((node) => {
- let type = node.type
- let line = node.source?.start?.line
- count++
+
+ bench.add(`Fair-CSSTree|${file}`, () => {
+ let ast = csstree.parse(css, {
+ positions: true,
+ parseValue: false,
+ parseAtrulePrelude: false,
+ parseRulePrelude: false,
+ })
+ // @ts-expect-error: no type definitions for css-tree
+ csstree.walk(ast, (node) => {
+ void node.type
+ void node.loc?.start.line
+ })
})
- })
-bench
- .add('Parse/walk - Wallace - Tailwind CSS', () => {
- let ast = parse(tailwindCSS)
- let count = 0
- walk(ast, (node, _depth) => {
- let type = node.type
- let line = node.line
- count++
+ bench.add(`CSSTree-Parse|${file}`, () => {
+ csstree.parse(css, { positions: true })
})
- })
- .add('Parse/walk - CSSTree - Tailwind CSS', () => {
- let ast = csstree.parse(tailwindCSS, { positions: true })
- let count = 0
- // @ts-expect-error: no type definitions for css-tree
- csstree.walk(ast, (node) => {
- let type = node.type
- let line = node.loc?.start.line
- count++
+
+ bench.add(`PostCSS-Parse|${file}`, () => {
+ postcss.parse(css)
})
- })
- .add('Parse/walk - PostCSS - Tailwind CSS', () => {
- let root = postcss.parse(tailwindCSS)
- let count = 0
- root.walk((node) => {
- let type = node.type
- let line = node.source?.start?.line
- count++
+
+ bench.add(`CSSTree|${file}`, () => {
+ let ast = csstree.parse(css, { positions: true })
+ // @ts-expect-error: no type definitions for css-tree
+ csstree.walk(ast, (node) => {
+ void node.type
+ void node.loc?.start.line
+ })
})
- })
+
+ bench.add(`PostCSS|${file}`, () => {
+ let root = postcss.parse(css)
+ root.walk((node) => {
+ void node.type
+ void node.source?.start?.line
+ })
+ })
+ }
+}
await bench.run()
-// File sizes
-const fileSizes = {
- large: largeCSS.length,
- bootstrap: bootstrapCSS.length,
- tailwind: tailwindCSS.length,
+// ─── Helpers ───────────────────────────────────────────────────────────────
+
+function ops(name: string): number {
+ const result = bench.tasks.find((t) => t.name === name)?.result
+ const stats = result && 'latency' in result ? result : null
+ return stats?.throughput?.mean ?? 0
}
-function getFileSize(taskName: string): string {
- const name = taskName.toLowerCase()
- if (name.includes('bootstrap')) {
- return `${(fileSizes.bootstrap / 1024).toFixed(2)} KB`
- } else if (name.includes('large')) {
- return `${(fileSizes.large / 1024).toFixed(2)} KB`
- } else if (name.includes('tailwind')) {
- return `${(fileSizes.tailwind / 1024).toFixed(2)} KB`
+function fmtOps(n: number): string {
+ return n > 0 ? n.toFixed(0) : 'N/A'
+}
+
+function fmtSize(file: CSSFile): string {
+ return `${(fileSizes[file] / 1024).toFixed(0)} KB`
+}
+
+function fmtMB(mb: number): string {
+ return `${mb.toFixed(1)} MB`
+}
+
+function forceGC(rounds = 5): void {
+ for (let i = 0; i < rounds; i++) {
+ ;(globalThis as { gc?: () => void }).gc!()
}
- return 'N/A'
}
-// Display results
-console.table(
- bench.tasks.map(({ name, result }) => {
- const stats = result && 'latency' in result ? result : null
- return {
- 'Task Name': name,
- 'File Size': getFileSize(name),
- 'ops/sec': stats?.throughput.mean.toFixed(0) ?? 'N/A',
- 'Average Time (ms)': stats?.latency.mean.toFixed(4) ?? 'N/A',
- Margin: stats?.latency.rme === null ? 'N/A' : `±${stats.latency.rme.toFixed(2)}%`,
+function measureMemoryMB(
+ css: string,
+ parser: 'wallace' | 'csstree' | 'postcss',
+ iterations = 3,
+): number {
+ const deltas: number[] = []
+
+ for (let i = 0; i < iterations; i++) {
+ forceGC()
+ const before = process.memoryUsage()
+
+ if (parser === 'wallace') {
+ let ast = parse(css)
+ walk(ast, (node, _depth) => {
+ void node.type
+ void node.line
+ })
+ } else if (parser === 'csstree') {
+ let ast = csstree.parse(css, { positions: true })
+ // @ts-expect-error: no type definitions for css-tree
+ csstree.walk(ast, (node) => {
+ void node.type
+ void node.loc?.start.line
+ })
+ } else {
+ let root = postcss.parse(css)
+ root.walk((node) => {
+ void node.type
+ void node.source?.start?.line
+ })
}
- }),
+
+ const after = process.memoryUsage()
+ deltas.push(after.heapUsed + after.external - (before.heapUsed + before.external))
+ }
+
+ const avg = deltas.reduce((a, b) => a + b, 0) / iterations
+ return avg / 1024 / 1024
+}
+
+// ─── Table 1: Wallace metrics ──────────────────────────────────────────────
+
+console.log('\n── Table 1: Wallace CSS Parser ──────────────────────────────────────────\n')
+
+console.table(
+ files.map((file) => ({
+ File: file,
+ Size: fmtSize(file),
+ 'Tokenize (ops/sec)': fmtOps(ops(`Tokenize|${file}`)),
+ 'Parse (ops/sec)': fmtOps(ops(`Parse|${file}`)),
+ 'Walk (ops/sec)': fmtOps(ops(`Walk|${file}`)),
+ 'Parse+Walk (ops/sec)': fmtOps(ops(`Parse+Walk|${file}`)),
+ })),
)
+
+if (!quick) {
+ // ─── Table 2: Parse speed comparison ────────────────────────────────────
+
+ console.log('\n── Table 2: Parse Speed – Wallace (baseline) vs CSSTree vs PostCSS ───────\n')
+
+ console.table(
+ files.map((file) => {
+ const w = ops(`Parse|${file}`)
+ const c = ops(`CSSTree-Parse|${file}`)
+ const p = ops(`PostCSS-Parse|${file}`)
+ return {
+ File: file,
+ Size: fmtSize(file),
+ 'Wallace (ops/sec)': fmtOps(w),
+ 'CSSTree (ops/sec)': fmtOps(c),
+ 'PostCSS (ops/sec)': fmtOps(p),
+ 'vs CSSTree': c > 0 ? `${(w / c).toFixed(1)}x faster` : 'N/A',
+ 'vs PostCSS': p > 0 ? `${(w / p).toFixed(1)}x faster` : 'N/A',
+ }
+ }),
+ )
+
+ // ─── Table 3: Parse+Walk speed comparison ───────────────────────────────
+
+ console.log('\n── Table 3: Parse+Walk Speed – Wallace (baseline) vs CSSTree vs PostCSS ──\n')
+
+ console.table(
+ files.map((file) => {
+ const w = ops(`Parse+Walk|${file}`)
+ const c = ops(`CSSTree|${file}`)
+ const p = ops(`PostCSS|${file}`)
+ return {
+ File: file,
+ Size: fmtSize(file),
+ 'Wallace (ops/sec)': fmtOps(w),
+ 'CSSTree (ops/sec)': fmtOps(c),
+ 'PostCSS (ops/sec)': fmtOps(p),
+ 'vs CSSTree': c > 0 ? `${(w / c).toFixed(1)}x faster` : 'N/A',
+ 'vs PostCSS': p > 0 ? `${(w / p).toFixed(1)}x faster` : 'N/A',
+ }
+ }),
+ )
+
+ // ─── Table 4: Fair Parse+Walk comparison (no sub-parsing) ───────────────
+
+ console.log(
+ '\n── Table 4: Parse+Walk Speed (fair) – Wallace no sub-parsing vs CSSTree no sub-parsing vs PostCSS ──\n',
+ )
+
+ console.table(
+ files.map((file) => {
+ const w = ops(`Fair-Wallace|${file}`)
+ const c = ops(`Fair-CSSTree|${file}`)
+ const p = ops(`PostCSS|${file}`)
+ return {
+ File: file,
+ Size: fmtSize(file),
+ 'Wallace (ops/sec)': fmtOps(w),
+ 'CSSTree (ops/sec)': fmtOps(c),
+ 'PostCSS (ops/sec)': fmtOps(p),
+ 'vs CSSTree': c > 0 ? `${(w / c).toFixed(1)}x faster` : 'N/A',
+ 'vs PostCSS': p > 0 ? `${(w / p).toFixed(1)}x faster` : 'N/A',
+ }
+ }),
+ )
+}
+
+// ─── Memory comparison ─────────────────────────────────────────────────────
+
+const hasGC = typeof (globalThis as { gc?: () => void }).gc === 'function'
+const memTableNum = quick ? 2 : 5
+
+if (hasGC) {
+ if (quick) {
+ console.log('\n── Table 2: Parse+Walk Memory – Wallace ─────────────────────────────────\n')
+
+ console.table(
+ files.map((file) => {
+ const w = measureMemoryMB(cssMap[file], 'wallace')
+ return { File: file, Size: fmtSize(file), Wallace: fmtMB(w) }
+ }),
+ )
+ } else {
+ console.log('\n── Table 5: Parse+Walk Memory – Wallace (baseline) vs CSSTree vs PostCSS ─\n')
+
+ console.table(
+ files.map((file) => {
+ const css = cssMap[file]
+ const w = measureMemoryMB(css, 'wallace')
+ const c = measureMemoryMB(css, 'csstree')
+ const p = measureMemoryMB(css, 'postcss')
+ return {
+ File: file,
+ Size: fmtSize(file),
+ Wallace: fmtMB(w),
+ CSSTree: fmtMB(c),
+ PostCSS: fmtMB(p),
+ 'vs CSSTree': c > 0 ? `${(c / w).toFixed(1)}x less` : 'N/A',
+ 'vs PostCSS': p > 0 ? `${(p / w).toFixed(1)}x less` : 'N/A',
+ }
+ }),
+ )
+ }
+} else {
+ console.log(
+ `\n── Table ${memTableNum}: Memory Usage – skipped (run with: node --expose-gc benchmark/index.js) ──\n`,
+ )
+}
diff --git a/benchmark/memory.ts b/benchmark/memory.ts
deleted file mode 100644
index f954b32..0000000
--- a/benchmark/memory.ts
+++ /dev/null
@@ -1,281 +0,0 @@
-// oxlint-disable no-unused-vars
-// Memory benchmark for CSS parsers
-// Run with: node --expose-gc benchmark/memory.js
-
-import { parse, walk } from '../dist/index.js'
-import * as fs from 'node:fs'
-import * as path from 'node:path'
-// @ts-expect-error: no type definitions for css-tree
-import * as csstree from 'css-tree'
-import * as postcss from 'postcss'
-
-// Check if GC is available
-if (typeof global.gc !== 'function') {
- console.error('Error: This benchmark requires --expose-gc flag')
- console.error('Run with: node --expose-gc benchmark/memory.js')
- process.exit(1)
-}
-
-// Load CSS files
-const smallCSS = fs.readFileSync(path.resolve('benchmark/small.css'), 'utf-8')
-const mediumCSS = fs.readFileSync(path.resolve('benchmark/medium.css'), 'utf-8')
-const bootstrapCSS = fs.readFileSync(
- path.resolve('node_modules/bootstrap/dist/css/bootstrap.css'),
- 'utf-8',
-)
-const tailwindCSS = fs.readFileSync(
- path.resolve('node_modules/tailwindcss/dist/tailwind.css'),
- 'utf-8',
-)
-
-interface MemorySnapshot {
- heapUsed: number
- external: number
- total: number
-}
-
-interface MemoryResult {
- fileName: string
- fileSize: number
- baseline: MemorySnapshot
- afterParse: MemorySnapshot
- afterWalk: MemorySnapshot
- afterCleanup: MemorySnapshot
- parseDelta: number
- walkDelta: number
- totalDelta: number
- retainedDelta: number
-}
-
-function formatBytes(bytes: number): string {
- return `${(bytes / 1024 / 1024).toFixed(2)} MB`
-}
-
-function getMemorySnapshot(): MemorySnapshot {
- const mem = process.memoryUsage()
- return {
- heapUsed: mem.heapUsed,
- external: mem.external,
- total: mem.heapUsed + mem.external,
- }
-}
-
-function forceGC(rounds = 5): void {
- for (let i = 0; i < rounds; i++) {
- global.gc!()
- }
-}
-
-function measureMemory(
- fileName: string,
- cssContent: string,
- parser: 'wallace' | 'csstree' | 'postcss',
-): MemoryResult {
- // Force GC and get baseline
- forceGC()
- const baseline = getMemorySnapshot()
-
- let ast: any = null
-
- // Parse
- if (parser === 'wallace') {
- ast = parse(cssContent)
- } else if (parser === 'csstree') {
- ast = csstree.parse(cssContent, { positions: true })
- } else if (parser === 'postcss') {
- ast = postcss.parse(cssContent)
- }
-
- const afterParse = getMemorySnapshot()
-
- // Walk
- let count = 0
- if (parser === 'wallace') {
- walk(ast, (node, _depth) => {
- const type = node.type
- const line = node.line
- count++
- })
- } else if (parser === 'csstree') {
- csstree.walk(ast, (node: any) => {
- const type = node.type
- const line = node.loc?.start.line
- count++
- })
- } else if (parser === 'postcss') {
- ast.walk((node: any) => {
- const type = node.type
- const line = node.source?.start?.line
- count++
- })
- }
-
- const afterWalk = getMemorySnapshot()
-
- // Cleanup and measure retained memory
- ast = null
- forceGC()
- const afterCleanup = getMemorySnapshot()
-
- const parseDelta = afterParse.total - baseline.total
- const walkDelta = afterWalk.total - afterParse.total
- const totalDelta = afterWalk.total - baseline.total
- const retainedDelta = afterCleanup.total - baseline.total
-
- return {
- fileName,
- fileSize: cssContent.length,
- baseline,
- afterParse,
- afterWalk,
- afterCleanup,
- parseDelta,
- walkDelta,
- totalDelta,
- retainedDelta,
- }
-}
-
-function runBenchmark(
- name: string,
- css: string,
- parser: 'wallace' | 'csstree' | 'postcss',
- iterations = 3,
-): MemoryResult {
- const results: MemoryResult[] = []
-
- for (let i = 0; i < iterations; i++) {
- results.push(measureMemory(name, css, parser))
- }
-
- // Average the results
- const avg: MemoryResult = {
- fileName: name,
- fileSize: results[0].fileSize,
- baseline: results[0].baseline,
- afterParse: results[0].afterParse,
- afterWalk: results[0].afterWalk,
- afterCleanup: results[0].afterCleanup,
- parseDelta: results.reduce((sum, r) => sum + r.parseDelta, 0) / iterations,
- walkDelta: results.reduce((sum, r) => sum + r.walkDelta, 0) / iterations,
- totalDelta: results.reduce((sum, r) => sum + r.totalDelta, 0) / iterations,
- retainedDelta: results.reduce((sum, r) => sum + r.retainedDelta, 0) / iterations,
- }
-
- return avg
-}
-
-console.log('Memory Benchmark for CSS Parsers')
-console.log('=================================\n')
-
-const testFiles = [
- { name: 'small.css', content: smallCSS },
- { name: 'medium.css', content: mediumCSS },
- { name: 'bootstrap.css', content: bootstrapCSS },
- { name: 'tailwind.css', content: tailwindCSS },
-]
-
-const parsers: Array<'wallace' | 'csstree' | 'postcss'> = ['wallace', 'csstree', 'postcss']
-
-for (const parser of parsers) {
- console.log(`\n${parser.toUpperCase()} Parser`)
- console.log('-'.repeat(80))
-
- const results: MemoryResult[] = []
-
- for (const file of testFiles) {
- const result = runBenchmark(file.name, file.content, parser, 3)
- results.push(result)
- }
-
- console.table(
- results.map((r) => ({
- File: r.fileName,
- 'Size (KB)': (r.fileSize / 1024).toFixed(2),
- 'Parse (+MB)': formatBytes(r.parseDelta),
- 'Walk (+MB)': formatBytes(r.walkDelta),
- 'Total (+MB)': formatBytes(r.totalDelta),
- 'Retained (+MB)': formatBytes(r.retainedDelta),
- 'Memory Efficiency': `${((r.fileSize / r.totalDelta) * 100).toFixed(1)}%`,
- })),
- )
-}
-
-// Comparison table
-console.log('\n\nCOMPARISON: Parse + Walk Memory Usage')
-console.log('-'.repeat(80))
-
-const comparisonResults: { [key: string]: { [parser: string]: number } } = {}
-
-for (const file of testFiles) {
- comparisonResults[file.name] = {}
- for (const parser of parsers) {
- const result = runBenchmark(file.name, file.content, parser, 3)
- comparisonResults[file.name][parser] = result.totalDelta
- }
-}
-
-const comparisonTable = testFiles.map((file) => {
- const wallace = comparisonResults[file.name]['wallace']
- const tree = comparisonResults[file.name]['csstree']
- const post = comparisonResults[file.name]['postcss']
-
- return {
- File: file.name,
- 'Size (KB)': (file.content.length / 1024).toFixed(2),
- 'Wallace (MB)': formatBytes(wallace),
- 'CSSTree (MB)': formatBytes(tree),
- 'PostCSS (MB)': formatBytes(post),
- 'Wallace vs CSSTree': `${((wallace / tree) * 100).toFixed(1)}%`,
- 'Wallace vs PostCSS': `${((wallace / post) * 100).toFixed(1)}%`,
- }
-})
-
-console.table(comparisonTable)
-
-console.log('\n\nUNDERSTANDING THE METRICS')
-console.log('-'.repeat(80))
-console.log('\n📊 Memory Measurements Explained:\n')
-
-console.log('Parse (+MB):')
-console.log(' Memory consumed during parsing (creating the AST)')
-console.log(' Lower is better - indicates efficient AST representation\n')
-
-console.log('Walk (+MB):')
-console.log(' Additional memory used during AST traversal')
-console.log(' Can be negative if GC runs during walking\n')
-
-console.log('Total (+MB):')
-console.log(' Peak memory usage = Parse + Walk')
-console.log(' Total memory overhead for parse + walk operation\n')
-
-console.log('Retained (+MB): ⚠️ IMPORTANT METRIC')
-console.log(' Memory that remains allocated AFTER clearing references and forcing GC')
-console.log(' This represents memory leaks or objects kept alive by closures/caches')
-console.log(' ')
-console.log(' What it means:')
-console.log(' • Close to 0 MB: Excellent - minimal retention, GC cleaned up everything')
-console.log(' • Low (< 5% of Total): Good - some minor caching or retained references')
-console.log(' • High (> 20% of Total): Concerning - potential memory leaks or large caches')
-console.log(' ')
-console.log(' Why it matters:')
-console.log(' • In long-running processes (servers, build tools, linters), retained memory')
-console.log(' accumulates over time if not properly released')
-console.log(' • Low retained memory means the parser can be used repeatedly without')
-console.log(' gradually consuming all available memory\n')
-
-console.log('Memory Efficiency:')
-console.log(' Ratio of input file size to memory consumed')
-console.log(' Higher percentage = more efficient (closer to file size)\n')
-
-console.log('Wallace vs X:')
-console.log(' Percentage of memory used compared to competitor')
-console.log(' Lower is better (e.g., 47.3% = Wallace uses less than half the memory)\n')
-
-console.log('-'.repeat(80))
-console.log('\n✅ Key Takeaways:')
-console.log(" • Wallace's arena-based design minimizes allocations during parsing")
-console.log(' • Lower "Total" memory means faster parsing (better cache locality)')
-console.log(' • Lower "Retained" memory means safer for long-running applications')
-console.log(' • All parsers can be garbage collected when done (references cleared)')
-console.log('')
diff --git a/knip.json b/knip.json
index 807fac5..50744cd 100644
--- a/knip.json
+++ b/knip.json
@@ -1,4 +1,11 @@
{
"ignore": ["/benchmark/**"],
- "ignoreDependencies": ["@projectwallace/preset-oxlint", "bootstrap", "tailwindcss"]
+ "ignoreDependencies": [
+ "@projectwallace/preset-oxlint",
+ "bootstrap",
+ "css-tree",
+ "postcss",
+ "tailwindcss",
+ "tinybench"
+ ]
}
diff --git a/package.json b/package.json
index 805397e..98ebb72 100644
--- a/package.json
+++ b/package.json
@@ -79,8 +79,7 @@
"test-coverage": "vitest --coverage",
"test-build": "pnpm run build && vitest run --config vitest.config.build.ts",
"build": "tsdown",
- "benchmark": "pnpm run build && node benchmark/index.ts",
- "benchmark:memory": "pnpm run build && node --expose-gc benchmark/memory.ts",
+ "benchmark": "pnpm run build && node --expose-gc benchmark/index.ts",
"lint": "oxlint --config .oxlintrc.json && oxfmt --check",
"check": "tsc --noEmit",
"knip": "knip",