Skip to content

every logo is revalidated on every page view, and /capital has 158 of them - #2934

Merged
Flotapponnier merged 1 commit into
devfrom
fix/logo-cache-headers
Oct 9, 2026
Merged

Flotapponnier merged 1 commit into
devfrom
fix/logo-cache-headers

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

every logo is revalidated on every page view, and /capital has 158 of them

Provider marks are plain files under public/, and Next serves those as
cache-control: public, max-age=0, must-revalidate unless the config says
otherwise. The config did not: the only Cache-Control rule in next.config.ts
was for /api/citable.

must-revalidate means a browser holding the whole set in its disk cache still
has to ask the server about each file before it is allowed to paint it. On
/capital that is 158 conditional requests per visit. Measured against prod
from a machine sitting next to the cdg1 edge, each 304 costs about 95 ms and
carries no bytes. The marks render as empty circles until they land, which is
what a reader sees.

Why it turned slow recently rather than gradually: the page's logo set has
been growing, and most of the growth is new. Counting the files /capital
actually references by the month each was added to the repo:

2026-05 28 files 476 KB running 28 / 476 KB
2026-06 16 89 KB 44 / 564 KB
2026-07 19 663 KB 63 / 1227 KB
2026-08 18 326 KB 81 / 1553 KB
2026-09 72 1785 KB 153 / 3338 KB

September alone doubled the file count and added more weight than the four
months before it put together. A missing cache header is harmless at eighty
images and is not at a hundred and sixty.

So /logos/* gets a day of silence and then a month of serving the cached copy
while a fresh one is fetched behind it. Deliberately not immutable: these
filenames are stable and their CONTENTS get replaced — Chainstack sent a new
mark in #2910 — so a year-long immutable cache would pin an old logo on every
returning reader. A day is short enough that a swapped mark turns up on its
own.

Verified rather than assumed, because a config header does not always win:
the same block carries a comment about /api/citable, where this layer loses
to the Cache-Control the route handler sets. Static files have no handler, so
the rule applies. Checked on a dev server:

$ curl -sI /logos/ronin.png
Cache-Control: public, max-age=86400, stale-while-revalidate=2592000

and /favicon.ico still answers no-cache, must-revalidate, so the scope is
the one intended.

This does not touch the other half of the problem, which is a separate job:
3.5 MB of marks on one page, for circles rendered at 22 pixels. 32 files are
over 30 KB and carry 69 % of the weight, ronin.png alone is 227 KB. Re-encoded
at 64 px those 158 files would be roughly 630 KB, an 82 % cut on first load.
That one rewrites binaries and wants its own PR.

… them

Provider marks are plain files under `public/`, and Next serves those as
`cache-control: public, max-age=0, must-revalidate` unless the config says
otherwise. The config did not: the only Cache-Control rule in next.config.ts
was for /api/citable.

must-revalidate means a browser holding the whole set in its disk cache still
has to ask the server about each file before it is allowed to paint it. On
/capital that is 158 conditional requests per visit. Measured against prod
from a machine sitting next to the cdg1 edge, each 304 costs about 95 ms and
carries no bytes. The marks render as empty circles until they land, which is
what a reader sees.

Why it turned slow recently rather than gradually: the page's logo set has
been growing, and most of the growth is new. Counting the files /capital
actually references by the month each was added to the repo:

  2026-05   28 files    476 KB     running  28 /  476 KB
  2026-06   16          89 KB               44 /  564 KB
  2026-07   19         663 KB               63 / 1227 KB
  2026-08   18         326 KB               81 / 1553 KB
  2026-09   72        1785 KB              153 / 3338 KB

September alone doubled the file count and added more weight than the four
months before it put together. A missing cache header is harmless at eighty
images and is not at a hundred and sixty.

So /logos/* gets a day of silence and then a month of serving the cached copy
while a fresh one is fetched behind it. Deliberately not `immutable`: these
filenames are stable and their CONTENTS get replaced — Chainstack sent a new
mark in #2910 — so a year-long immutable cache would pin an old logo on every
returning reader. A day is short enough that a swapped mark turns up on its
own.

Verified rather than assumed, because a config header does not always win:
the same block carries a comment about /api/citable, where this layer loses
to the Cache-Control the route handler sets. Static files have no handler, so
the rule applies. Checked on a dev server:

  $ curl -sI /logos/ronin.png
  Cache-Control: public, max-age=86400, stale-while-revalidate=2592000

and /favicon.ico still answers `no-cache, must-revalidate`, so the scope is
the one intended.

This does not touch the other half of the problem, which is a separate job:
3.5 MB of marks on one page, for circles rendered at 22 pixels. 32 files are
over 30 KB and carry 69 % of the weight, ronin.png alone is 227 KB. Re-encoded
at 64 px those 158 files would be roughly 630 KB, an 82 % cut on first load.
That one rewrites binaries and wants its own PR.
@Flotapponnier
Flotapponnier merged commit ace675d into dev Oct 9, 2026
2 checks passed
Flotapponnier added a commit that referenced this pull request Oct 9, 2026
3.5 MB of logos on one page, for circles drawn at 22 pixels

The cache header landed in #2934 and fixes the repeat visit: a reader who
already holds the marks no longer asks the server about each one. It does
nothing for the first visit, which still downloads the lot. On /capital that
is 158 files and 3.5 MB, and the marks are rendered in 22 px circles.

The files were simply never sized for their use. ronin.png is 2000x2000 and
227 KB. tradoor.png is 1667x1667. tron.png is 2000x2000 and 104 KB. They are
whatever the provider's press kit shipped.

The largest size any `<ProviderLogo>` renders at anywhere in the codebase is
56, so every raster over 30 KB is capped at 160 px on its longest side: about
3x retina for the biggest use and 7x for the common 22 px chip. Format is
unchanged file by file — PNG stays PNG, WebP stays WebP — so
`logo-manifest.ts` needs no edit and no path moves.

  97 files   7,371 KB -> 1,333 KB   (-81%)
  /capital   3,506 KB -> 1,302 KB   (-63%)
  public/logos   13 MB -> 6.8 MB

Two files were left exactly as they were because re-encoding them came out
larger, which is the rule the script applies: a result that does not shrink
is not written.

Checked rather than assumed, because the failure mode here is silent and
ugly. Alpha and colour space survive on every sampled file (ronin, zircuit,
immutable, rhea-finance, sun, kinetiq, tron, wallet-v: `hasAlpha` and `space`
identical before and after), aspect ratios are preserved rather than squared
off (wallet-v 794x203 -> 160x41), and three of the resized files were opened
and look like their logos.

An earlier run of this nearly shipped something worse: `tar --exclude=script`
while packaging the rpc-cost harness also matched `cmd/script/`, the entire
Go source. Different job, same lesson about patterns that match more than the
one path they were written for.
@Flotapponnier
Flotapponnier deleted the fix/logo-cache-headers branch November 3, 2026 11:49
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