Skip to content

Commit 4d1c708

Browse files
maehrclaude
andauthored
perf(site): compile the registry once per build, not twice (#139)
Astro loads `astro.config.mjs` in a module graph of its own, separate from the one the pages render in. `src/lib/registry.ts` is therefore instantiated twice in a single build, and the module-level memo is not shared between the two copies. An instrumented `astro build` called `compileRegistry` twice. Deferring the first call does not help, because the two graphs never share a module variable whenever it runs. Memoise on `globalThis` instead, under a registered symbol, which is the one scope both graphs agree on. An instrumented build now calls `compileRegistry` once. Compiling the real registry takes about 2.6s and materialises 86397 references, so the second pass cost that time and a second copy in memory on every build and every `dev` start. Lifetime is unchanged: the process, exactly as the module memo was. A `dev` server still needs a restart after the `data/` submodule changes. Closes #131 Claude-Session: https://claude.ai/code/session_011HLhfpsrWxq56jnZr1gD8B Co-authored-by: Moritz Mähr <14755525+maehr@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 257f17e commit 4d1c708

2 files changed

Lines changed: 43 additions & 3 deletions

File tree

‎src/lib/registry.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// The compiled registry is memoised on `globalThis` rather than in a module
2+
// variable, because Astro loads `astro.config.mjs` in a module graph of its own
3+
// and would otherwise compile the whole registry a second time.
4+
process.env.TEXTREFS_REGISTRY_FIXTURE = '1';
5+
6+
import { test } from 'node:test';
7+
import assert from 'node:assert/strict';
8+
import { loadRegistry, loadWorks } from './registry.js';
9+
import { fixtureRegistry } from './registry.fixture.js';
10+
11+
const CACHE_KEY = Symbol.for('textrefs.compiledRegistry');
12+
13+
test('the fixture short-circuit never compiles or caches the real registry', () => {
14+
// The env check has to stay above the cache, or every fixture-backed test
15+
// would pay for a full compile of the real registry and leave it on the
16+
// global for whatever ran next.
17+
assert.equal(loadRegistry(), fixtureRegistry);
18+
assert.equal(
19+
Object.getOwnPropertySymbols(globalThis).includes(CACHE_KEY),
20+
false,
21+
'the fixture path must not populate the global cache',
22+
);
23+
});
24+
25+
test('repeated loads return one registry', () => {
26+
assert.equal(loadRegistry(), loadRegistry());
27+
assert.equal(loadWorks(), loadRegistry().works);
28+
});

‎src/lib/registry.ts‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,23 @@ import {
44
} from '../../scripts/compile.js';
55
import { fixtureRegistry } from './registry.fixture.js';
66

7-
let cached: CompiledRegistry | null = null;
7+
// Astro loads `astro.config.mjs` in a module graph of its own, separate from
8+
// the one the pages render in, so this module is instantiated twice in a single
9+
// build and a module-level memo is not shared between the two. Compiling the
10+
// real registry takes about 2.6s and materialises 86k records, so the second
11+
// pass is worth avoiding. `globalThis` is the one scope both graphs agree on;
12+
// the registered symbol keeps the key from colliding with anything else.
13+
//
14+
// Lifetime is the process, exactly as the module-level memo was, so a `dev`
15+
// server still needs a restart after the `data/` submodule changes.
16+
const CACHE_KEY = Symbol.for('textrefs.compiledRegistry');
17+
type CacheHost = { [CACHE_KEY]?: CompiledRegistry };
18+
819
function registry(): CompiledRegistry {
920
if (process.env.TEXTREFS_REGISTRY_FIXTURE === '1') return fixtureRegistry;
10-
cached ??= compileRegistry();
11-
return cached;
21+
const host = globalThis as CacheHost;
22+
host[CACHE_KEY] ??= compileRegistry();
23+
return host[CACHE_KEY];
1224
}
1325

1426
/**

0 commit comments

Comments
 (0)