diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..39600c7 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2025-02-18 - KMP WasmJs JSON Decoding Overhead +**Learning:** JSON decoding in Kotlin Multiplatform for WasmJs targets carries significant overhead. Repeatedly parsing the `localStorage` manifest JSON string in `LocalStorageThemeCache.kt` on every read/write was causing unnecessary bottlenecks. +**Action:** Use an in-memory cache initialized via `by lazy` to parse the `localStorage` JSON only once upon the first access, and perform subsequent reads/writes directly from/to this in-memory cache to avoid redundant parsing. diff --git a/halogen-engine/src/wasmJsMain/kotlin/halogen/engine/LocalStorageThemeCache.kt b/halogen-engine/src/wasmJsMain/kotlin/halogen/engine/LocalStorageThemeCache.kt index aba1f32..620ef84 100644 --- a/halogen-engine/src/wasmJsMain/kotlin/halogen/engine/LocalStorageThemeCache.kt +++ b/halogen-engine/src/wasmJsMain/kotlin/halogen/engine/LocalStorageThemeCache.kt @@ -51,17 +51,17 @@ public class LocalStorageThemeCache( // ── Manifest helpers ──────────────────────────────────────────────── - private fun readManifest(): MutableSet { - val raw = jsGetItem(manifestKey.toJsString())?.toString() ?: return mutableSetOf() - return try { + private val manifestCache: MutableSet by lazy { + val raw = jsGetItem(manifestKey.toJsString())?.toString() ?: return@lazy mutableSetOf() + try { json.decodeFromString>(raw).toMutableSet() } catch (_: Exception) { mutableSetOf() } } - private fun writeManifest(keys: Set) { - val encoded = json.encodeToString(keys) + private fun writeManifest() { + val encoded = json.encodeToString(manifestCache) jsSetItem(manifestKey.toJsString(), encoded.toJsString()) } @@ -110,9 +110,8 @@ public class LocalStorageThemeCache( sizeBytes = specJson.encodeToByteArray().size, ) writeEntry(storageKey, entry) - val manifest = readManifest() - manifest.add(key) - writeManifest(manifest) + manifestCache.add(key) + writeManifest() _changes.tryEmit(CacheEvent.Inserted(key, source)) } @@ -124,9 +123,8 @@ public class LocalStorageThemeCache( val storageKey = "$prefix$key" val existed = readEntry(storageKey) != null removeEntry(storageKey) - val manifest = readManifest() - manifest.remove(key) - writeManifest(manifest) + manifestCache.remove(key) + writeManifest() if (existed) { _changes.tryEmit(CacheEvent.Evicted(key)) } @@ -134,37 +132,35 @@ public class LocalStorageThemeCache( override suspend fun evict(keys: Set) { val removed = mutableSetOf() - val manifest = readManifest() for (k in keys) { val storageKey = "$prefix$k" if (readEntry(storageKey) != null) { removeEntry(storageKey) - manifest.remove(k) + manifestCache.remove(k) removed.add(k) } } - writeManifest(manifest) + writeManifest() if (removed.isNotEmpty()) { _changes.tryEmit(CacheEvent.EvictedBatch(removed)) } } override suspend fun clear() { - val manifest = readManifest() - for (key in manifest) { + for (key in manifestCache.toList()) { removeEntry("$prefix$key") } + manifestCache.clear() jsRemoveItem(manifestKey.toJsString()) _changes.tryEmit(CacheEvent.Cleared) } - override suspend fun keys(): Set = readManifest() + override suspend fun keys(): Set = manifestCache.toSet() - override suspend fun size(): Int = readManifest().size + override suspend fun size(): Int = manifestCache.size override suspend fun entries(): List { - val manifest = readManifest() - return manifest.mapNotNull { key -> + return manifestCache.toList().mapNotNull { key -> val entry = readEntry("$prefix$key") ?: return@mapNotNull null ThemeCacheEntry( key = key,