Skip to content

Commit ee3b8de

Browse files
committed
perf: add caching layer for frequently accessed operations
Add instance and result caching for hot paths in the rendering pipeline: - ProjectNode: cache root document entry with O(1) lookup, add fast-path to getDocumentEntry(), lazy cache invalidation in setDocumentEntries() - DocumentNode: add hasDocumentEntry() helper for safe checks - RenderContext: use hash map for O(1) document lookup by file path, optimize withDocument() to clone directly instead of rebuilding - DocumentNameResolver: cache URI analysis and resolved paths - ExternalReferenceResolver: pre-compile schema regex pattern - SluggerAnchorNormalizer: cache slugger instance and normalization results - AbstractUrlGenerator: cache isRelativePath() checks - RelativeUrlGenerator: cache computed relative paths - PreNodeRendererFactory: cache renderer lookups by node class - TwigEnvironmentBuilder: enable filesystem template caching - TwigTemplateRenderer: cache global context to avoid redundant addGlobal() See https://cybottm.github.io/render-guides/ for benchmark data.
1 parent 8acd95a commit ee3b8de

File tree

10 files changed

+233
-33
lines changed

10 files changed

+233
-33
lines changed

packages/guides/src/NodeRenderers/PreRenderers/PreNodeRendererFactory.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@
2121

2222
/**
2323
* Decorator to add pre-rendering logic to node renderers.
24+
*
25+
* Note: Caching assumes PreNodeRenderer::supports() only checks the node's
26+
* class type, not instance-specific properties. If a PreNodeRenderer needs
27+
* to check node properties, caching by class would return incorrect results.
2428
*/
2529
final class PreNodeRendererFactory implements NodeRendererFactory
2630
{
31+
/** @var array<class-string<Node>, NodeRenderer<Node>> */
32+
private array $cache = [];
33+
2734
public function __construct(
2835
private readonly NodeRendererFactory $innerFactory,
2936
/** @var iterable<PreNodeRenderer> */
@@ -33,6 +40,12 @@ public function __construct(
3340

3441
public function get(Node $node): NodeRenderer
3542
{
43+
// Cache by node class to avoid repeated preRenderer iteration
44+
$nodeFqcn = $node::class;
45+
if (isset($this->cache[$nodeFqcn])) {
46+
return $this->cache[$nodeFqcn];
47+
}
48+
3649
$preRenderers = [];
3750
foreach ($this->preRenderers as $preRenderer) {
3851
if (!$preRenderer->supports($node)) {
@@ -43,9 +56,9 @@ public function get(Node $node): NodeRenderer
4356
}
4457

4558
if (count($preRenderers) === 0) {
46-
return $this->innerFactory->get($node);
59+
return $this->cache[$nodeFqcn] = $this->innerFactory->get($node);
4760
}
4861

49-
return new PreRenderer($this->innerFactory->get($node), $preRenderers);
62+
return $this->cache[$nodeFqcn] = new PreRenderer($this->innerFactory->get($node), $preRenderers);
5063
}
5164
}

packages/guides/src/Nodes/DocumentNode.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ public function getFootnoteTargetAnonymous(): FootnoteTarget|null
279279
return null;
280280
}
281281

282+
public function hasDocumentEntry(): bool
283+
{
284+
return $this->documentEntry !== null;
285+
}
286+
282287
public function getDocumentEntry(): DocumentEntryNode
283288
{
284289
if ($this->documentEntry === null) {

packages/guides/src/ReferenceResolvers/DocumentNameResolver.php

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@
2323

2424
final class DocumentNameResolver implements DocumentNameResolverInterface
2525
{
26+
/** @var array<string, string> */
27+
private array $absoluteUrlCache = [];
28+
29+
/** @var array<string, string> */
30+
private array $canonicalUrlCache = [];
31+
32+
/** @var array<string, bool> */
33+
private array $isAbsoluteCache = [];
34+
35+
/** @var array<string, bool> */
36+
private array $isAbsolutePathCache = [];
37+
2638
/**
2739
* Returns the absolute path, including prefixing '/'.
2840
*
@@ -31,20 +43,31 @@ final class DocumentNameResolver implements DocumentNameResolverInterface
3143
*/
3244
public function absoluteUrl(string $basePath, string $url): string
3345
{
34-
$uri = BaseUri::from($url);
35-
if ($uri->isAbsolute()) {
36-
return $url;
46+
$cacheKey = $basePath . '|' . $url;
47+
if (isset($this->absoluteUrlCache[$cacheKey])) {
48+
return $this->absoluteUrlCache[$cacheKey];
49+
}
50+
51+
// Cache URI analysis results separately by URL
52+
if (!isset($this->isAbsoluteCache[$url])) {
53+
$uri = BaseUri::from($url);
54+
$this->isAbsoluteCache[$url] = $uri->isAbsolute();
55+
$this->isAbsolutePathCache[$url] = $uri->isAbsolutePath();
56+
}
57+
58+
if ($this->isAbsoluteCache[$url]) {
59+
return $this->absoluteUrlCache[$cacheKey] = $url;
3760
}
3861

39-
if ($uri->isAbsolutePath()) {
40-
return $url;
62+
if ($this->isAbsolutePathCache[$url]) {
63+
return $this->absoluteUrlCache[$cacheKey] = $url;
4164
}
4265

4366
if ($basePath === '/') {
44-
return $basePath . $url;
67+
return $this->absoluteUrlCache[$cacheKey] = $basePath . $url;
4568
}
4669

47-
return '/' . trim($basePath, '/') . '/' . $url;
70+
return $this->absoluteUrlCache[$cacheKey] = '/' . trim($basePath, '/') . '/' . $url;
4871
}
4972

5073
/**
@@ -57,8 +80,13 @@ public function absoluteUrl(string $basePath, string $url): string
5780
*/
5881
public function canonicalUrl(string $basePath, string $url): string
5982
{
83+
$cacheKey = $basePath . '|' . $url;
84+
if (isset($this->canonicalUrlCache[$cacheKey])) {
85+
return $this->canonicalUrlCache[$cacheKey];
86+
}
87+
6088
if ($url[0] === '/') {
61-
return ltrim($url, '/');
89+
return $this->canonicalUrlCache[$cacheKey] = ltrim($url, '/');
6290
}
6391

6492
$dirNameParts = explode('/', $basePath);
@@ -78,6 +106,6 @@ public function canonicalUrl(string $basePath, string $url): string
78106
$urlPass1[] = $part;
79107
}
80108

81-
return ltrim(implode('/', $dirNameParts) . '/' . implode('/', $urlPass1), '/');
109+
return $this->canonicalUrlCache[$cacheKey] = ltrim(implode('/', $dirNameParts) . '/' . implode('/', $urlPass1), '/');
82110
}
83111
}

packages/guides/src/ReferenceResolvers/ExternalReferenceResolver.php

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
use phpDocumentor\Guides\Nodes\Inline\LinkInlineNode;
1717
use phpDocumentor\Guides\RenderContext;
1818

19+
use function array_fill_keys;
1920
use function filter_var;
2021
use function parse_url;
21-
use function preg_match;
2222
use function str_starts_with;
2323

2424
use const FILTER_VALIDATE_EMAIL;
@@ -35,7 +35,67 @@
3535
final class ExternalReferenceResolver implements ReferenceResolver
3636
{
3737
public final const PRIORITY = -100;
38-
final public const SUPPORTED_SCHEMAS = '(?:aaa|aaas|about|acap|acct|acd|acr|adiumxtra|adt|afp|afs|aim|amss|android|appdata|apt|ar|ark|at|attachment|aw|barion|bb|beshare|bitcoin|bitcoincash|blob|bolo|browserext|cabal|calculator|callto|cap|cast|casts|chrome|chrome-extension|cid|coap|coap+tcp|coap+ws|coaps|coaps+tcp|coaps+ws|com-eventbrite-attendee|content|content-type|crid|cstr|cvs|dab|dat|data|dav|dhttp|diaspora|dict|did|dis|dlna-playcontainer|dlna-playsingle|dns|dntp|doi|dpp|drm|drop|dtmi|dtn|dvb|dvx|dweb|ed2k|eid|elsi|embedded|ens|ethereum|example|facetime|fax|feed|feedready|fido|file|filesystem|finger|first-run-pen-experience|fish|fm|ftp|fuchsia-pkg|geo|gg|git|gitoid|gizmoproject|go|gopher|graph|grd|gtalk|h323|ham|hcap|hcp|http|https|hxxp|hxxps|hydrazone|hyper|iax|icap|icon|im|imap|info|iotdisco|ipfs|ipn|ipns|ipp|ipps|irc|irc6|ircs|iris|iris\.beep|iris\.lwz|iris\.xpc|iris\.xpcs|isostore|itms|jabber|jar|jms|keyparc|lastfm|lbry|ldap|ldaps|leaptofrogans|lorawan|lpa|lvlt|magnet|mailserver|mailto|maps|market|matrix|message|microsoft\.windows\.camera|microsoft\.windows\.camera\.multipicker|microsoft\.windows\.camera\.picker|mid|mms|modem|mongodb|moz|ms-access|ms-appinstaller|ms-browser-extension|ms-calculator|ms-drive-to|ms-enrollment|ms-excel|ms-eyecontrolspeech|ms-gamebarservices|ms-gamingoverlay|ms-getoffice|ms-help|ms-infopath|ms-inputapp|ms-launchremotedesktop|ms-lockscreencomponent-config|ms-media-stream-id|ms-meetnow|ms-mixedrealitycapture|ms-mobileplans|ms-newsandinterests|ms-officeapp|ms-people|ms-project|ms-powerpoint|ms-publisher|ms-remotedesktop|ms-remotedesktop-launch|ms-restoretabcompanion|ms-screenclip|ms-screensketch|ms-search|ms-search-repair|ms-secondary-screen-controller|ms-secondary-screen-setup|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-connectabledevices|ms-settings-displays-topology|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|ms-spd|ms-stickers|ms-sttoverlay|ms-transit-to|ms-useractivityset|ms-virtualtouchpad|ms-visio|ms-walk-to|ms-whiteboard|ms-whiteboard-cmd|ms-word|msnim|msrp|msrps|mss|mt|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|num|ocf|oid|onenote|onenote-cmd|opaquelocktoken|openpgp4fpr|otpauth|p1|pack|palm|paparazzi|payment|payto|pkcs11|platform|pop|pres|prospero|proxy|pwid|psyc|pttp|qb|query|quic-transport|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|sarif|secondlife|secret-token|service|session|sftp|sgn|shc|shttp (OBSOLETE)|sieve|simpleledger|simplex|sip|sips|skype|smb|smp|sms|smtp|snews|snmp|soap\.beep|soap\.beeps|soldat|spiffe|spotify|ssb|ssh|starknet|steam|stun|stuns|submit|svn|swh|swid|swidpath|tag|taler|teamspeak|tel|teliaeid|telnet|tftp|things|thismessage|tip|tn3270|tool|turn|turns|tv|udp|unreal|upt|urn|ut2004|uuid-in-package|v-event|vemmi|ventrilo|ves|videotex|vnc|view-source|vscode|vscode-insiders|vsls|w3|wais|web3|wcr|webcal|web+ap|wifi|wpid|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc\.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s)';
38+
39+
/**
40+
* Regex alternation pattern of supported URI schemes.
41+
*
42+
* @deprecated Use isSupportedScheme() for O(1) lookup instead of regex matching.
43+
* @see https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
44+
*/
45+
final public const SUPPORTED_SCHEMAS = '(?:aaa|aaas|about|acap|acct|acd|acr|adiumxtra|adt|afp|afs|aim|amss|android|appdata|apt|ar|ark|at|attachment|aw|barion|bb|beshare|bitcoin|bitcoincash|blob|bolo|browserext|cabal|calculator|callto|cap|cast|casts|chrome|chrome-extension|cid|coap|coap\+tcp|coap\+ws|coaps|coaps\+tcp|coaps\+ws|com-eventbrite-attendee|content|content-type|crid|cstr|cvs|dab|dat|data|dav|dhttp|diaspora|dict|did|dis|dlna-playcontainer|dlna-playsingle|dns|dntp|doi|dpp|drm|drop|dtmi|dtn|dvb|dvx|dweb|ed2k|eid|elsi|embedded|ens|ethereum|example|facetime|fax|feed|feedready|fido|file|filesystem|finger|first-run-pen-experience|fish|fm|ftp|fuchsia-pkg|geo|gg|git|gitoid|gizmoproject|go|gopher|graph|grd|gtalk|h323|ham|hcap|hcp|http|https|hxxp|hxxps|hydrazone|hyper|iax|icap|icon|im|imap|info|iotdisco|ipfs|ipn|ipns|ipp|ipps|irc|irc6|ircs|iris|iris\.beep|iris\.lwz|iris\.xpc|iris\.xpcs|isostore|itms|jabber|jar|jms|keyparc|lastfm|lbry|ldap|ldaps|leaptofrogans|lorawan|lpa|lvlt|magnet|mailserver|mailto|maps|market|matrix|message|microsoft\.windows\.camera|microsoft\.windows\.camera\.multipicker|microsoft\.windows\.camera\.picker|mid|mms|modem|mongodb|moz|ms-access|ms-appinstaller|ms-browser-extension|ms-calculator|ms-drive-to|ms-enrollment|ms-excel|ms-eyecontrolspeech|ms-gamebarservices|ms-gamingoverlay|ms-getoffice|ms-help|ms-infopath|ms-inputapp|ms-launchremotedesktop|ms-lockscreencomponent-config|ms-media-stream-id|ms-meetnow|ms-mixedrealitycapture|ms-mobileplans|ms-newsandinterests|ms-officeapp|ms-people|ms-project|ms-powerpoint|ms-publisher|ms-remotedesktop|ms-remotedesktop-launch|ms-restoretabcompanion|ms-screenclip|ms-screensketch|ms-search|ms-search-repair|ms-secondary-screen-controller|ms-secondary-screen-setup|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-connectabledevices|ms-settings-displays-topology|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|ms-spd|ms-stickers|ms-sttoverlay|ms-transit-to|ms-useractivityset|ms-virtualtouchpad|ms-visio|ms-walk-to|ms-whiteboard|ms-whiteboard-cmd|ms-word|msnim|msrp|msrps|mss|mt|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|num|ocf|oid|onenote|onenote-cmd|opaquelocktoken|openpgp4fpr|otpauth|p1|pack|palm|paparazzi|payment|payto|pkcs11|platform|pop|pres|prospero|proxy|pwid|psyc|pttp|qb|query|quic-transport|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|sarif|secondlife|secret-token|service|session|sftp|sgn|shc|shttp \(OBSOLETE\)|sieve|simpleledger|simplex|sip|sips|skype|smb|smp|sms|smtp|snews|snmp|soap\.beep|soap\.beeps|soldat|spiffe|spotify|ssb|ssh|starknet|steam|stun|stuns|submit|svn|swh|swid|swidpath|tag|taler|teamspeak|tel|teliaeid|telnet|tftp|things|thismessage|tip|tn3270|tool|turn|turns|tv|udp|unreal|upt|urn|ut2004|uuid-in-package|v-event|vemmi|ventrilo|ves|videotex|vnc|view-source|vscode|vscode-insiders|vsls|w3|wais|web3|wcr|webcal|web\+ap|wifi|wpid|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc\.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s)';
46+
47+
/**
48+
* List of supported URI schemes for O(1) lookup.
49+
*
50+
* @var list<string>
51+
*/
52+
private const SUPPORTED_SCHEMAS_LIST = [
53+
'aaa', 'aaas', 'about', 'acap', 'acct', 'acd', 'acr', 'adiumxtra', 'adt', 'afp', 'afs', 'aim', 'amss',
54+
'android', 'appdata', 'apt', 'ar', 'ark', 'at', 'attachment', 'aw', 'barion', 'bb', 'beshare', 'bitcoin',
55+
'bitcoincash', 'blob', 'bolo', 'browserext', 'cabal', 'calculator', 'callto', 'cap', 'cast', 'casts',
56+
'chrome', 'chrome-extension', 'cid', 'coap', 'coap+tcp', 'coap+ws', 'coaps', 'coaps+tcp', 'coaps+ws',
57+
'com-eventbrite-attendee', 'content', 'content-type', 'crid', 'cstr', 'cvs', 'dab', 'dat', 'data', 'dav',
58+
'dhttp', 'diaspora', 'dict', 'did', 'dis', 'dlna-playcontainer', 'dlna-playsingle', 'dns', 'dntp', 'doi',
59+
'dpp', 'drm', 'drop', 'dtmi', 'dtn', 'dvb', 'dvx', 'dweb', 'ed2k', 'eid', 'elsi', 'embedded', 'ens',
60+
'ethereum', 'example', 'facetime', 'fax', 'feed', 'feedready', 'fido', 'file', 'filesystem', 'finger',
61+
'first-run-pen-experience', 'fish', 'fm', 'ftp', 'fuchsia-pkg', 'geo', 'gg', 'git', 'gitoid', 'gizmoproject',
62+
'go', 'gopher', 'graph', 'grd', 'gtalk', 'h323', 'ham', 'hcap', 'hcp', 'http', 'https', 'hxxp', 'hxxps',
63+
'hydrazone', 'hyper', 'iax', 'icap', 'icon', 'im', 'imap', 'info', 'iotdisco', 'ipfs', 'ipn', 'ipns',
64+
'ipp', 'ipps', 'irc', 'irc6', 'ircs', 'iris', 'iris.beep', 'iris.lwz', 'iris.xpc', 'iris.xpcs', 'isostore',
65+
'itms', 'jabber', 'jar', 'jms', 'keyparc', 'lastfm', 'lbry', 'ldap', 'ldaps', 'leaptofrogans', 'lorawan',
66+
'lpa', 'lvlt', 'magnet', 'mailserver', 'mailto', 'maps', 'market', 'matrix', 'message',
67+
'microsoft.windows.camera', 'microsoft.windows.camera.multipicker', 'microsoft.windows.camera.picker',
68+
'mid', 'mms', 'modem', 'mongodb', 'moz', 'ms-access', 'ms-appinstaller', 'ms-browser-extension',
69+
'ms-calculator', 'ms-drive-to', 'ms-enrollment', 'ms-excel', 'ms-eyecontrolspeech', 'ms-gamebarservices',
70+
'ms-gamingoverlay', 'ms-getoffice', 'ms-help', 'ms-infopath', 'ms-inputapp', 'ms-launchremotedesktop',
71+
'ms-lockscreencomponent-config', 'ms-media-stream-id', 'ms-meetnow', 'ms-mixedrealitycapture',
72+
'ms-mobileplans', 'ms-newsandinterests', 'ms-officeapp', 'ms-people', 'ms-project', 'ms-powerpoint',
73+
'ms-publisher', 'ms-remotedesktop', 'ms-remotedesktop-launch', 'ms-restoretabcompanion', 'ms-screenclip',
74+
'ms-screensketch', 'ms-search', 'ms-search-repair', 'ms-secondary-screen-controller',
75+
'ms-secondary-screen-setup', 'ms-settings', 'ms-settings-airplanemode', 'ms-settings-bluetooth',
76+
'ms-settings-camera', 'ms-settings-cellular', 'ms-settings-cloudstorage', 'ms-settings-connectabledevices',
77+
'ms-settings-displays-topology', 'ms-settings-emailandaccounts', 'ms-settings-language',
78+
'ms-settings-location', 'ms-settings-lock', 'ms-settings-nfctransactions', 'ms-settings-notifications',
79+
'ms-settings-power', 'ms-settings-privacy', 'ms-settings-proximity', 'ms-settings-screenrotation',
80+
'ms-settings-wifi', 'ms-settings-workplace', 'ms-spd', 'ms-stickers', 'ms-sttoverlay', 'ms-transit-to',
81+
'ms-useractivityset', 'ms-virtualtouchpad', 'ms-visio', 'ms-walk-to', 'ms-whiteboard', 'ms-whiteboard-cmd',
82+
'ms-word', 'msnim', 'msrp', 'msrps', 'mss', 'mt', 'mtqp', 'mumble', 'mupdate', 'mvn', 'news', 'nfs', 'ni',
83+
'nih', 'nntp', 'notes', 'num', 'ocf', 'oid', 'onenote', 'onenote-cmd', 'opaquelocktoken', 'openpgp4fpr',
84+
'otpauth', 'p1', 'pack', 'palm', 'paparazzi', 'payment', 'payto', 'pkcs11', 'platform', 'pop', 'pres',
85+
'prospero', 'proxy', 'pwid', 'psyc', 'pttp', 'qb', 'query', 'quic-transport', 'redis', 'rediss', 'reload',
86+
'res', 'resource', 'rmi', 'rsync', 'rtmfp', 'rtmp', 'rtsp', 'rtsps', 'rtspu', 'sarif', 'secondlife',
87+
'secret-token', 'service', 'session', 'sftp', 'sgn', 'shc', 'shttp (OBSOLETE)', 'sieve', 'simpleledger',
88+
'simplex', 'sip', 'sips', 'skype', 'smb', 'smp', 'sms', 'smtp', 'snews', 'snmp', 'soap.beep', 'soap.beeps',
89+
'soldat', 'spiffe', 'spotify', 'ssb', 'ssh', 'starknet', 'steam', 'stun', 'stuns', 'submit', 'svn', 'swh',
90+
'swid', 'swidpath', 'tag', 'taler', 'teamspeak', 'tel', 'teliaeid', 'telnet', 'tftp', 'things', 'thismessage',
91+
'tip', 'tn3270', 'tool', 'turn', 'turns', 'tv', 'udp', 'unreal', 'upt', 'urn', 'ut2004', 'uuid-in-package',
92+
'v-event', 'vemmi', 'ventrilo', 'ves', 'videotex', 'vnc', 'view-source', 'vscode', 'vscode-insiders', 'vsls',
93+
'w3', 'wais', 'web3', 'wcr', 'webcal', 'web+ap', 'wifi', 'wpid', 'ws', 'wss', 'wtai', 'wyciwyg', 'xcon',
94+
'xcon-userid', 'xfire', 'xmlrpc.beep', 'xmlrpc.beeps', 'xmpp', 'xri', 'ymsgr', 'z39.50', 'z39.50r', 'z39.50s',
95+
];
96+
97+
/** @var array<string, true> Hash set for O(1) schema lookup */
98+
private static array|null $schemaHashSet = null;
3999

40100
public function resolve(LinkInlineNode $node, RenderContext $renderContext, Messages $messages): bool
41101
{
@@ -51,8 +111,8 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess
51111
return true;
52112
}
53113

54-
$url = parse_url($node->getTargetReference(), PHP_URL_SCHEME);
55-
if ($url !== null && $url !== false && preg_match('/^' . self::SUPPORTED_SCHEMAS . '$/', $url)) {
114+
$scheme = parse_url($node->getTargetReference(), PHP_URL_SCHEME);
115+
if ($scheme !== null && $scheme !== false && self::isSupportedScheme($scheme)) {
56116
$node->setUrl($node->getTargetReference());
57117

58118
return true;
@@ -61,6 +121,20 @@ public function resolve(LinkInlineNode $node, RenderContext $renderContext, Mess
61121
return false;
62122
}
63123

124+
/**
125+
* Check if a URI scheme is supported using O(1) hash set lookup.
126+
*
127+
* This is ~6x faster than regex matching against the 371 IANA schemes.
128+
*/
129+
private static function isSupportedScheme(string $scheme): bool
130+
{
131+
if (self::$schemaHashSet === null) {
132+
self::$schemaHashSet = array_fill_keys(self::SUPPORTED_SCHEMAS_LIST, true);
133+
}
134+
135+
return isset(self::$schemaHashSet[$scheme]);
136+
}
137+
64138
public static function getPriority(): int
65139
{
66140
return self::PRIORITY;

packages/guides/src/ReferenceResolvers/SluggerAnchorNormalizer.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,28 @@
1919

2020
final class SluggerAnchorNormalizer implements AnchorNormalizer
2121
{
22+
private AsciiSlugger|null $slugger = null;
23+
24+
/** @var array<string, string> */
25+
private array $cache = [];
26+
2227
public function reduceAnchor(string $rawAnchor): string
2328
{
24-
$slugger = new AsciiSlugger();
25-
$slug = $slugger->slug($rawAnchor);
29+
// Check cache first - same anchors are resolved many times
30+
if (isset($this->cache[$rawAnchor])) {
31+
return $this->cache[$rawAnchor];
32+
}
33+
34+
if ($this->slugger === null) {
35+
$this->slugger = new AsciiSlugger();
36+
}
37+
38+
$slug = $this->slugger->slug($rawAnchor);
39+
$result = strtolower($slug->toString());
40+
41+
// Cache the result for future calls
42+
$this->cache[$rawAnchor] = $result;
2643

27-
return strtolower($slug->toString());
44+
return $result;
2845
}
2946
}

0 commit comments

Comments
 (0)