Skip to content

0118: Load Codec roster portraits by pack

Status: accepted (2026-08-24) · Scope: Codec roster, character pack assets, local dashboard transport

The Codec roster shows 21 expressions for every installed character pack. A 48-pack roster therefore contains 1,008 portrait cells. The installed source files measured 35.4 MiB across 714 WebP files on 2026-08-24. Extended expressions that use a base-expression fallback still had distinct request URLs, so the browser could issue 1,008 requests even though some responses had the same bytes.

The image route was not the slow part. One isolated pack served 21 individual portraits concurrently in 0.227 seconds on a cold pass and 0.116 seconds on a warm pass. The delay came from allowing hundreds of image URLs into the browser queue while scrolling through the roster.

The dashboard runs Next directly at http://localhost through next dev or next start. The installed Next 16 server constructs node:http for the normal origin and node:https for its experimental HTTPS mode. The current origin therefore negotiates HTTP/1.1. Browser-facing HTTP/2 requires TLS, and HTTP/3 requires TLS plus QUIC.

Keep native loading="lazy" on every portrait. Rejected because the browser decides how far ahead to fetch. A long or fast scroll can still add hundreds of requests to the same HTTP/1.1 connection pool.

Build one sprite for every pack. Rejected. A sprite cuts the request count, but it replaces the pack’s individual portrait assets with a second generated representation and complicates cropping and cache invalidation.

Virtualize the roster. Rejected because native browser Find cannot inspect rows removed from the DOM. Harnery does not have a complete in-app Find replacement for this page.

Move the dashboard to HTTP/2 or HTTP/3. Deferred as a separate transport feature. Next’s integrated server has no HTTP/2 or HTTP/3 mode. A custom Next server would give up framework optimizations and take ownership of upgrades and development behavior. A reverse proxy such as Caddy can expose HTTP/2 and HTTP/3, but it needs a trusted local HTTPS certificate, another process, a different public origin, and UDP access for HTTP/3. That work affects links, the isolated files origin, Server-Sent Events, tunnels, and certificate setup. It does not remove the 35.4 MiB first-view payload.

CodecRosterExpressions is a small client leaf inside the server-rendered roster. It renders every pack, expression figure, and caption immediately, so browser Find still sees the complete roster. The first pack includes its 21 portrait URLs in the server response. Later packs render fixed-size placeholders until an IntersectionObserver sees the expression grid within a 1,200-pixel vertical margin.

When a deferred pack enters that margin, the component assigns its individual versioned URLs. It keeps observing the pack and removes those <img> elements after the pack leaves the margin. The figures and captions stay in the DOM. Browsers without IntersectionObserver load the pack instead of leaving permanent placeholders.

The existing image route keeps its bounded byte cache, request coalescing, ETag, Last-Modified, and one-year immutable browser cache. HTTP/1.1 remains the default local transport. A future opt-in HTTPS proxy may add HTTP/2 and HTTP/3, but it must preserve HMR, Server-Sent Events, both localhost origins, and the standard harn web up lifecycle before becoming a supported mode.

The initial server response now contains 21 portrait URLs instead of 1,008. All expression labels remain present before hydration. Nearby packs start loading before they enter the viewport, and the page keeps the original individual WebP files instead of generating sprites.

A reload after viewing the full roster exposed a second bottleneck. Chrome’s Network panel reported the portraits as 200 (memory cache) in 0 ms, but some visible cells remained blank. The memory-cache hit proved that the response bytes were available. It did not prove that Chrome had a decoded bitmap ready to paint. A representative portrait was 40,798 bytes on disk and 512 by 768 pixels. Its decoded RGBA bitmap needs about 1.5 MiB. Mounting roughly 900 full-size portraits can therefore ask the browser to manage more than 1 GiB of decoded image data even when it transfers no bytes.

The asset route now accepts the fixed variant=roster-v1 query. It uses Sharp to produce a 256-pixel-wide WebP, keeps that transformed result in the existing bounded promise cache, and sends the same one-year immutable browser cache headers. CodecRosterExpressions uses this variant, observes packs with a 1,200-pixel vertical margin, and removes offscreen image elements when they leave the margin. When an extended expression uses a required-expression fallback, the roster now reuses the fallback URL instead of requesting the same file under a second expression URL. The source portrait route remains unchanged for the live Codec view.

For the representative portrait, the roster response was 12,444 bytes and its decoded bitmap is 384 KiB, one quarter of the source pixel count and decoded memory. After compilation, the local route served that cached thumbnail in 0.009 seconds. One cold 21-expression pack produced 307,660 response bytes in 0.207 seconds. The first server-rendered roster response still contained 21 image URLs, every expression label, and no sprite reference.