Speed up startup: parallel JS loading + versioned asset caching
loader.js:
- Fetch /api/version first; append ?v=<version> to every script URL
- Load scripts with async=false (fetch in parallel, execute in DOM order)
- 4 sequential batches instead of 17 sequential awaits:
utils → settings → [9 feature modules] → init → [4 post-init] → nav
- 17 round-trips → 6 round-trips; 9 and 4 files now download in parallel
- Section HTML keeps ?v=<timestamp> (no-store, always fresh)
server.py:
- Add static_cache_headers middleware:
/static/js/*?v=... → Cache-Control: max-age=31536000, immutable
/static/sections/* → Cache-Control: no-store
/static/* (no v=) → Cache-Control: max-age=3600
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7f35d32f3e
commit
a5efac5da5
16
CHANGELOG.md
16
CHANGELOG.md
@ -7,6 +7,22 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **Parallel JS module loading** — `loader.js` previously loaded all 16
|
||||||
|
modules sequentially (17 round-trips). Restructured into 4 ordered batches
|
||||||
|
with `async=false` so files are fetched in parallel but execute in the
|
||||||
|
correct dependency order:
|
||||||
|
`utils` → `settings` → *(9 feature modules in parallel)* → `init` →
|
||||||
|
*(4 post-init modules in parallel)* → `nav`
|
||||||
|
Round-trips reduced from 17 to 6; 9 files now download simultaneously.
|
||||||
|
- **Version-based JS/CSS cache busting** — `loader.js` fetches
|
||||||
|
`/api/version` first and appends `?v=<version>` to every script URL.
|
||||||
|
A new `Cache-Control: public, max-age=31536000, immutable` middleware in
|
||||||
|
`server.py` lets the browser cache versioned assets for a full year.
|
||||||
|
Bumping the version (via `scripts/release.py`) invalidates the cache.
|
||||||
|
Section HTML keeps `?v=<timestamp>` (no-store) so it is always fresh.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **Config and logs are now bind-mounted local folders** — replaced the
|
- **Config and logs are now bind-mounted local folders** — replaced the
|
||||||
|
|||||||
21
server.py
21
server.py
@ -6,7 +6,8 @@ import os
|
|||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.responses import Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from core.constants import STATIC_DIR, _BufferHandler
|
from core.constants import STATIC_DIR, _BufferHandler
|
||||||
@ -44,6 +45,24 @@ try:
|
|||||||
except Exception as _log_err:
|
except Exception as _log_err:
|
||||||
logger.warning("Could not set up file logging at %s: %s", _LOG_DIR, _log_err)
|
logger.warning("Could not set up file logging at %s: %s", _LOG_DIR, _log_err)
|
||||||
|
|
||||||
|
# ── Static-asset caching middleware ──────────────────────────────────────────
|
||||||
|
# JS and CSS are served with ?v=<version> by loader.js — safe to cache for 1 year.
|
||||||
|
# Section HTML uses ?v=<timestamp> (always fresh) so no long-term cache there.
|
||||||
|
|
||||||
|
@app.middleware("http")
|
||||||
|
async def static_cache_headers(request: Request, call_next):
|
||||||
|
response: Response = await call_next(request)
|
||||||
|
path = request.url.path
|
||||||
|
has_version = bool(request.query_params.get("v"))
|
||||||
|
if has_version and (path.startswith("/static/js/") or path.endswith(".css")):
|
||||||
|
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
|
||||||
|
elif path.startswith("/static/sections/"):
|
||||||
|
response.headers["Cache-Control"] = "no-store"
|
||||||
|
elif path.startswith("/static/") and not has_version:
|
||||||
|
response.headers["Cache-Control"] = "public, max-age=3600"
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
# ── Routers ───────────────────────────────────────────────────────────────────
|
# ── Routers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
app.include_router(admin.router)
|
app.include_router(admin.router)
|
||||||
|
|||||||
@ -1,58 +1,96 @@
|
|||||||
(async function () {
|
(async function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const SECTIONS = ['s-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-performance', 's-routing', 's-connect', 's-settings', 's-llms', 's-conversation'];
|
const SECTIONS = [
|
||||||
|
's-voices', 's-clone', 's-design', 's-studio', 's-tryout',
|
||||||
|
's-performance', 's-routing', 's-connect', 's-settings',
|
||||||
|
's-llms', 's-conversation',
|
||||||
|
];
|
||||||
|
|
||||||
function loadScript(src) {
|
// ── Fetch app version once for JS cache-busting ──────────────────────────
|
||||||
|
// JS files are served with ?v=<version> so the browser can cache them
|
||||||
|
// aggressively. Sections use Date.now() to always get fresh HTML.
|
||||||
|
var _appVersion = 'dev';
|
||||||
|
try {
|
||||||
|
var _vr = await fetch('/api/version');
|
||||||
|
var _vd = await _vr.json();
|
||||||
|
_appVersion = _vd.version || _appVersion;
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// ── 1. Fetch all section HTML partials in parallel ────────────────────────
|
||||||
|
var _ts = Date.now(); // sections always fresh during a session
|
||||||
|
await Promise.all(SECTIONS.map(async function (id) {
|
||||||
|
try {
|
||||||
|
var res = await fetch('/static/sections/' + id + '.html?v=' + _ts);
|
||||||
|
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (el) el.innerHTML = await res.text();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[loader] section', id, 'failed:', e.message);
|
||||||
|
var el = document.getElementById(id);
|
||||||
|
if (el) el.innerHTML =
|
||||||
|
'<p style="color:var(--red);padding:24px">Failed to load section <b>' +
|
||||||
|
id + '</b>: ' + e.message + '</p>';
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ── 2. Load JS in parallel batches ────────────────────────────────────────
|
||||||
|
// Scripts with async=false are FETCHED in parallel but EXECUTED in DOM
|
||||||
|
// insertion order — safe for modules that share a global scope.
|
||||||
|
//
|
||||||
|
// Batch order:
|
||||||
|
// A) utils.js — defines $, toast, escHtml; needed by everything
|
||||||
|
// B) settings.js — declares _appSettings; needed before features read it
|
||||||
|
// C) Feature modules — parallel fetch + ordered execute
|
||||||
|
// D) init.js — calls loadSettings() + initBenchmarkSampleControls()
|
||||||
|
// E) Post-init modules — parallel fetch + ordered execute
|
||||||
|
// F) nav.js — navigation overrides; must be last
|
||||||
|
|
||||||
|
function _load(src) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
var s = document.createElement('script');
|
var s = document.createElement('script');
|
||||||
s.src = src;
|
s.src = src + '?v=' + _appVersion;
|
||||||
|
s.async = false; // fetch in parallel, execute in DOM order
|
||||||
s.onload = resolve;
|
s.onload = resolve;
|
||||||
s.onerror = function () { reject(new Error('Failed to load ' + src)); };
|
s.onerror = function () { reject(new Error('Failed to load ' + src)); };
|
||||||
document.body.appendChild(s);
|
document.body.appendChild(s);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Fetch all section partials in parallel and inject into their shells.
|
function _loadBatch(srcs) {
|
||||||
// ?v= busts the browser cache so updated HTML is always used.
|
// Append all <script async=false> at once → browser fetches all in parallel
|
||||||
var _v = Date.now();
|
// and executes them in the order they were appended.
|
||||||
await Promise.all(SECTIONS.map(async function (id) {
|
return Promise.all(srcs.map(_load));
|
||||||
try {
|
|
||||||
var res = await fetch('/static/sections/' + id + '.html?v=' + _v);
|
|
||||||
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
|
||||||
var html = await res.text();
|
|
||||||
var el = document.getElementById(id);
|
|
||||||
if (el) el.innerHTML = html;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[loader] section', id, 'failed:', e.message);
|
|
||||||
var el = document.getElementById(id);
|
|
||||||
if (el) el.innerHTML = '<p style="color:var(--red);padding:24px">Failed to load section <b>' + id + '</b>: ' + e.message + '</p>';
|
|
||||||
}
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
// 2. Load main application logic (runs init immediately on parse — sections must exist first)
|
// A — foundation (sequential: each must finish before the next batch starts)
|
||||||
const JS_MODULES = [
|
await _load('/static/js/utils.js');
|
||||||
'/static/js/utils.js',
|
await _load('/static/js/settings.js');
|
||||||
|
|
||||||
|
// C — feature modules (9 files fetched in parallel, executed in order)
|
||||||
|
await _loadBatch([
|
||||||
'/static/js/voice-inspector.js',
|
'/static/js/voice-inspector.js',
|
||||||
'/static/js/voice-sources.js',
|
'/static/js/voice-sources.js',
|
||||||
'/static/js/integrations.js',
|
'/static/js/integrations.js',
|
||||||
'/static/js/routing.js',
|
'/static/js/routing.js',
|
||||||
'/static/js/settings.js',
|
|
||||||
'/static/js/voice-clone.js',
|
'/static/js/voice-clone.js',
|
||||||
'/static/js/voice-library.js',
|
'/static/js/voice-library.js',
|
||||||
'/static/js/tts-preview.js',
|
'/static/js/tts-preview.js',
|
||||||
'/static/js/benchmark.js',
|
'/static/js/benchmark.js',
|
||||||
'/static/js/stt.js',
|
'/static/js/stt.js',
|
||||||
'/static/js/init.js',
|
]);
|
||||||
|
|
||||||
|
// D — init (needs everything above to be defined)
|
||||||
|
await _load('/static/js/init.js');
|
||||||
|
|
||||||
|
// E — post-init modules (4 files fetched in parallel, executed in order)
|
||||||
|
await _loadBatch([
|
||||||
'/static/js/engines.js',
|
'/static/js/engines.js',
|
||||||
'/static/js/ai-backends.js',
|
'/static/js/ai-backends.js',
|
||||||
'/static/js/generation.js',
|
'/static/js/generation.js',
|
||||||
'/static/js/conversation.js',
|
'/static/js/conversation.js',
|
||||||
];
|
]);
|
||||||
for (const src of JS_MODULES) {
|
|
||||||
await loadScript(src);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Apply scroll-based navigation overrides (must run after app.js defines switchTab etc.)
|
// F — navigation
|
||||||
await loadScript('/static/nav.js');
|
await _load('/static/nav.js');
|
||||||
})();
|
})();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user