v0.53.1
v0.53.1, 2026-09-02
Changed
- An
i.redd.itexpando now loads the original image instead of reddit's signed
preview. The preview is re-encoded to webp, so an expando showed a lossy copy
at whatever size reddit felt like serving, and saving it wrote webp bytes into
a file named.pngbecause the signed URL's path still ends in.png. Images
past 24 megapixels keep showing the preview, which is what was capping the
bytes an expando pulls inline, but the save control is pointed at the original
either way. A crosspost reads all of this from the parent post, the same one
its preview already came from. - A filter rule's regular expression is compiled once when the rules are parsed
rather than once per rule per post. The catastrophic-pattern check and
new RegExpran inside the per-post evaluation, and the shipped default rule
set contains an enabled regex rule, so that was everyone's default path: about
two thousand compilations across a thousand-post scroll for a pattern that
never changed. A malformed or catastrophic pattern is still refused, now at
parse time, and still matches nothing rather than throwing mid-filter. - A live score tick on current Reddit no longer re-runs a post's whole
preparation. Reddit updates the score as an attribute, and the observer routed
that through the full pass — eight to ten attribute copies, five class toggles,
up to nine selector queries and the shadow-part exposure, per post, per tick,
hundreds of times while a feed hydrates. The attribute path now does only the
mirroring it needs. A post that has never been prepared still gets the full
pass even if an attribute change is the first thing to reach it.
Added
yarn publish:release, the half the release helper deliberately stopped
before. It runs every gate and the Firefox audit, refuses a dirty tree, the
wrong remote, a lightweight tag, a tag that does not name the commit being
published, a tagged tree whose version or CHANGELOG does not match, or a
missing artifact. Then it pushes, hashes both ZIPs, renames them after the
tag, uploads them with aSHA256SUMSfile, and reads the release back off
GitHub to confirm the assets arrived at the sizes it built. Re-running an
already-published version completes it rather than making a second one.
Fixed
-
Release files now stay in a system temporary directory while Git's push hooks
repeat the build. The hooks cleardist, which previously removed the staged
ZIPs, checksum file, and release notes after the tag reached GitHub but before
the release could be created. -
The thread minimap forced a layout per comment, every time it rebuilt. It read
each comment's rectangle and appended that comment's stripe in the same loop,
so every read had to flush the previous write — and the whole rail is rebuilt
200ms after any mutation anywhere in the comment area, which includes every
class this extension toggles on any comment. Rectangles are now read in one
pass, the stripes are built off-document and attached once, a single delegated
listener on the rail replaces one per stripe, and the scroll handler is
frame-throttled against a cached document height instead of measuring one on
every event. -
Six defects in the route-aware lifecycle, found by an adversarial review of
the change that introduced it. A route change betweenDOMContentLoadedand
loadranafterLoadfor every eligible module and thenloadran them all
again — measured as two IntersectionObservers and a second scroll listener from
the media expander alone. The stage gate did not coverbeforeLoad, which is
the only stage two comments-scoped modules have, so on the delayed-page-type
case the change was named for they still ran zero times rather than once. The
route scope was aborted on every navigation with nothing subscribed to it,
because the stages it started were handed the page-length signal instead. The
four stages a route change offers were dispatched together rather than in
order. And the full per-post preparation still ran on every child mutation,
which reddit produces throughout hydration — it now runs when slotted content
actually arrives. -
Old Reddit's endless scroll could stall on a page of pure overlap. Skipping
every repeated row means such a page changes the DOM by nothing at all, and the
only thing that asks for another page is anIntersectionObserver, which fires
on transitions — so with the sentinel already in view and the geometry
unchanged, no further callback ever arrived. A page that adds nothing now
fetches the next one itself, bounded at three in a row so a listing that has
genuinely run out is not walked to reddit's pagination ceiling. -
Current Reddit navigates without ever unloading, and the module lifecycle did
not know it. ApushStatethat changed no DOM was invisible — which is the
normal case, because reddit swaps the URL before rendering the new view — so
only the back button and an incidental mutation were ever noticed. The
Navigation API'snavigatesuccessis now the primary signal, with the observer
andpopstatebehind it, and every source runs through one dedupe keyed on the
URL plus what reddit says the page is.Two consequences follow. A page stage now runs for modules that have only just
become eligible, so a module whoseincludefinally matches gets its turn
instead of sitting the session out; modules that already ran are not run again,
which would have duplicated every observer and control they made. And there is
a route-length scope beside the page-length one, so work started for the
listing you left can be cancelled instead of running against a page that no
longer exists.Page-scoped stages also wait for reddit's own answer about what the page is
before deciding who belongs on it./r/x/s/<id>share links are post pages
that match no path pattern, so the fallback called them listings and every
comments-scoped module sat them out permanently. -
Hiding a post could destroy an embed instead of quietening it. Routing the
hide path through the expando collapse lifecycle inherited that lifecycle's
fallback: four of the thirty-five iframe hosts declare a pause command, and for
the other thirty-one — CodePen, JSFiddle, Pastebin, the poll hosts, Twitch —
collapse removes the frame. That is right when the reader asked for it and
re-expanding rebuilds; it is wrong for a filter, which takes an in-progress
edit or a filled-in poll with it and gives nothing back. Hiding now asks for a
collapse that keeps content, so a frame with no pause command is left exactly
where it was. -
A back/forward-cache restore left old Reddit without its per-post work. The
page signal was aborted on anypagehide, including one that only puts the
page into the cache — and the document comes back with that signal still
aborted, so the two waits that register a post's visible tasks failed before
observing anything. Nothing appended after a restore got an expando, a filter,
or a vote colour. Only a real teardown aborts now, and both waits swallow their
own cancellation rather than raising an unhandled rejection per pending post on
every navigation away. -
Two contrast failures on the light Classic palette, found by putting current
Reddit's injected controls under an automated WCAG sweep for the first time.
Absolute timestamps were faded withopacity: 0.75, which took reddit's own
tagline grey down to 3.16:1; they now use the managed muted ink, which is
contrast-checked and flips with the palette where an opacity multiplier cannot.
And the palette's muted grey was reddit's#666, which clears AA on white but
not on the surfaces this theme tints with the accent — the submitter chip
measured 4.09:1. It is a little darker now and clears both. -
Three DOM waits could never end, and a multi-event wait left its losing
listener attached.waitForSelectorMatchwas the expensive one: its only
caller runs for every link post, waiting for an expando the reader may never
open, so an attribute observer and a pending async frame accumulated per post
across an infinite scroll. All three now take the same timeout and abort
contract the other waiters use, and core hands them a signal tied to the page's
lifetime.waitForEventruns one race and removes every listener it installed
when any of them wins. -
A shred run that lost its account kept deleting. The heartbeat renewed the
lease and threw the answer away, so a run whose lease had gone — to a service
worker restart, or to timer throttling in a background tab outrunning the
expiry — carried on issuing edits and deletes while another tab held the
account. That is the exact overlap the lease exists to prevent. A heartbeat
that cannot hold the account now re-asserts once, and stops the run between
comments if the account really has been taken, saying so in the outcome. -
Typing in a shred panel's confirmation box while it was asking the background
whether another run was going re-enabled the Shred button. The second click's
refusal landed after the first run had started and overwrote its live progress
line with "another run is already going", then handed the button back mid-run. -
Hiding a post left an expando's embedded player running. Pausing a
<video>
reaches the media the page owns, and nothing else: an expando this extension
opened holds its own player, which knows the pause command its host
understands. Hiding now collapses those through the same lifecycle a manual
collapse uses, so unhiding rebuilds them. A third-party frame nobody declared a
pause command for is deliberately left alone — the only generic way to stop one
is to drop or reload it, and a filter hides a post you may unhide a moment
later. -
Old Reddit's endless scroll could show the same post twice, and drew a "page
loaded" separator over nothing when a whole page was overlap. Reddit'safter
cursor overlaps by design, and a post that moves up the listing between two
fetches is served twice. The repeat was removed after being fetched, parsed,
inserted and registered, which is the wrong layer to rely on alone. The append
loop now compares against the fullnames already on the page and skips what it
has, keeps rows that carry no usable id rather than guessing, and emits a
separator only for a page that actually added something. -
The optional-permission prompt could not work on Firefox. It opened in an
extension popup window, and Firefox refusespermissions.request()from one
(Mozilla Bug 1957822), so the window appeared and then could not do the single
thing it existed for. Firefox now gets a normal tab, which is a context the
browser is willing to raise its own permission panel from; Chrome keeps the
centred popup window, which works there. -
Two tabs could shred the same account at once. The guard was a boolean scoped
to one tab, which by construction could never see the tab that matters, and
the module's own header already described what happens when two runs overlap:
they share reddit's per-account write limiter, and the second tries to delete
comments the first already deleted, turning a normal outcome into a page of
failures reported as untouched. Nothing on the page could arbitrate it either,
because old Reddit and current Reddit are different origins.A run now takes a lease on the account, held by the extension background,
which is the only context both renderers share. It is keyed by the normalized
username, renewed while the run goes, released when the run ends, and expires
on its own if the tab that held it dies. A second tab is refused, told where
the other run is and how long it has been going, and can retry when that run
finishes.