From 4d05d6d3a74b6104dbfaa9d62ca131515e83626d Mon Sep 17 00:00:00 2001 From: Simon Corry Date: Fri, 31 Jul 2026 12:29:05 -0400 Subject: [PATCH] Matcher entries compile their boundary regex lazily behind a substring prefilter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unicode-property boundary regexes are expensive to construct (~0.3ms each; a 140-entry consumer paid ~40ms of pure startup). Each entry now compiles on first use, and matching prefilters with a cheap lowercase substring check; the regex only confirms or rejects those few candidates. A boundary match is always also a substring match, so the semantics are byte-for-byte the same — the existing fixtures pin that. --- scripts/prose-matcher.js | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/scripts/prose-matcher.js b/scripts/prose-matcher.js index 467f82b..a2ed298 100644 --- a/scripts/prose-matcher.js +++ b/scripts/prose-matcher.js @@ -65,12 +65,29 @@ const WORD_EDGE_END = /[\p{L}\p{N}]$/u; // Word boundaries only guard edges that are themselves word characters; // a punctuation-edged phrase keeps substring semantics on that side. +// +// Performance shape: the boundary regexes use Unicode property escapes, +// which are expensive to CONSTRUCT (measured ~0.3ms each; a 140-entry +// list paid ~40ms at startup). So each entry compiles its regex lazily, +// and matching prefilters with a cheap lowercase substring check first: +// the regex only ever confirms or rejects a substring hit, and a +// boundary match is always also a substring match, so semantics are +// unchanged. export function compilePhrases(list) { return list.map(({ bad, good }) => { - const escaped = bad.replace(RE_SPECIALS, '\\$&'); - const lead = WORD_EDGE.test(bad) ? '(?