Skip to content

perf: staged fitText search, avatar/background cache split, CLI/util concurrency - #75

Merged
otnc merged 4 commits into
mainfrom
perf/staged-fit-and-cache-separation
Aug 29, 2026
Merged

perf: staged fitText search, avatar/background cache split, CLI/util concurrency#75
otnc merged 4 commits into
mainfrom
perf/staged-fit-and-cache-separation

Conversation

@otnc

@otnc otnc commented Aug 29, 2026

Copy link
Copy Markdown
Owner

What does this change?

Four independent performance fixes found during a codebase review, each its own commit:

  1. fitText: skip the full tokenize+wrap for font sizes a cheap width lower bound already rules out (never changes the result — see the code comment for why it's safe alongside the existing non-monotonic-search behavior). Matters most for long CJK text at a large setScale().
  2. backgroundImage: give it its own cache (backgroundImageCache) instead of sharing avatarCache — a burst of distinct avatars could previously evict a fixed background image, forcing a re-fetch.
  3. CLI outdated/update: run the independent package/Twemoji/font checks concurrently instead of one round-trip at a time. update's actual mutations and their printed output stay sequential and in the same order.
  4. findProjectRoot: memoize by startDir — every font/Twemoji cache-dir resolution in a run was re-walking the same ancestor chain.

Checklist

  • npm run ci passes (Biome lint and format)
  • npm run typecheck passes
  • npm run test passes
  • npm run build && npm run check:build passes
  • Tests cover the change
  • README updated, if the public API changed

otnc added 4 commits August 29, 2026 20:37
No wrapping algorithm can fit more content per line than maxWidth, so a font
size whose segments' total width alone already implies more lines than the
budget allows is guaranteed not to fit — that can be checked with a single
whole-segment measurement, without tokenizing and wrapping the text.

This is a pure short-circuit: it never changes which font size the search
settles on, only skips full wraps that were always going to fail, so it's
safe alongside fitText's existing non-monotonic-search caveat (a smaller
size can occasionally need more lines than a larger one, from kinsoku).

Matters most for long CJK text at a large setScale(): every character
boundary is a fallback break candidate there, so a full wrap tokenizes and
measures roughly one segment per character at every candidate size.
…vatarCache

loadBackgroundImage delegated straight to loadAvatar, which always went
through the shared avatarCache singleton — the exact coupling avatarCache's
own doc comment says to avoid ("a burst of avatar fetches evicting emoji ...
would be a strange coupling between two unrelated kinds of asset"), just
between avatars and background images instead of avatars and emoji.

With a 64-entry, 5-minute-TTL avatar cache, a burst of distinct avatars could
evict a fixed background image and force a re-fetch on the next render.
loadAvatar now takes an injectable cache (defaulting to avatarCache), and
loadBackgroundImage passes a new, separate backgroundImageCache sized for a
small set of reused assets (16 entries, 30-minute TTL) instead.

Exposes configureBackgroundImageCache/clearBackgroundImageCache/
backgroundImageCacheInfo from the package root, matching the equivalent
avatarCache trio already there.
outdatedCommand and updateCommand awaited checkPackageUpdate, twemojiInfo(+
latestTwemojiVersion) and listInstalledFonts(+checkFontUpdates) one after
another, even though the three checks don't depend on each other — only
twemojiLatest depends on twemoji's own result. Each is a network or disk
round-trip, so the command's total latency was the sum of all three instead
of the slowest one.

updateCommand keeps its actual updates (and their printed output) sequential
and in the same package → Twemoji → fonts order as before; only the
read-only status gathering above them now runs concurrently.
resolveCacheDir() calls findProjectRoot() with no override on every font
and Twemoji cache-dir resolution that isn't already short-circuited by
MIQ_FONT_CACHE_DIR, each one synchronously walking up from cwd checking for
package.json. A single run resolves the same startDir every time and a
project's own root doesn't move while the process is alive, so the walk
only needs to happen once.
@otnc
otnc merged commit c828234 into main Aug 29, 2026
7 checks passed
@otnc
otnc deleted the perf/staged-fit-and-cache-separation branch August 29, 2026 12:15
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.

1 participant