perf: add cache for images

This commit is contained in:
2026-10-11 04:30:05 +05:00
parent b7613e8c0f
commit 5f4c3833a1
6 changed files with 32 additions and 9 deletions
+8
View File
@@ -168,6 +168,14 @@
форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder форматирование по колонкам??? для hex\bytes\rgba - пример в placeholder
- [ ] прикрутить кастомный color picker, добавить прозрачность (range?) - [ ] прикрутить кастомный color picker, добавить прозрачность (range?)
- [ ] обновить страницу kit или сделать storybook - [ ] обновить страницу kit или сделать storybook
- [ ] **проверить `cachedDataUrl` (WeakMap-кэш превью) в реальной работе** — GC
срабатывает недетерминированно, поэтому освобождение base64-строк (≈1.3×
бинарного размера картинки на каждый тайл) не гарантировано своевременно.
Проверить на длинной сессии с большими картинками (несколько ранов подряд,
смена инструментов): растёт ли heap в DevTools → Memory после повторных
ранов и уходит ли после принудительной сборки. Если записи копятся —
заменить на ограниченный LRU или явную инвалидацию при
`resetForHost`/новом ране (`web/src/lib/core/io.ts`, `cachedDataUrl`)
- [ ] `PreviewTile`: высота плитки задана только `min-height: clamp(...)`, - [ ] `PreviewTile`: высота плитки задана только `min-height: clamp(...)`,
поэтому она не следует за пропорциями результата — широкий результат поэтому она не следует за пропорциями результата — широкий результат
уезжает в узкую полосу, а высокий упирается в `max-height: 100vh`. Нужно, уезжает в узкую полосу, а высокий упирается в `max-height: 100vh`. Нужно,
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { toDataUrl } from "$lib/core/io"; import { cachedDataUrl } from "$lib/core/io";
import type { ToolImageFile } from "$lib/registry"; import type { ToolImageFile } from "$lib/registry";
interface Props { interface Props {
@@ -8,7 +8,7 @@
let { files }: Props = $props(); let { files }: Props = $props();
const parts = $derived( const parts = $derived(
files.map((file) => ({ name: file.name, url: toDataUrl(file.image) })), files.map((file) => ({ name: file.name, url: cachedDataUrl(file.image) })),
); );
</script> </script>
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { toDataUrl } from "$lib/core/io"; import { cachedDataUrl } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types"; import type { PixelImage } from "$lib/core/types";
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import type { FileResult, ResultKind, ResultNote } from "$lib/registry"; import type { FileResult, ResultKind, ResultNote } from "$lib/registry";
@@ -41,7 +41,7 @@
}: Props = $props(); }: Props = $props();
const shown = $derived(maskOn && mask ? mask : result); const shown = $derived(maskOn && mask ? mask : result);
const resultUrl = $derived(shown ? toDataUrl(shown) : null); const resultUrl = $derived(shown ? cachedDataUrl(shown) : null);
const resultDims = $derived( const resultDims = $derived(
resultKind === "image" && result resultKind === "image" && result
? `${result.width} × ${result.height}` ? `${result.width} × ${result.height}`
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { toDataUrl } from "$lib/core/io"; import { cachedDataUrl } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types"; import type { PixelImage } from "$lib/core/types";
import { t } from "$lib/i18n/t"; import { t } from "$lib/i18n/t";
import type { InputMode, ToolImageFile } from "$lib/registry"; import type { InputMode, ToolImageFile } from "$lib/registry";
@@ -33,7 +33,7 @@
onuploadmany, onuploadmany,
}: Props = $props(); }: Props = $props();
const sourceUrl = $derived(source ? toDataUrl(source) : null); const sourceUrl = $derived(source ? cachedDataUrl(source) : null);
const sourceDims = $derived( const sourceDims = $derived(
source source
? `${source.width} × ${source.height}` + ? `${source.width} × ${source.height}` +
@@ -5,7 +5,7 @@
import { Star } from "@lucide/svelte"; import { Star } from "@lucide/svelte";
import { DragDropProvider } from "@dnd-kit/svelte"; import { DragDropProvider } from "@dnd-kit/svelte";
import { debounce } from "$lib/core/debounce"; import { debounce } from "$lib/core/debounce";
import { toDataUrl } from "$lib/core/io"; import { cachedDataUrl } from "$lib/core/io";
import type { PixelImage } from "$lib/core/types"; import type { PixelImage } from "$lib/core/types";
import { import {
chainStepTitle, chainStepTitle,
@@ -301,7 +301,7 @@
{#if inSet.length > 1} {#if inSet.length > 1}
<PartsGrid files={inSet} /> <PartsGrid files={inSet} />
{:else if input} {:else if input}
<img src={toDataUrl(input)} alt="" /> <img src={cachedDataUrl(input)} alt="" />
{:else} {:else}
<span class="empty">{t("resultCard.noResult")}</span> <span class="empty">{t("resultCard.noResult")}</span>
{/if} {/if}
@@ -344,7 +344,7 @@
<PartsGrid files={outSet} /> <PartsGrid files={outSet} />
{:else if out} {:else if out}
<img <img
src={toDataUrl( src={cachedDataUrl(
masks.maskOn[i] && masks.masks[i] masks.maskOn[i] && masks.masks[i]
? (masks.masks[i] as PixelImage) ? (masks.masks[i] as PixelImage)
: out, : out,
+15
View File
@@ -82,6 +82,21 @@ export function toBase64(img: PixelImage): string {
return toDataUrl(img).slice("data:image/png;base64,".length); return toDataUrl(img).slice("data:image/png;base64,".length);
} }
const dataUrlCache = new WeakMap<PixelImage, string>();
// Display helper: PNG-encode on the main thread is expensive and templates
// re-render often with the same image (layout branch swaps, toggles). Results
// are treated as immutable -- a re-run produces new objects -- so an
// identity-keyed cache cannot go stale.
export function cachedDataUrl(img: PixelImage): string {
let url = dataUrlCache.get(img);
if (url === undefined) {
url = toDataUrl(img);
dataUrlCache.set(img, url);
}
return url;
}
export async function decodeTextImage(text: string): Promise<PixelImage> { export async function decodeTextImage(text: string): Promise<PixelImage> {
const cleaned = text.trim().replace(/^data:[^,]*,/, ""); const cleaned = text.trim().replace(/^data:[^,]*,/, "");
if (cleaned.length === 0) { if (cleaned.length === 0) {