Why Browser Extensions Slow Down Your Browser – Ours Did Too

Browser extensions slow down your browser by running code on every page. We shipped our entire interface into every iframe for 100 days. Here's the receipt.

If your browser got slower and you can't work out why, the honest answer is usually the row of little icons next to your address bar. For four months in 2026, one of those icons was ours – and UltraWideo was measurably part of the problem on every page you opened, whether you used it or not.

This is the post-mortem. Real commits, real byte counts, real regressions we shipped to more than 80,000 people. Why browser extensions slow down your browser is a question with a boring general answer and a much more useful specific one, so you get both.

Why browser extensions slow down your browser

Extensions slow browsers down mainly because they don't wait to be asked. Most of an extension's cost isn't in the feature you use – it's in the content script, code the browser injects into every page matching the extension's permissions, before you've clicked anything. Ten extensions each injecting a modest bundle means all of that code downloads, parses and executes on every tab you open, plus every <iframe> inside it.

That last part is what gets people. A single article page can contain a dozen frames – embedded players, ads, comment widgets, consent dialogs. If an extension declares all_frames, its payload is paid once per frame, not once per page.

We know because we did it.

What we shipped in 4.0, and why

For most of UltraWideo's life, the interface was a normal toolbar popup – the thing you get when you click an extension's icon. Version 4.0.0, released on 29 April 2026, moved it into the page. The changelog led with it as the headline feature:

New floating panel right on the page – drag it where you like, summon it with a shortcut, no more trips to the toolbar.

There was a real reason, and it's worth stating fairly because it's the same reason a lot of extensions make this choice. A toolbar popup cannot open while a video is in element fullscreen. That isn't a bug or a gap we could engineer around; it's how browser chrome works in Chrome and Firefox alike. (Browser fullscreen — the F11 kind — is fine; the toolbar reveals on hover. It's the video-goes-fullscreen case that locks you out.) UltraWideo's entire job is fixing how video fills your screen, and the moment you actually care most – fullscreen, lights down, film starting – is precisely the moment the browser refuses to show a popup.

Putting the panel in the page solved that. It could be re-parented into the fullscreen element and painted on top. It followed the video everywhere.

It also meant the panel now lived inside somebody else's document, and had to survive whatever that document did.

The bill came due

Living in someone else's page turned out to be expensive in four separate currencies.

Bytes. At version 4.0.9, the manifest ran two content scripts with all_frames: true on every http:// and https:// URL – minus five CAPTCHA origins we'd already had to carve out. A clean production build of that version put 744,667 uncompressed bytes of JavaScript into every frame of every page: 88,390 bytes of video engine and 656,277 bytes of Vue control panel. Only 32,914 bytes of that engine was the code that finds and reshapes video; the other 55,476 was a WebGL library, statically imported for a Pro ambient-light feature most frames would never run. Every ad iframe on every site you visited paid for all of it. The guard that decides "this frame can't host our UI, bail out" sat at character 651,005 of a 656,120-character file – 99.2% of the way in. The frame parsed the entire app before reaching the line that concluded it wasn't needed.

Compatibility. Tailwind's spacing units are in rem, which resolve against the host page's root font size. YouTube sets html { font-size: 10px }, which shrank the whole panel. The fix was an 88-line custom build plugin, vite-rem-to-px.ts, whose entire job was rewriting every rem to px. Our dropdown library locked the host page's <body> when a menu opened, breaking layouts on sites that relied on overflow. We believed Netflix was pulling focus back to its own player, so whenever focus left the panel we waited one animation frame and, if nothing inside had claimed it, put it back – and then shipped a bug where closing the panel left YouTube's arrow-key seeking dead until you reloaded. The panel host carried z-index: 2147483647, the largest value browsers accept, and still lost to Apple TV, whose player is a modal <dialog> that the browser paints above everything regardless.

Breakage in places a video extension has no business being. Our content script was injecting into Cloudflare Turnstile, hCaptcha and reCAPTCHA frames – mounting a shadow root into their <body>, injecting stylesheets and running a MutationObserver inside them – which tripped their anti-tamper checks and could make verification fail or loop. Five origins had to be hard-coded into an exclusion list. An aspect-ratio tool was breaking logins.

Leaks. For the first 23 days, nothing in production tore any of it down. The cleanup path was registered only inside if (import.meta.hot), a development-only flag the build strips out – so a five-second scan loop, a MutationObserver on document.documentElement and a patch to HTMLMediaElement.prototype.setMediaKeys ran for the entire life of every frame they were injected into. Version 4.0.2 (22 May) wired a real pagehide teardown and replaced the fixed five-second scan with one that backs off to 30 seconds, pauses on hidden tabs and gives up after fifteen fruitless minutes. Over the same period ambient light leaked WebGL contexts, toward a cap of 16 live contexts per renderer process in Chrome and Safari – 8 on Chrome for Android – at which point the browser evicts the oldest one, potentially the host page's own.

The retreat, version by version

We didn't fix this in one decision. We backed out of it across four releases and the better part of three months, and the changelog reads like a slow-motion admission.

4.0.4 (9 June) shipped a way to hide the panel permanently – one commit, the only code change in the entire release. The headline we put on our own site read: "Hide the floating panel for good – it now stays hidden across every site and reload." All four bullets in the release notes were about hiding the panel. When an entire release is about making your headline feature go away, the feature has a problem.

4.0.5 (21 June) flipped the default. The floating button was off unless you opted back in, and we said why in plain language:

The on-page button could cover parts of some sites, and many of you asked for a normal toolbar experience.

That release also carried the least flattering detail in this whole story, and the most honest one. Because existing users would never see an in-popup notice, we force-opened a browser tab explaining where the button had gone, fronted by an apology modal titled "Pardon the interruption 🙏". The commit that built it explains the copy without decoration: "Aim: pre-empt negative reviews about the interruption." Interrupting people to explain a breaking change is defensible. Writing the apology with one eye on the store rating is the part worth admitting.

4.1.0 (7 August) stopped mounting the panel in every frame, loading it once and only entering an embed when that embed actually went fullscreen. UltraWideo had been shipping its whole interface into every iframe on every page for 100 days. That single release delivered 98% of the eventual per-frame saving. It also replaced ten of fourteen site-specific CSS hacks with one general layout repair.

4.1.1 (8 August) – the next day – reverted that repair wholesale. On YouTube it had measured the page mid-fullscreen-transition, marked the DOM all the way up to ytd-app, and collapsed the player's own layout. The video still looked correct, so the only symptom users saw was that the player controls stopped appearing on mouse movement. Two narrower fixes were tried first and both broke MLB, so the whole layer went back to its previous state rather than being iterated on against a live release.

That is what four months of defending an architecture looks like from the inside.

The document that said it was impossible

On 25 July we wrote an internal design doc for making the in-page panel cheaper. It opens by ruling out the alternative, in bold:

Native default_popup is a non-starter. A toolbar popup is browser chrome anchored to the icon; it can never overlay a <video> and is inaccessible during element-fullscreen — universally, in Chrome and Firefox, unchanged and not changing.

Eighteen days later, that file was deleted by the commit that moved UltraWideo's UI into default_popup.

What changed wasn't the constraint. The element-fullscreen half of it was correct, and still is. What changed was noticing the very next bullet in our own document:

The panel never touched the video.

Settings already flowed from the panel through storage to the background worker and out to each frame. The panel was never manipulating video directly; it was sending messages. So the only thing the in-page architecture actually bought was pixels over a fullscreen video – and we were paying for it on every frame of every page, all the time, for a moment that lasts as long as it takes to change a setting.

Once the question became "what's the cheapest way to draw a confirmation over fullscreen video?" rather than "how do we host an entire Vue app there?", the answer was obviously not 656 kilobytes.

What moving to the native popup deleted

Version 4.2.0 made the popup a real browser popup. The numbers, measured from clean production builds of all three releases:

Measured (uncompressed bytes, clean production Chrome build)v4.0.9 (in-page)v4.1.1 (lazy panel)v4.2.0 (native popup)
JS parsed in every frame of every page744,667105,96894,758
Everything injected or fetched in the top frame809,537814,34398,182
The panel's stylesheet, per mounting frame61,64761,7760
Web-accessible resource patterns341
App.vue812 lines820 lines65 lines

Moving UltraWideo's interface out of the page cut the JavaScript parsed in every frame of every page by 87%, from 744,667 to 94,758 uncompressed bytes, and cut the top frame's total from 809,537 to 98,182 — 8.2× less.

Read that middle column carefully, because it's the honest part. 4.1.0 made the panel load lazily, and that is where almost all of the per-frame win came from — iframes went from 744,667 bytes to 105,968. But the top frame got marginally worse: the lazily-loaded chunk is 643,376 bytes, and the top frame still fetched it on every page, so it ended up paying 814,343. Making the panel lazy fixed the iframe problem and left the original one intact. Only deleting the in-page UI fixed both.

The migration commit changed 220 files: 6,242 insertions against 8,377 deletions, with 28 files deleted outright. Non-test source fell 23% – 11,622 lines to 8,903 – while test code grew by half, 3,731 lines to 5,534.

Things that stopped existing entirely: the hand-written rem-to-px plugin and the shadow-DOM Tailwind plugin beside it, the Vue app that used to mount into the page, the Netflix focus guard, the keyboard interception layer, the draggable trigger, auto-hide, corner placement, and five settings that are now cleared out of your saved configuration so they stop travelling between your devices.

What it cost, honestly

A post-mortem that only reports wins isn't one.

Voice Control is gone. It was a Pro feature. You held the on-page button to start it, it stopped on its own after a couple of seconds of silence, and it ran from inside the page – which is exactly what this release stops doing. A toolbar popup closes the moment you look away from it, so there is nowhere left for it to listen from. Everything it could set is still reachable from the popup, the shortcuts and the gestures, but the feature itself is not coming back. If you subscribed for it specifically, talk to us from your dashboard and we'll sort you out.

The companion button is gone, and some people genuinely liked it. It had been off by default since June, but "off by default" and "deleted" are different things, and we removed the option rather than carrying it.

The content script got slightly bigger – 89,929 bytes in 4.1.1 to 94,758 in 4.2.0, up 5.4%. Two surfaces couldn't disappear, they could only move. Fullscreen still needs feedback, so a small confirmation pill now draws over the video when a shortcut or gesture changes something. And the Pro letterbox measure tool needs you to drag bars across a video, which the popup can't survive – so the popup deliberately closes itself and hands the drag to the page. Both still use a shadow root, and both still rely on the same trick the old panel did to paint over a fullscreen or modal player. The mechanism survived; what died is the 656 KB of application that used to ride along with it. The constraint needed the 4,829 bytes the content script grew by.

Fourteen sites still need bespoke CSS, in 97 lines of stylesheet. MLB, YouTube, Channel 4, ZDF, Canal+, Crunchyroll, LinkedIn and others all break the assumption that a video fills the box its player gave it. Moving our UI out of the page didn't fix that, because that was never the UI's fault.

Your permissions did not change. The video engine still runs in every frame – it has to, since that's where the videos are. We didn't reduce what UltraWideo is allowed to see. We reduced what it does with the invitation.

If you build extensions, the actual lesson

The rule we'd write on the wall now: put your UI in the browser's popup unless you can name the specific thing that makes it impossible – and then check whether that thing is worth what it costs on every page.

For us the impossible thing was real. A popup over an element-fullscreen video doesn't exist and won't. But we let one genuine constraint justify an architecture that also carried a Vue app, a stylesheet, a shadow root, two build plugins and a focus-restoring handler into every iframe on the internet. The constraint needed 4,829 bytes. We gave it 656,277, and spent four months maintaining the difference.

The other lesson is cheaper: the browser's popup is boring, and boring is the feature. It can't be covered by a site's layout, pushed around by its CSS, blamed for its bugs, or caught in its CAPTCHA. Every hour we spent fighting YouTube's root font size and Netflix's focus handling was an hour not spent on making video fill screens.

FAQ

Do browser extensions really slow down Chrome?

Yes, though how much varies enormously. The cost comes from content scripts – code injected into pages before you interact with the extension. A lightweight extension adds a few milliseconds; a heavy one can add seconds to page load. The number of extensions matters less than what each one injects and whether it runs in every frame.

How do I find which extension is slowing down my browser?

Open your browser's task manager and sort by CPU or memory – extensions appear as their own rows. In Chrome on Windows and Linux that's Shift + Esc; on macOS it's Window → Task Manager. Firefox has its own at about:performance. To confirm which one it is, disable them in batches rather than one at a time; halving the list finds the culprit faster.

Can an extension cause video to stutter or drop frames?

It can. Anything doing per-frame work on or near the video – overlays, filters, canvas effects, WebGL – competes with the player for the same budget. Extensions that leak GPU contexts can also cause the browser to evict the page's own, which looks like the site breaking rather than the extension.

Why do extensions run on every page instead of only when I click them?

Because clicking the icon happens too late for most features. An extension that modifies pages has to be present before you'd notice it's missing. The trade-off is that the code runs everywhere, including on the vast majority of pages where you'll never use it – which is exactly the trap we fell into.

Where did the UltraWideo floating panel go?

It was removed in 4.2.0. Click the UltraWideo icon in your toolbar, or press Alt + Shift + V (Ctrl + Cmd + V on macOS). If you can't see the icon, open your browser's extensions menu – the puzzle-piece – and pin UltraWideo. Nearly everything inside is unchanged: the same tabs, video modes, scale, pan, per-site scopes and preferences. What went with the button are the three settings that only existed to configure it – Popup Trigger, Interface Placement and Auto-hide trigger – and Voice Control.

Does the popup still work in fullscreen?

Not when the video itself is in fullscreen – and no extension's popup does, because browsers don't allow it there. In browser fullscreen (F11) it opens normally, since the toolbar reveals on hover. When a video is fullscreen the keyboard shortcuts and gestures are the interface, and a small pill appears over the video confirming what changed.

Did any of this change how my videos look?

The modes themselves are untouched – normal, upscale, stretch and custom compute exactly the same transform they always did, and scale and pan resolve the same way. A few things around them did move. Voice Control (Pro) is gone. The Pro letterbox Measure overlay is now drawn on the page instead of inside the panel, and a measured crop saves to the scope you actually picked rather than the most specific one that already existed. A page with an embedded player now keeps the embed's own per-site settings instead of having them overwritten on your first change.


If you've got an ultrawide, a dual-monitor setup or just a browser that letterboxes everything, UltraWideo is free and does the main job on its own – and as of 4.2.0 it does it without putting an interface on the pages you visit. What's left in the page is the video engine, a few kilobytes of stylesheet, and a pill that appears over the video for a second and a half when you change something. If you want per-site memory, cloud sync and ambient light on top, Pro is where those live. And if you're here because black bars on your ultrawide are the actual problem, start there instead.