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]
|
||||
|
||||
### 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
|
||||
|
||||
- **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 pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from core.constants import STATIC_DIR, _BufferHandler
|
||||
@ -44,6 +45,24 @@ try:
|
||||
except Exception as _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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
app.include_router(admin.router)
|
||||
|
||||
@ -1,58 +1,96 @@
|
||||
(async function () {
|
||||
'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) {
|
||||
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.onerror = function () { reject(new Error('Failed to load ' + src)); };
|
||||
document.body.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Fetch all section partials in parallel and inject into their shells.
|
||||
// ?v= busts the browser cache so updated HTML is always used.
|
||||
var _v = Date.now();
|
||||
await Promise.all(SECTIONS.map(async function (id) {
|
||||
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>';
|
||||
function _loadBatch(srcs) {
|
||||
// Append all <script async=false> at once → browser fetches all in parallel
|
||||
// and executes them in the order they were appended.
|
||||
return Promise.all(srcs.map(_load));
|
||||
}
|
||||
}));
|
||||
|
||||
// 2. Load main application logic (runs init immediately on parse — sections must exist first)
|
||||
const JS_MODULES = [
|
||||
'/static/js/utils.js',
|
||||
// A — foundation (sequential: each must finish before the next batch starts)
|
||||
await _load('/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-sources.js',
|
||||
'/static/js/integrations.js',
|
||||
'/static/js/routing.js',
|
||||
'/static/js/settings.js',
|
||||
'/static/js/voice-clone.js',
|
||||
'/static/js/voice-library.js',
|
||||
'/static/js/tts-preview.js',
|
||||
'/static/js/benchmark.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/ai-backends.js',
|
||||
'/static/js/generation.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.)
|
||||
await loadScript('/static/nav.js');
|
||||
// F — navigation
|
||||
await _load('/static/nav.js');
|
||||
})();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user