perf: voice list sessionStorage cache — instant render on reload (v1.12.20)
After a successful /api/voices fetch, results are written to sessionStorage (key ttsvc_vc). On the next page load loadVoiceLibrary() reads the cache and renders voices immediately before the network request completes, eliminating the blank-list / skeleton flash entirely. The background fetch always runs and overwrites the cache with fresh data. Error handling is graceful: if the network fails but cache was served, the error is suppressed (stale data stays visible). The Refresh button clears the cache first to force a full reload cycle. Silent-refresh logic (from v1.12.19) prevents the second loadVoiceLibrary() call (engines.js batch E) from blanking already-rendered voices. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ce0f4c43bc
commit
ad155f6c5c
@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
---
|
||||
|
||||
## [1.12.20] — 2026-06-28
|
||||
|
||||
### Performance
|
||||
- **Voice list sessionStorage cache** — voices are written to `sessionStorage` after every successful fetch (`ttsvc_vc`). On the next page load the list renders instantly from cache while the fresh fetch runs silently in the background. The Refresh button clears the cache first to guarantee a clean reload.
|
||||
|
||||
---
|
||||
|
||||
## [1.12.19] — 2026-06-28
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.12.19">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.12.20">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -336,7 +336,7 @@
|
||||
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.12.19"></script>
|
||||
<script src="/static/loader.js?v=1.12.20"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -42,6 +42,18 @@ let _activePlayVoiceId = null;
|
||||
let _activePlayUrl = null;
|
||||
let _libraryLoadPromise = null;
|
||||
const BENCHMARK_SAMPLE_STORAGE_KEY = 'vcf-benchmark-sample-text';
|
||||
const _VL_CACHE_KEY = 'ttsvc_vc';
|
||||
|
||||
function _vlCacheRead() {
|
||||
try { return JSON.parse(sessionStorage.getItem(_VL_CACHE_KEY) || 'null'); } catch (_) { return null; }
|
||||
}
|
||||
function _vlCacheWrite(voices) {
|
||||
try { sessionStorage.setItem(_VL_CACHE_KEY, JSON.stringify(voices)); } catch (_) {}
|
||||
}
|
||||
function _vlCacheClear() {
|
||||
try { sessionStorage.removeItem(_VL_CACHE_KEY); } catch (_) {}
|
||||
}
|
||||
window._vlCacheClear = _vlCacheClear;
|
||||
const _libraryFilters = {text:'', lang:'', sex:'', type:'', rating:''};
|
||||
const DEFAULT_BENCHMARK_SAMPLE_TEXT = 'Hello, how are you today? Please read this sample clearly for a fair voice benchmark.';
|
||||
|
||||
@ -613,25 +625,42 @@ async function loadVoiceLibrary() {
|
||||
_libraryLoadPromise = (async () => {
|
||||
setBusyButton('refresh-voices-btn', true);
|
||||
const list = $('voice-list');
|
||||
const silent = _voices.length > 0; // already rendered — refresh without blanking the list
|
||||
|
||||
// Serve from sessionStorage cache immediately so the list is never blank on reload
|
||||
const cached = _voices.length === 0 ? _vlCacheRead() : null;
|
||||
if (cached && Array.isArray(cached) && cached.length) {
|
||||
_voices = cached;
|
||||
window._voices = _voices;
|
||||
if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices);
|
||||
renderVoiceList();
|
||||
updatePreviewVoiceMatchPanel();
|
||||
status(`Loaded ${_voices.length} voices`);
|
||||
}
|
||||
|
||||
// Always background-fetch fresh data; skip skeleton if something is already shown
|
||||
const silent = _voices.length > 0;
|
||||
if (list && !silent) list.innerHTML = loadingMarkup('Loading voice library', 'Scanning voices, reference text, metadata, ratings, and benchmark results.', 8);
|
||||
if (!silent) { $('voice-count').textContent = 'Loading voices…'; updateLibraryInsights('loading'); status('Loading voice library…'); }
|
||||
try {
|
||||
const r = await fetch('/api/voices');
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
|
||||
_voices = await r.json();
|
||||
window._voices = _voices; // expose for cross-module access (Script Rehearser etc.)
|
||||
const fresh = await r.json();
|
||||
_vlCacheWrite(fresh);
|
||||
_voices = fresh;
|
||||
window._voices = _voices;
|
||||
if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices);
|
||||
renderVoiceList();
|
||||
updatePreviewVoiceMatchPanel();
|
||||
if (typeof renderPerfHistory === 'function') renderPerfHistory();
|
||||
status(`Loaded ${_voices.length} voices`);
|
||||
} catch(e) {
|
||||
if (list) list.innerHTML = '<div style="color:var(--red);padding:8px;font-size:13px">Failed to load voices: ' + (e.message||String(e)) + '</div>';
|
||||
$('voice-count').textContent = 'Load failed';
|
||||
updateLibraryInsights('error');
|
||||
if (!silent) {
|
||||
if (list) list.innerHTML = '<div style="color:var(--red);padding:8px;font-size:13px">Failed to load voices: ' + (e.message||String(e)) + '</div>';
|
||||
$('voice-count').textContent = 'Load failed';
|
||||
updateLibraryInsights('error');
|
||||
}
|
||||
status('Voice library load failed: ' + e.message);
|
||||
throw e;
|
||||
if (!silent) throw e;
|
||||
} finally {
|
||||
setBusyButton('refresh-voices-btn', false);
|
||||
_libraryLoadPromise = null;
|
||||
@ -640,7 +669,7 @@ async function loadVoiceLibrary() {
|
||||
return _libraryLoadPromise;
|
||||
}
|
||||
|
||||
$('refresh-voices-btn').addEventListener('click', loadVoiceLibrary);
|
||||
$('refresh-voices-btn').addEventListener('click', () => { _vlCacheClear(); loadVoiceLibrary(); });
|
||||
$('sync-voice-folders-btn').addEventListener('click', async () => {
|
||||
$('sync-voice-folders-btn').disabled = true;
|
||||
status('Syncing active_voices and hidden_voices…');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user