Skip to content

Add an opt-in cache for the converted elements - #256

Open
fastfrwrd wants to merge 4 commits into
gilbarbara:mainfrom
fastfrwrd:feat/cache-elements
Open

fastfrwrd wants to merge 4 commits into
gilbarbara:mainfrom
fastfrwrd:feat/cache-elements

Conversation

@fastfrwrd

@fastfrwrd fastfrwrd commented Sep 14, 2026 •

Copy link
Copy Markdown

Closes #255. Revisits the second point of #167.

What this does

cacheRequests caches the SVG text, but the conversion of that text is not cached: useInlineSVG runs getNode() (a DOMParser parse) and a DOM → React conversion on every mount, including on a cache hit — the lazy reducer initializer re-parses cached content. The cost therefore scales with the number of mounts rather than with the number of distinct SVGs, which hurts surfaces that mount the same few SVGs constantly by design: virtualized tables, log lines, trees.

This adds a cacheElements prop, off by default, that keeps the converted element in a module-level cache and reuses it.

<SVG cacheElements src="https://cdn.svglogos.dev/logos/react.svg" />

How the key works

Entries are keyed by the content and by everything else that shapes the output, so nothing changes about what gets rendered:

Input In the key
content the preProcessor result, so two different pre-processors of the same src stay separate
title undefined (keep an existing <title>) is kept distinct from null (remove it) and from any string
description yes
uniquifyIDs when it is on, the hash and baseURL each go in as their own part, so no value of one can look like the other; instances with random hashes never share an entry and instances sharing a uniqueHash do

The cache is an LRU capped at 100 entries (ELEMENT_CACHE_MAX_SIZE), and clearElementCache() is exported for apps that want to empty it. With the prop unset there are no reads, no writes and no growth.

convertToElement() and its key builder now hold the parse + convert step that useInlineSVG used in two places (the cached-content initializer and getElement), so both paths share the caching and the preProcessor is applied exactly once per conversion.

Tests

test/cacheElements.spec.tsx and test/modules/elementCache.spec.ts: 28 tests covering the default-off behaviour (including that the cache stays empty), one conversion for N mounts, per-instance props still applied to the reused element, warm cacheStore content, failures not being cached, and one test per keying rule — each of those fails with a wrong title/description/fill/id in the DOM, not just a wrong call count.

Mutation-tested with StrykerJS on the new code: 95.5% on elementCache.ts and 87.5% on convertToElement. The four survivors are equivalent mutants (the marker value used for an undefined title, the empty-string fallback for an absent baseURL, a cache read that can never hit when caching is off, and a store.delete(undefined) no-op the types require).

pnpm run validate passes: lint, typecheck, 122 tests with coverage thresholds met, build, size (7.83 kB CJS / 7.27 kB ESM against the 10 kB limit) and attw.

Verified in a real app

Wired into Grafana with the local build (a portal: resolution) and one prop added to its Icon component, then measured in a table panel with 300 rows and filterable columns:

Run Icon mounts SVG parses
without cacheElements 86 86 (same on every repeat)
with cacheElements, cold cache 86 8 — one per distinct icon
with cacheElements, warm cache 86 0

The interaction script was identical in each case: 4 nav open/close cycles, 15 sort toggles across 5 columns, 8 column-filter popups, and a full scroll of 300 rows in both directions. Parses were counted by wrapping DOMParser.prototype.parseFromString and icon mounts by a MutationObserver. Icons rendered correctly throughout — per-instance sizes (16/18/24 px) still applied to the reused elements, no empty SVGs, no console errors.

🤖 Generated with Claude Code

`cacheRequests` caches the SVG text, but the content is still parsed into a
DOM node and converted into a React element on every mount, so the cost
scales with the number of mounts rather than with the number of distinct
SVGs. Apps that mount the same handful of SVGs over and over — icons in a
virtualized list or table — pay it repeatedly.

The new `cacheElements` prop, off by default, keeps the converted element
and reuses it. Entries are keyed by the content and by every prop that
shapes the output (`title`, `description`, the `preProcessor` result and,
for `uniquifyIDs`, the hash and baseURL), so nothing changes about what is
rendered. The cache holds the 100 most recently used elements and
`clearElementCache()` empties it.

Refs gilbarbara#167

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@codesandbox

codesandbox Bot commented Sep 14, 2026

Copy link
Copy Markdown

Review or Edit in CodeSandbox

Open the branch in Web Editor • VS Code • Insiders

Open Preview

Copilot AI lite review requested due to automatic review settings September 14, 2026 15:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cache-key collisions can cause incorrect SVG markup to be reused.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds an opt-in LRU cache for converted SVG React elements.

Changes:

  • Adds cacheElements and clearElementCache().
  • Centralizes conversion and cache-key generation.
  • Adds integration, LRU, and documentation coverage.
File summaries
File Summary
test/modules/elementCache.spec.ts Tests cache operations and LRU eviction.
test/cacheElements.spec.tsx Tests integration behavior and keying rules.
src/types.ts Defines the cacheElements prop.
src/modules/utils.ts Centralizes conversion and cache-key generation. Critical: unescaped `
src/modules/useInlineSVG.ts Integrates cached conversion into the hook.
src/modules/elementCache.ts Implements the LRU element cache.
src/index.tsx Exports cache clearing and filters the internal prop.
src/config.ts Defines the cache size limit.
README.md Documents the new cache option.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/modules/utils.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment thread README.md

Entries are keyed by the content and by every prop that shapes the output, so `title`, `description`, `preProcessor` and `uniquifyIDs` keep working as they do without it. Instances using `uniquifyIDs` only share an entry when they also share a `uniqueHash`, since the hash ends up in the markup.

The cache holds the 100 most recently used elements. You can empty it yourself:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this number should be configurable, lmk if you want that and we could add configureElementCache or something?

fastfrwrd and others added 2 commits September 14, 2026 11:57
The two are now separate parts of the cache key, so add the case that the
joined version got wrong: a hash containing the separator and a baseURL
that moves it, which render different ids and url() references but shared
one entry before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The key builder took an anonymous array and left the shaping rules at the
call site, which put SVG-specific knowledge — the marker for an undefined
title, the uniquifyIDs gating — inside a module that otherwise only deals
in strings.

It now lives next to convertToElement with its inputs declared as
parameters, and owns those rules. elementCache is back to being a plain
keyed store, so its spec drops the two tests that only described the old
array signature; the keying rules are covered where they are observable,
in cacheElements.spec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cache the converted element, not just the fetched text (picking #167 back up)

2 participants