Release history
Changelog
Every new feature, fix and speed-up on s&box Explorer, newest first.
- Last 30 days
- 90
- Features shipped
- 233
- Fixes
- 218
- Days with changes
- 66
4 changes in September 2026 Clear filters
September 2026
1
-
Performance
10:32 PM
Serve forum pages from cache and stop every expiry costing a second
Details
A thread listing took 937ms on a cache miss and the entries lasted five minutes, so roughly every fifth minute somebody paid a full second for a page that had not changed. Profiling first ruled out the obvious suspect: sanitising fourteen posts costs 1ms, so the HTML pass is not worth caching and is left alone.
The forum fetchers now go through CacheManager::getOrSet rather than a plain read-then-fetch. That was already in the codebase and does two things this needs: it takes a lock so only one request regenerates while everyone else is handed the existing entry immediately, and its XFetch beta regenerates probabilistically before expiry so the changeover is spread out instead of landing on one unlucky visitor.
Lifetimes now follow how mutable the data actually is. The forum list itself changes about never, so an hour. Page one of a listing keeps its five minutes because new threads land there; later pages get half an hour because they only change when threads shuffle off page one. A thread page that is not the last page cannot gain posts, so it is held for six hours after it is read.
Forum activity keeps its own short mutex so a cold batch of ten upstream calls is not started by several visitors at once; the rest are handed the stale copy. It is best-effort rather than a real lock, since the worst case is two batches running and the cost is one wasted fetch.
Every fetcher keeps the last good answer for a week and falls back to it when Facepunch fails, which the tests cover along with the refetch on expiry.
Warm page loads are now 30-40ms across the forum index, listings, deep pages and threads.
bd13b1b
2
-
Performance
8:52 AM
Stop loading a 300KB png for a 72px icon
Details
The package icon beside the title renders at 72 square but was served the full-size Thumb: 268KB for roundbound, 378KB for micromayhem and sandbox. Every package publishes Square webp variants at 32 through 512, and the 128px one is 2-5KB - a 99% saving on an image that carries fetchpriority="high" and so competes with the hero for bandwidth on first paint.
This is the same conversion the browse grids and homepage got in e7738c3; the detail page's own icon was missed. thumbSources falls back to Thumb, so packages cached before the change still render.
370b617 -
Performance
8:29 AM
Stop shipping a 5MB icon font and full-size PNGs where WebP exists
Details
Two findings from measuring what the browser actually downloads.
The Material Symbols request left every variable axis open (opsz,wght,FILL,[email protected],100..700,0..1,-50..200), so Google served the complete variable font: 5,248KB. base.css renders every icon at a single point in that space - FILL 1, wght 400, GRAD 0, opsz 24 - so the axes are now pinned to exactly that: 447KB, a 4.8MB saving on a first visit. Weight ranges are not worth it; any range costs 1,208KB, so the two rules asking for wght 500 and the profile tag icons asking for 600 are set to 400, which at icon sizes is not a visible difference. material-symbols.css also imported Material Symbols Outlined, a second 3,887KB face that nothing could use: base.css overrides the outlined class to the Rounded family with !important. That import is gone, and head.php and the stylesheet now request one identical URL so it is fetched once.
Facepunch publishes resized WebP variants of every thumbnail - Square 32-512, Wide 160x90-910x512, Tall - in the same responses the site already reads, and the site was using the full-size PNG. thumbSources() picks the smallest variant that covers the space and falls back to the old field, so packages cached before this change still render. Measured, same pages: /t/model/ 1.10MB -> 0.60MB, /hidden-gems/ 11.23MB -> 0.67MB. The homepage reads its lists from a 30-60 minute cache, so it changes over as that expires; fetchCarouselPackages was verified directly and now resolves to a 320x180.webp.
The screenshot rail on package pages loaded the full-size stage image for each thumbnail. Screenshots carry their own Thumb, usually the same file but not always - one package had a 5.6MB PNG whose thumb is 114KB - so the rail uses it where it differs.
Org logos are left alone: the API exposes only Thumb for an organisation, the CDN ignores resize parameters, and some orgs upload 600KB PNGs shown at 24px. That one needs an image proxy or a word with Facepunch.
Three files show a large diff because git normalises their CRLF line endings to LF on staging under core.autocrlf=input; `git diff -w` shows the real change is 8, 3 and 7 lines.
e7738c3
1
-
Performance
4:42 AM
Stop background polling and collapse two N+1 queries
Details
Four pages polled on a bare setInterval, so a left-open background tab kept hitting the database indefinitely; Advanced Analytics did it every five seconds. adminPoll() skips the work while document.hidden and refreshes immediately when the tab comes back.
MCP API Keys resolved a username with its own query for every banned user rendered; one DISTINCT ON lookup now covers the whole list. Cleanup Invalid Users opened a transaction and issued three deletes per account; it is three set-based deletes in one transaction.
191f35b
Generated from the project's commit history whenever the site is deployed. Times are server time.