Skip to content

Commit a2a4010

Browse files
chaxusclaude
andcommitted
fix(v9): cache the indexed font catalog and x2t wasm -- CJK decks no longer sit on "Loading presentation" for minutes on production
Root cause of "opens instantly on localhost, loads forever on edit.chaxus.com": the editor's indexed fonts (/fonts/NNN, no extension, 200 KB - 5 MB each) had no Cache-Control rule, so Cloudflare Pages served them max-age=0 / cf-cache-status DYNAMIC, and in the service worker they missed the .ttf/.woff skip and fell into stale-while-revalidate whose `cache: 'no-cache'` revalidation re-downloaded every one on every open. The SDK loads document fonts through a serial queue; a real Chinese deck pulled 32 files / 37.6 MB (20 of them the Word/Slide "default fonts" Arial/Times/Courier x 4 faces the document never uses), and measured per-file times climbed 3s -> 89s -> 165s -> 214s+ while isDocumentLoadComplete stayed false. Fix: _headers pins /fonts/* and x2t.wasm.gz as immutable (they only change with a vendor swap, which also changes the referencing HTML; editor JS/HTML is deliberately NOT pinned since some of it carries our runtime patches), and the SW serves those two shapes cache-first instead of revalidating. Same 3-layer lesson as the campaign harness: production adds the CDN + SW layers that localhost never exercises. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b47d6b5 commit a2a4010

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

‎public/_headers‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,21 @@
3535
# week of browser cache with revalidation after is a fine trade.
3636
/ran-fonts/*
3737
Cache-Control: public, max-age=604800, must-revalidate
38+
39+
# The editor's indexed font catalog (public/fonts/000..266, XOR wire format,
40+
# 200 KB - 5 MB each). Named by catalog index and only ever replaced together
41+
# with the vendor build, so they are effectively immutable. Without an
42+
# explicit rule Pages served them max-age=0 / cf-cache-status DYNAMIC: every
43+
# open of a CJK deck re-downloaded 30+ MB of fonts through the SDK's serial
44+
# font queue and sat on "Loading presentation" for minutes on production
45+
# while localhost dev was instant. Long-lived at both the CF edge and the
46+
# browser.
47+
/fonts/*
48+
Cache-Control: public, max-age=31536000, immutable
49+
50+
# The x2t WASM (9.9 MB gz): the single largest first-save download; also
51+
# swapped only with the vendor. Editor JS/HTML under sdkjs/ and web-apps/
52+
# is deliberately NOT pinned here -- some of it carries our runtime patches
53+
# (x2t_helper.js) and the editor iframe HTML, which must revalidate.
54+
/sdkjs/common/wasm/x2t/x2t.wasm.gz
55+
Cache-Control: public, max-age=31536000, immutable

‎public/sw.js‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,33 @@ self.addEventListener('fetch', (event) => {
9797
// causes a crash in OnlyOffice v7.5's fallback font code path.
9898
if (/\.(ttf|woff2?|otf|eot)(\?.*)?$/.test(url.pathname)) return;
9999

100+
// 4a. The editor's indexed font catalog (/fonts/000..266, no extension) and
101+
// the x2t WASM are large, immutable, vendor-versioned binaries. They used to
102+
// fall through to stale-while-revalidate below, whose `cache: 'no-cache'`
103+
// revalidation re-downloaded every multi-MB font on every open; with the
104+
// SDK loading fonts serially, a CJK deck sat on "Loading presentation" for
105+
// minutes on production. Serve them cache-first: the network is consulted
106+
// only when the entry is missing.
107+
const isImmutableBinary = /^\/fonts\/\d{3}$/.test(url.pathname) || url.pathname.endsWith('/x2t.wasm.gz');
108+
if (isImmutableBinary) {
109+
event.respondWith(
110+
caches.match(event.request).then((cached) => {
111+
if (cached) return cached;
112+
return fetch(event.request).then((networkResponse) => {
113+
if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
114+
const responseToCache = networkResponse.clone();
115+
caches.open(RUNTIME_CACHE).then((cache) => {
116+
cache.put(event.request, responseToCache);
117+
limitCacheSize(RUNTIME_CACHE, MAX_RUNTIME_ITEMS);
118+
});
119+
}
120+
return networkResponse;
121+
});
122+
}),
123+
);
124+
return;
125+
}
126+
100127
// 4b. Skip the spellchecker engine: it is importScripts'd from inside a
101128
// dedicated worker during the editor's first boot, and routing that request
102129
// through a just-activated service worker hangs forever on a cold profile

0 commit comments

Comments
 (0)