Skip to content

Commit c49d97f

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 5820131 commit c49d97f

File tree

10 files changed

+562
-33
lines changed

10 files changed

+562
-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
}

0 commit comments

Comments
 (0)