F.B. Sponsored/Ad Post Blocker version history - 18 versions
F.B. Sponsored/Ad Post Blocker by Inky
Be careful with old versions! These versions are displayed for testing and reference purposes.You should always use the latest version of an add-on.
Latest version
Version 1.1.50
Released 16 Aug 2026 - 64.48 kBWorks with firefox 109.0 and later, android 120.0 and later- The mobile feed stopped paging because of a single attribute write.
1.1.49 preserved every post's height and the feed still stalled, so the
problem was never the geometry.
Isolated by hand on a live phone feed, with the extension installed but all
hiding switched off:
| What was done to 6 feed children | Feed after 15s |
| --- | --- |
| Nothing (extension inert) | +58 posts — paging normally |
|data-fbsb-hidden+ inline style | stalled |
|data-fbsb-hiddenonly, no styling | stalled |
|visibility: hiddenon their children | +34 posts — still paging |
Setting one data attribute, changing nothing visible, was enough to stop
Facebook's pager for the full window. Its own observers evidently treat a
direct feed child that has been written to as changed underneath it, and stop
reconciling.
So the post element is now untouchable — no style, no attribute. Both go on
its children instead: every element child getsvisibility: hidden, and the
marker attribute rides on the first of them. The children keep their boxes,
so the post keeps its height, and Facebook sees a feed it still owns.hiddenPostsis still keyed by the post, so recycled-node detection resolves
a marker to its parent when the marker is not itself a key.
Desktop is unchanged: it removes posts withdisplay: noneas before, and
nothing there is virtualised or reconciled this way.
That the feed keeps paging with the extension actually doing the hiding, over a
long scroll — the table above was measured by hand, six posts at a time. It
also leaves blank space where each hidden post was, which is now the only known
remaining problem on mobile rather than one of several.Source code released under MIT Licence
- The mobile feed stopped paging because of a single attribute write.
Older versions
Version 1.1.49
Released 16 Aug 2026 - 63.29 kBWorks with firefox 109.0 and later, android 120.0 and later- The mobile feed stopped loading after about 15 seconds of scrolling, and
it was us. Established by control test on stock Firefox for Android:
extension disabled, the feed pages in new posts indefinitely; extension
enabled, it stalls. Same account, same feed, same browser.
Facebook virtualises that feed and decides what to page in next by measuring
rendered content.display: noneremoves a post's height from that
measurement, so hiding posts corrupts the figure the loop works from — hide
enough and it stops paging entirely. This also explains two things that never
quite added up: unchecking "Hide posts from Pages/Groups you don't follow",
which hides the largest share of any feed, made the blackout go away; and
1.1.44's pre-hiding, which hid far more posts than anything before it, made
it dramatically worse.
On mobile a post is now hidden withvisibility: hiddeninstead. It becomes
invisible while its box keeps exactly the height it had, so Facebook's
accounting sees a feed that never changed shape.
The cost is deliberate and visible: a hidden ad leaves blank space where it
was, rather than vanishing. That is the same gap reported throughout
testing, now accepted on purpose — blank space you can scroll past beats
content you cannot reach. Desktop is unchanged and still removes posts
outright; nothing there is virtualised this way.
With placeholder mode on, mobile now shows both the placeholder bar and the
blank space of the post behind it. Untidy, and left alone for now: the point of
this release is to find out whether preserving height keeps the feed alive.Source code released under MIT Licence
- The mobile feed stopped loading after about 15 seconds of scrolling, and
Version 1.1.48
Released 16 Aug 2026 - 62.16 kBWorks with firefox 109.0 and later, android 120.0 and later- Real timings in the diagnostics panel. Three costs, kept apart, with each
shown as a share of wall-clock time on the page:
over 47s on page:
scan 312ms 0.7% (204 scans, 18422 els)
observer 88ms 0.2% (1310 calls)
retry 4ms 0.0% (12 ticks)
They are separated because they fail for different reasons, and 1.1.35
proved a healthy scan figure says nothing about the other two: it reported
1.0ms across 274 scanswhile the page was unusable, because the cost was in
the retry loop, which never callsscanRoot. The observer had the same blind
spot until 1.1.42. Now all three are visible, and on a phone, which is where
none of them could be read before.
The percentage is the number worth reading. Milliseconds alone mean nothing
without knowing over how long they accumulated.
Scan timing was already collected in release builds —reportStatsreturns
early whenDEBUGis false, so nothing ever reset it — it simply had no way
to be seen. Observer timing is now collected unconditionally too: two
performance.now()calls per callback cost far less than what they measure,
and a figure that only exists in a build nobody installs is not a
measurement. Retry timing is new.
In aDEBUGbuild these are a rolling 2s window rather than cumulative,
becausereportStatsresets them. Worth remembering before comparing figures
between the two builds.Source code released under MIT Licence
- Real timings in the diagnostics panel. Three costs, kept apart, with each
Version 1.1.47
Released 16 Aug 2026 - 60.88 kBWorks with firefox 109.0 and later, android 120.0 and later- The mobile feed was slow to catch up, and it was the extension's fault.
Confirmed by control test: with the extension disabled the same feed loaded
quickly and normally.
A label whose post Facebook hasn't rendered gets deferred to the reveal
observer — but it was also being queued into the retry loop, which
re-examines every entry every 50ms for a full 8 seconds. Those retries can
never succeed: the candidate has no box and won't get one until Facebook
reveals it, at which point the IntersectionObserver handles it anyway. On a
virtualised feed most sponsored labels take that path, so the queue filled
with work that was guaranteed to be wasted.
This is the 1.1.35 regression's shape reached from a different direction — a
flooded retry queue burning the 50ms loop — and the same lesson applies:
queue only what can actually resolve later. Deferred labels are now left to
the reveal observer, which is already waiting for exactly the event that
would make them resolvable.unfollowedlabels were already excluded on the same reasoning. This extends
it to the case virtualisation creates.- The diagnostics panel reports
reveals— how many scans the reveal path has
triggered. It should track how far you have scrolled; climbing while the page
is still means the observer is firing when it shouldn't.
Source code released under MIT Licence
- The mobile feed was slow to catch up, and it was the extension's fault.
Version 1.1.46
Released 16 Aug 2026 - 60.05 kBWorks with firefox 109.0 and later, android 120.0 and later- The breakage warning fired on every mobile page load, blaming Facebook for
the extension working correctly. Observed on a real device running 1.1.45:
[fbsb] 20 labels matched, none could be anchored to a post ... If this is
a phone, the mobile class was probably renamed.
The mobile class had not been renamed — the warning says so itself two lines
earlier, reporting the gate asmobile. What actually happened is that most
of a virtualised feed is unrendered at any moment, those candidates have no
box, and since 1.1.45 they are deliberately deferred to the reveal observer.
The 1.1.42 check counted every deferral as a failure to anchor, so it
tripped its threshold on any mobile feed within a second of loading.
A miss on a post Facebook hasn't rendered is now counted separately and
excluded from the threshold. What remains is what the check was written for:
labels that should have anchored against something rendered, and didn't.
This mattered beyond the noise. The warning is notDEBUG-gated, so it
reached anyone with a console open, and it pointed confidently at the wrong
cause — the exact failure mode 1.1.42 was built to prevent.- The diagnostics panel reports
deferredalongside the other counts. A large
number there next to a healthyanchoredis the reveal mechanism working,
not failing.
Source code released under MIT Licence
- The breakage warning fired on every mobile page load, blaming Facebook for
Version 1.1.45
Released 16 Aug 2026 - 59 kBWorks with firefox 109.0 and later, android 120.0 and laterSource code released under MIT Licence
Version 1.1.44
Released 15 Aug 2026 - 57.28 kBWorks with firefox 109.0 and later, android 120.0 and later- Ads kept appearing on a phone while the badge said posts were hidden.
Both statements were true. Facebook virtualises the mobile feed: only a
window of posts is rendered, the rest sit atdisplay: nonewith afiller
element reserving their scroll height. Measured on a live phone feed, 42 of
64 feed children were hidden at once, behind a filler 13,226px tall.
A virtualised-out post reportsoffsetWidth0 — it has no box. The width
rule infindMobilePostContainerread that as "narrower than 60% of the
feed" and returnednull, so every off-screen ad was classified, rejected,
and forgotten. Facebook then revealed it on scroll, unfiltered. The posts
that were on screen resolved normally, which is why the badge kept
climbing while ads stayed visible.
The rule exists to reject nested carousel items — real boxes that happen to
be narrow. It can say nothing about an element with no box at all, so it is
now applied only to elements that have one. "Not currently rendered" and
"too narrow to be a post" are different claims, and only the second is
evidence against something being a post.
Desktop is unaffected: nothing there is virtualised this way, so every
candidate has a box and the rule applies exactly as before.
Long blank gaps between posts are a separate problem with the same root.
Facebook sizes that filler assuming the posts it virtualised still occupy
their heights; hiding one shrinks the content without shrinking the filler.
This release does not address that.
It is also not yet known whether a hide applied while a post is virtualised
out survives Facebook revealing it — if Facebook overwrites the inline style,
the ad returns, and the observer would not notice because it watcheschildListonly, not attributes. That is the next thing to measure, and the
reason this ships as one change rather than two.Source code released under MIT Licence
- Ads kept appearing on a phone while the badge said posts were hidden.
Version 1.1.42
Released 15 Aug 2026 - 53.39 kBWorks with firefox 109.0 and later, android 120.0 and later- Structural breakage now announces itself. Every mobile code path is gated
on` and the app banner on.fixed-container.bottom`. Both names are Facebook's to change, and when
either goes the symptom is silence: labels still classify, nothing anchors,
and the extension looks completely healthy while hiding nothing. 1.1.39 spent
three stacked fixes inside exactly that blind spot.
The check is not "doeshtml-rendererstill match" — that only catches the
rename already imagined. It counts classified labels against anchored ones,
and warns once per page if 20 labels match while none resolve. That signature
means detection works and container resolution does not, which is what a
structural rename looks like on either layout, desktop landmarks included.
The warning reports which layout gate was taken so the two cases can be told
apart immediately.
The app banner gets its own check, since it is the one target anchored by
selector rather than by climbing: reaching resolution at all means the text
matched and the layout gate passed, so failing to find the bar is
unambiguous and needs no threshold.
This one is deliberately notDEBUG-gated. A diagnostic that only speaks
in a build the user isn't running does not fix a silent failure. It is one
console.warn, at most once per page, and it cannot fire on a page where
anything at all was successfully hidden.
It warns; it does not adapt. A structural fallback was considered and rejected:
guessing the layout from "no ARIA landmarks, shallow document" would let a
wrong guess disable every mobile path silently — reintroducing the failure mode
this is meant to remove, one level further down.Source code released under MIT Licence
- Structural breakage now announces itself. Every mobile code path is gated
Version 1.1.41
Released 14 Aug 2026 - 51.52 kBWorks with firefox 109.0 and later, android 120.0 and later- Declared an Android compatibility floor of 120. AMO validation flagged
permissions.requestas unimplemented at the stated minimum, and it was
right: per Mozilla's compatibility data that API landed in Firefox for
Android 120, while the manifest claimed 109. Below 120 the "Allow on
facebook.com" button in both the popup and the setup page would have done
nothing at all — on the one platform 1.1.39 exists to serve. 120 is also
where Firefox for Android gained general extension support, so it is the
floor at which any of this is installable anyway.
gecko_androidsets a compatibility range separate from desktop, which is
what it is for. Desktop stays at 109.
AMO still warns thatdata_collection_permissionspostdates the stated
minimum (140 desktop, 142 Android). Those are left alone deliberately: it is
a manifest key, unknown keys are ignored by older browsers, and nothing
behaves differently. Silencing them would mean raising the desktop floor from
109 to 140 — cutting off every user between — to quiet a cosmetic warning
about a key whose entire content is a declaration that no data is collected.Source code released under MIT Licence
- Declared an Android compatibility floor of 120. AMO validation flagged
Version 1.1.39
Released 14 Aug 2026 - 49.77 kBWorks with firefox 109.0 and later- Works on Facebook's mobile web layout. Installed on a phone the
extension hid nothing at all, while looking perfectly healthy: permissions
granted, content script injected, no errors. Three separate faults were
stacked behind that, each invisible until the one before it was fixed.
Container resolution had nothing to anchor to. Mobile web ("weblite" —
it tags` withhtml-renderer) is a different app, not a narrowrole="article"
desktop. It exposes no ARIA landmarks whatsoever: no, noaria-posinset, nodata-pagelet, norole="complementary", and the<div>
author header is a plainrather than a heading. Every strategy infindPostContainerkeys off one of those, so all of them returnednull.findMobilePostContainerclimbs instead: that layout's feed is a singleunfollowed
container whose direct children are the posts, so the post is the last
ancestor before the first ancestor with many children. A width check
rejects nested carousels, which can also clear the child-count bar. For, the label must sit inside the container's first child —isAuthorLevelLabel`'s "don't hide a post over a quoted author's Follow
button" rule, expressed without headings to key off.
Ad labels were unmatchable. Weblite draws its icons from a font mapped
into the Private Use Area and packs them into the same span as the text, so
an ad's label is literally"Ad\u{F078B}\u{F17E0}". Those glyphs are
categoryCo, andINVISIBLE_CHARS_REstripped onlyCfandMn, so the
cleaned text never equalled"Ad". This is precisely why mobile hid
unfollowed posts but never ads:"Follow"happens to sit in a span of its
own, with no icons alongside it.
The decoy filter threw the labels away before either fix could matter.
isImplausiblyShallowtreats anything within 10 levels of` as aMOBILE_SHALLOW_DEPTH_LIMIT`).
portal/decoy span, which holds on desktop where real posts sit 15+ deep.
Weblite's entire document is about 11 levels and an ad label measures
exactly 10, so every real ad was discarded before resolution was attempted.
The limit is now layout-aware (
Desktop behaviour is unchanged. TheCostrip does apply to both, but it
can only shorten text: the neighbouring organic-post span is a timestamp
plus the same icons,"1h\u{F212D}\u{F3196}", which cleans to"1h"and
matches no target. Confirmed against a live desktop feed as well as mobile.Source code released under MIT Licence
- Works on Facebook's mobile web layout. Installed on a phone the
Version 1.1.38
Released 13 Aug 2026 - 46.91 kBWorks with firefox 109.0 and later- The
DEBUGperf line now reports MutationObserver cost separately. It
previously timedscanRootonly, so everything the observer callback does
includingcacheLabelTargetswalking every subtree Facebook inserts during
its initial render was invisible. That blind spot had already hidden one
regression: the retry-loop freeze fixed in 1.1.35 reported a healthy
1.0ms across 274 scanswhile the page was unusable.
Measured on a real feed, the observer costs 2–16ms per 2s window and falls
after load rather than spiking during it, which ruled it out as the cause of
a slow first paint that had been attributed to it.
- README leads with an Install section pointing at the store listings.
Both sideload sections are now labelledDevelopment:a temporary add-on
disappears on restart and never updates, so it is the wrong way to install
this for normal use.
No behaviour change in release builds: the instrumentation isDEBUG-only.Source code released under MIT Licence
- The
Version 1.1.37
Released 13 Aug 2026 - 45.66 kBWorks with firefox 109.0 and later- A setup page opens once, on first install. The popup prompt added in
1.1.36 only helps someone who opens the popup, and a new Firefox user has no
reason to: the extension appears installed and simply does nothing. The page
explains that facebook.com access is still needed and requests it directly.
It reads the current permission state rather than assuming: where access is
already granted always the case on Chromium it shows a short "you're all
set" confirmation instead of asking for anything. Gated on
reason === "install"so upgrades don't reopen it, and thetabs.create
call is wrapped, because failing to open a setup page must not take the
background script down with it.
-build.ps1copiesonboarding/. The payload is an explicit file list, so a
new directory ships only when added here — worth remembering when adding
another.Source code released under MIT Licence
- A setup page opens once, on first install. The popup prompt added in
Version 1.1.35
Released 13 Aug 2026 - 41.37 kBWorks with firefox 109.0 and laterThe feed stopped loading. 1.1.30 queued any element carrying an
aria-labelledbywhose target didn't resolve, on the theory that ad labels
arrive late; 1.1.33 then tightened the retry loop to one frame. Facebook has
a great many elements with dangling label references a 300-post feed
carries roughly 1,800 — so the queue flooded and every entry was
re-examined every 16ms for the full 8s window.
The theory was wrong regardless: late-arriving labels were never what hid
feed ads. Following the sprite reference (1.1.32) was. That queueing is
removed, and the retry loop is back to 50ms.
- The retry queue is now capped (MAX_PENDING_LABELS). It exists for ads
staged in a hidden node and reparented a moment later, which is a handful of
entries at most; a future change that queues too eagerly should degrade
detection, not the page.Source code released under MIT Licence
Version 1.1.33
Released 13 Aug 2026 - 39.25 kBWorks with firefox 109.0 and laterSource code released under MIT Licence
Version 1.1.21
Released 12 Aug 2026 - 31.31 kBWorks with firefox 109.0 and laterAdded CHANGELOG.md for version update notesSource code released under MIT Licence
Version 1.1.20
Released 12 Aug 2026 - 28.77 kBWorks with firefox 109.0 and laterF.B. Sponsored/Ad Post Blocker — 1.1.20
Fixed
Sponsored posts were not being hidden at all. Facebook's scrambled "Sponsored" label pads the real characters with decoy spans, distinguished by class-list length — but the direction of that signal had flipped. The filter was discarding the real characters (~22 classes) and keeping the decoys (~7). Detection now assembles every plausible partition and matches against any, so a future flip can't silently kill it again.
Hidden posts immediately reappeared. Any node added inside a hidden post was treated as Facebook recycling the container, and the post was restored. Ads mutate constantly after being hidden (video players, lazy-loaded media), so they were un-hidden within milliseconds — and never re-examined, because scanning only ever runs on newly-added nodes. Restoring now re-checks the evidence first.
Group posts were wrongly hidden. A post embedding a shared post inherited the quoted author's "Follow" button, so a group you're a member of quoting someone you don't follow was hidden entirely. Follow/Join now only counts for the post's own author.
The toolbar counter reset by itself. The background script is an event page — Firefox suspends it after ~30s idle, discarding the in-memory tally. The badge then jumped back to 1 while the popup reported 0. The count is now read back from the badge itself, which survives suspension, with per-tab serialisation so concurrent updates can't lose increments.
Performance
Element text is now read only on leaves and character-split labels. Wrappers are skipped — the leaf holding the text is visited in its own right, so reading wrappers re-walked the same subtree once per nesting level.
The getComputedStyle walk is gated behind a structural check, instead of running on any element with two or more children.
Facebook's portal accessibility spans (<span id="r…_">, which contain the word "Sponsored" but belong to no post) are dropped before entering the retry queue.
Unfollowed labels no longer enter the retry queue — a Follow button and its author header always render together, so retrying can't change the outcome.
Changed
DEBUG now defaults to false. When enabled it reports the running build version, a rolling scan-cost summary, and unresolved matches capped at 15.
README rewritten to correct several stale claims and document the traps.
Known issues
Right-column sidebar ads are detected but not hidden. They have no aria-posinset, and the sidebar no longer carries the role="complementary" landmark the fallback relied on. Hiding nothing was preferred over risking an over-broad match.
Unfollowed detection assumes the post's own author header is the first heading in the post. If it isn't, those posts are missed (fails quiet rather than hiding wrongly).
Feed ad detection rests on the scrambled-text path; the "… sponsored content" aria-label only matches sidebar ads. Some feed ads may slip through.
English-language labels only.Source code released under MIT Licence
Version 1.1.10
Released 12 Aug 2026 - 23.01 kBWorks with firefox 109.0 and laterFixes a bug where a "Follow [creator]" button in a Reels comment panel could be mistakenly resolved through the sidebar ad detection path, incorrectly hiding the entire comments panel. The "hide unfollowed Pages/Groups" feature no longer uses that fallback at all, it's now restricted to genuine feed posts only. No other behavior changes.Source code released under MIT Licence
Version 1.1.8
Released 9 Aug 2026 - 21.82 kBWorks with firefox 109.0 and laterReels' comments/info panel could get incorrectly hidden. Root cause: the "hide posts from unfollowed Pages/Groups" feature was matching a "Follow [Reel creator]" button and climbing up to the nearest role="complementary" ancestor to decide what to hide, a fallback originally built for the main feed's right column ad sidebar, but FBs also happens to mark a Reel page's entire comments panel with that same role="complementary" landmark. The "unfollowed" detection no longer uses that sidebar fallback at all; it's now restricted to sponsored/suggested content only, which is what it was actually validated against.Source code released under MIT Licence