Front-load worker fonts so a cold page render isn't cut short by the draw timeout

After clearing the page-texture cache, the worker's first drawSpread had to load the EB
Garamond faces AND rasterize inside a single 4s draw-timeout budget. On a cold load that could
exceed 4s, so the timeout fired, the draw resolved to null (no title painted), the loader then
completed over a black scene, and the title only appeared on a later render ("the image
returned outside the loader's progress indicators").

The renderer now awaits the worker's fonts-ready signal before its first timed draw (with a
15s safety cap so it can't hang), so font loading happens during the loader as its own step
rather than inside a draw's timeout window. Draw timeout raised 4s -> 6s for cold-render
headroom. Verified live: title page renders within the loader, no texture-worker-timeout
problems. Suite 178.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 07:00:22 +02:00
parent 705d1ea6bf
commit 91b5999cd2
2 changed files with 29 additions and 9 deletions
+25 -8
View File
@@ -115,6 +115,8 @@ class BookTextureRendererModule extends BaseModule {
this.addEventListener(document, 'story:history-restoring', this.stopAnimations);
this.addEventListener(document, 'story:client-reset', this.stopAnimations);
this.currentSpread = this.pagination?.getCurrentSpread?.() || { index: 0, left: [], right: [], pageMeta: { left: null, right: null } };
this.reportProgress(60, 'Loading page fonts in render worker');
await this.waitForWorkerFonts();
await this.drawSpread(this.currentSpread);
this.reportProgress(100, 'Book texture renderer ready');
return true;
@@ -125,10 +127,15 @@ class BookTextureRendererModule extends BaseModule {
this.rasterWorker = new Worker(`/js/book-texture-worker.js${version}`);
this.pendingRasterizations = new Map();
this.rasterRequestId = 0;
this.rasterTimeoutMs = 4000;
this.rasterTimeoutMs = 6000;
this.rasterChain = Promise.resolve();
this.fontsReadyPromise = new Promise((resolve) => { this.resolveFontsReady = resolve; });
this.rasterWorker.onmessage = (event) => {
const data = event.data || {};
if (data.type === 'fonts-ready') {
this.resolveFontsReady?.();
return;
}
if (data.type !== 'drawn') return;
this.settleRasterization(data.requestId, data.results);
};
@@ -144,6 +151,17 @@ class BookTextureRendererModule extends BaseModule {
this.rasterWorker.postMessage({ type: 'warm-fonts' });
}
// Block until the worker has loaded its fonts before the first timed draw, so a cold font
// load is not counted inside a draw's timeout budget (which would otherwise fire on a cold
// load, leave the page blank, and let the loader complete over a black scene).
async waitForWorkerFonts() {
if (!this.fontsReadyPromise) return;
await Promise.race([
this.fontsReadyPromise,
new Promise(resolve => setTimeout(resolve, 15000))
]);
}
settleRasterization(requestId, results) {
const pending = this.pendingRasterizations.get(requestId);
if (!pending) return;
@@ -867,13 +885,12 @@ class BookTextureRendererModule extends BaseModule {
changed = true;
}
});
if (changed) {
document.dispatchEvent(new CustomEvent('webgl-book:page-reveal-fast-forward', {
detail: {
blockIds
}
}));
}
document.dispatchEvent(new CustomEvent('webgl-book:page-reveal-fast-forward', {
detail: {
blockIds,
broad: !changed
}
}));
}
completeRevealBlockIds(blockIds = []) {