Files
liquidjs/src/util/operator-trie.ts
T
Max MedveandGitHub 3a0d80d1f4 perf(parser): memoize createTrie to avoid rebuilding tries per Tokenizer (#911)
The Tokenizer constructor calls createTrie(operators) and
createTrie(literalValues) on every instantiation, and liquidjs builds a
fresh Tokenizer per output/tag while parsing. On typical templates this
rebuilt the same prefix-tries dozens of times and showed up as a large
share of parse CPU in profiling.

Memoize createTrie with a module-level WeakMap keyed on the input object.
The inputs (operators, literalValues) are stable references and the trie
is only ever read afterward (via matchTrie), never mutated, so caching by
reference is behavior-preserving. WeakMap (not Map) lets short-lived,
per-instance operator objects and their tries be garbage collected.
2026-06-22 20:11:06 +08:00

44 lines
1.1 KiB
TypeScript

import { isWord } from '../util/character'
interface TrieInput<T> {
[key: string]: T
}
export type Trie<T> = {
data?: T
end?: true
needBoundary?: true
} & Record<string, any>
// Tries are built once per input object and reused: the Tokenizer rebuilds them
// on every instantiation, but `input` (operators/literalValues) is a stable
// reference. WeakMap-keying by `input` lets short-lived operator objects (and
// their tries) be garbage collected. The returned trie is treated as read-only
// by callers (matchTrie only reads it); do not mutate it.
const trieCache = new WeakMap<TrieInput<any>, Trie<any>>()
export function createTrie<T = any> (input: TrieInput<T>): Trie<T> {
const cached = trieCache.get(input)
if (cached) return cached
const trie: Trie<T> = {}
for (const [name, data] of Object.entries(input)) {
let node = trie
for (let i = 0; i < name.length; i++) {
const c = name[i]
node[c] = node[c] || {}
if (i === name.length - 1 && isWord(name[i])) {
node[c].needBoundary = true
}
node = node[c]
}
node.data = data
node.end = true
}
trieCache.set(input, trie)
return trie
}