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
|
## [1.12.19] — 2026-06-28
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
<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) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -336,7 +336,7 @@
|
|||||||
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
||||||
|
|
||||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
<!-- 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -42,6 +42,18 @@ let _activePlayVoiceId = null;
|
|||||||
let _activePlayUrl = null;
|
let _activePlayUrl = null;
|
||||||
let _libraryLoadPromise = null;
|
let _libraryLoadPromise = null;
|
||||||
const BENCHMARK_SAMPLE_STORAGE_KEY = 'vcf-benchmark-sample-text';
|
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 _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.';
|
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 () => {
|
_libraryLoadPromise = (async () => {
|
||||||
setBusyButton('refresh-voices-btn', true);
|
setBusyButton('refresh-voices-btn', true);
|
||||||
const list = $('voice-list');
|
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 (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…'); }
|
if (!silent) { $('voice-count').textContent = 'Loading voices…'; updateLibraryInsights('loading'); status('Loading voice library…'); }
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/voices');
|
const r = await fetch('/api/voices');
|
||||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
|
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
|
||||||
_voices = await r.json();
|
const fresh = await r.json();
|
||||||
window._voices = _voices; // expose for cross-module access (Script Rehearser etc.)
|
_vlCacheWrite(fresh);
|
||||||
|
_voices = fresh;
|
||||||
|
window._voices = _voices;
|
||||||
if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices);
|
if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices);
|
||||||
renderVoiceList();
|
renderVoiceList();
|
||||||
updatePreviewVoiceMatchPanel();
|
updatePreviewVoiceMatchPanel();
|
||||||
if (typeof renderPerfHistory === 'function') renderPerfHistory();
|
if (typeof renderPerfHistory === 'function') renderPerfHistory();
|
||||||
status(`Loaded ${_voices.length} voices`);
|
status(`Loaded ${_voices.length} voices`);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
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>';
|
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';
|
$('voice-count').textContent = 'Load failed';
|
||||||
updateLibraryInsights('error');
|
updateLibraryInsights('error');
|
||||||
|
}
|
||||||
status('Voice library load failed: ' + e.message);
|
status('Voice library load failed: ' + e.message);
|
||||||
throw e;
|
if (!silent) throw e;
|
||||||
} finally {
|
} finally {
|
||||||
setBusyButton('refresh-voices-btn', false);
|
setBusyButton('refresh-voices-btn', false);
|
||||||
_libraryLoadPromise = null;
|
_libraryLoadPromise = null;
|
||||||
@ -640,7 +669,7 @@ async function loadVoiceLibrary() {
|
|||||||
return _libraryLoadPromise;
|
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').addEventListener('click', async () => {
|
||||||
$('sync-voice-folders-btn').disabled = true;
|
$('sync-voice-folders-btn').disabled = true;
|
||||||
status('Syncing active_voices and hidden_voices…');
|
status('Syncing active_voices and hidden_voices…');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user