Note: I didn't use AI to write this. I originally had it try to analyze the profiler GC logs, but there wasn't enough information for it to be conclusive (see below).
This information is accurate as of 2026-10-29 on Firefox 158.0.1, Fedora 44.
I didn't test this on Chrome.
TLDR: I'm pretty sure it's their DataDog RUM, plus, to a much lesser degree, global keybindings. See below for some TamperMonkey user scripts you can try.
For the last little while (at least a few weeks, at most a year, don't remember exactly when it got this bad), I've been having performance issues on GitHub. It's been stuttery in general, with multi-second periods of jank every now and then, and with memory usage steadily increasing over time.
With long-running sessions and ~50 tabs, I've seen it go into tens of gigabytes of memory usage, while pinning the corresponding process at 100% CPU (or close) for extended periods of time.
A few times, I've even had it get so out of hand that the GC couldn't keep up and it ended up OOMing.
I finally ended up running the profiler to try and track it down...
This screenshot is just one example from today, a few minutes after opening a GitHub page, of runaway memory and GC jank on a GitHub page while doing nothing but scrolling around on it.
Before I did this investigation, I had half-heartedly searched for causes.
I suspected it was Bitwarden at first given its history of causing performance issues in pages (t still does, it's just nothing compared to this). For example, as of the current version of the Bitwarden extension (2026.10.0), even when autofill is off, it scans the entire page multiple times in quite an inefficient manner (it keeps re-querying backwards to find headers on elements rather than doing it all in a few passes). But, after disabling Bitwarden, I realized that it's an issue with GitHub or Firefox.
I then suspected it may be changes in Firefox 157/158 given that there was anecdotal evidence of this on various Reddit threads, but everyone attributed it to a different cause and different fix (1, 2)... and, of the people who provided more detail, GitHub was a common factor.
Looking specifically at GitHub and Firefox, I found a few threads (1, 2, etc) and some stuff on bugzilla (1, 2). The second bugzilla bug claimed it was a GitHub issue with selectionchange, which was claimed to have later been fixed, but it definitely isn't the only thing.
I tried passing Claude the performance profile I captured, and GC/CC logs from about:memory, but there wasn't enough information for it to be conclusive.
This was partly caused by Firefox dropping stuff due to the sheer volume (one of the concise CC logs for a single process was already multiple gigabytes, and each GC was collecting millions of objects), but also because I hadn't found a reliable way to reproduce it on-demand.
I abandoned this approach after a bit.
I ultimately ended up making educated guesses myself based on the JS traces during the periods of increasing memory and jank, then trying them out. I started with two obvious things which stood out in the JS allocation flame graph during an extended period of jank and memory usage increase (this particular one was while typing out the body of a GitHub release): global keybindings, and the DataDog RUM.
I also suspect this may be related to or exacerbated by the client-side navigation keeping objects (elements, event handlers, closures, etc) alive.
I decided to start digging into the referenced JS chunks to determine the plausiblity of these theories.
TLDR: Even if you have "Character keys" disabled, every keypress (including in text fields) results in rebuilding the keybinding map from scratch, and triggering multple page layouts while searching for modals.
GlobalCommands is in chunk gw-4bf343b417f9ec19.js... this is the relevant snippet (with a few additional comments):
const GlobalCommands = ({ commands }) => {
const ref = useRef(null)
const onTrigger = useEffectEvent(handleTriggerEvent) // k
const handle = useKeybindingHandler(runGlobalCommand) // mF.r(h)
const onKeyDown = useEffectEvent(e => {
const modal = topmostModalCached(e) // rG.ck, cached per event
if ((modal && !modal.contains(ref.current)) || alreadySeen(e)) return
handle(e, getKeybindingMap(globalCommandIds())) // RIB.fg(lS.hd())
})
useKeybindingConflictWarnings("global", commands) // gt.A
useRegisterCommands("global", commands) // lS.JT
useEffect(() => {
const ac = new AbortController()
document.addEventListener("keydown", onKeyDown, { signal: ac.signal })
document.addEventListener("ui-command-trigger", onTrigger, { signal: ac.signal })
return () => ac.abort()
}, [])
return <div ref={ref} className="d-none" />
}
So for each keydown ANYWHERE on the page (including inside textboxes):
- All mounted instances of
GlobalCommandsget called. - The first instance that passes the modal check does most of the logic:
topmostModal()runsquerySelectorAll('dialog:modal, [role="dialog"][aria-modal="true"]')over the whole document, then readsclientHeightrecursively on each match.- This is already definitely relevant since reading
clientHeightforces a layout.
- This is already definitely relevant since reading
- This is already definitely relevant since reading
getKeybindingMap()is rebuilt from scratch.- This is somewhat expensive since it involves, for each registered command, a feature-flag lookup, regexes on the binding, and a parse into a
Map.
- This is somewhat expensive since it involves, for each registered command, a feature-flag lookup, regexes on the binding, and a parse into a
- This is somewhat expensive since it involves, for each registered command, a feature-flag lookup, regexes on the binding, and a parse into a
- Then, after doing all that work, it checks if keyboard shortcuts are enabled and that the user isn't within a text input.
- An analytics event gets sent when a command matches.
Nevertheless, this isn't enough to cause the level of jankiness and memory usage I've been running into.
Next, I looked at DataDog. I didn't bother to figure out exactly which part was causing it since it's obvious it's one of the main issues either way.
Ironically, one of the things GitHub uses DataDog for is to track performance, but it's DataDog itself likely causing those performance issues.
This the current slightly abbreviated <script id="datadog-rum-config">:
datadogRum.init({
applicationId: config.applicationId,
clientToken: config.clientToken,
site: config.site,
service: config.service,
env: config.env,
version: document.head.querySelector('meta[name="release"]')?.content ?? "",
proxy: ({ path, parameters, subdomain }) => proxyUrl(config.proxyUrl, path, parameters, subdomain), // ?ddforward=...
workerUrl: "/assets-cdn/worker/chunk-m-*.js",
...(config.remoteConfiguration ? { remoteConfiguration: { id: config.remoteConfiguration.id }, remoteConfigurationProxy: ... } : {}),
profilingSampleRate: config.profilingSampleRate,
sessionSampleRate: config.sessionSampleRate,
sessionReplaySampleRate: 0,
enableExperimentalFeatures: ["feature_flags"],
compressIntakeRequests: true,
telemetrySampleRate: 0,
defaultPrivacyLevel: "mask-user-input",
useSecureSessionCookie: location.protocol === "https:",
trackAnonymousUser: false,
trackViewsManually: true,
trackUserInteractions: true,
enablePrivacyForActionName: true,
trackResources: true,
allowedTracingUrls: [{ match: sameOriginExceptRaw, propagatorTypes: ["tracecontext"] }],
allowedGraphQlUrls: [{ match: isGraphQL, trackPayload: false, trackResponseErrors: false }],
trackLongTasks: true,
beforeSend: scrubUrlsAndDropCachedImages,
plugins: [reactPlugin({ router: false })],
silentMultipleInit: true,
});
The trackViewsManually is used to track views on the client-side navigation GitHub does.
For me, while logged in, on a repository page, the sessionSampleRate was 100%, and the profilingSampleRate was 5%, with no remoteConfiguration (see my userscript below)
Also, interestingly, I don't see DataDog listed as a subprocessor in the GitHub docs (https://docs.github.com/en/site-policy/privacy-policies/github-subprocessors)...
I wrote some TamperMonkey userscripts to block the problematic thingies.
Restart firefox, or at least kill all the GitHub processes in about:performance after installing these. I also recommend disabling Bitwarden and other heavy extensions to avoid confounding things.
We can match it based on an addEventListener for a document-level keydown event preceding the custom ui-command-trigger event.
// ==UserScript==
// @name GitHub: remove GlobalCommands keydown listeners
// @namespace https://pgaskin.net
// @supportURL https://gist.github.com/pgaskin/9f1445e8261f488a50498ea1b98272e8
// @updateURL https://gist.github.com/pgaskin/9f1445e8261f488a50498ea1b98272e8/raw/github-neuter-global-keybindings.user.js
// @version 1.0
// @match https://github.com/*
// @run-at document-start
// @grant none
// @sandbox JavaScript
// @inject-into page
// ==/UserScript==
(() => {
"use strict";
const add = EventTarget.prototype.addEventListener;
const remove = EventTarget.prototype.removeEventListener;
const pending = new WeakMap();
EventTarget.prototype.addEventListener = function addEventListener(type, listener, options) {
if (this === document && options?.signal instanceof AbortSignal) {
if (type === "keydown") {
pending.set(options.signal, listener);
} else if (type === "ui-command-trigger") {
const kd = pending.get(options.signal);
if (kd) {
pending.delete(options.signal);
remove.call(document, "keydown", kd);
}
}
}
return add.call(this, type, listener, options);
}
})();
The proper fix would be for GitHub to avoid registering the events in the first place if global keybindings are disabled, and to cache the keybinding map, only rebuilding it when it changes.
Regardless, this is only a small (but still noticeable) portion of the jank. GitHub should still fix it, though, as it's a relatively small change.
// ==UserScript==
// @name GitHub: log/disable Datadog RUM
// @namespace https://pgaskin.net
// @supportURL https://gist.github.com/pgaskin/9f1445e8261f488a50498ea1b98272e8
// @updateURL https://gist.github.com/pgaskin/9f1445e8261f488a50498ea1b98272e8/raw/github-neuter-datadog.user.js
// @version 1.0
// @match https://github.com/*
// @run-at document-start
// @grant none
// ==/UserScript==
(() => {
"use strict";
// set this to true to log the datadog params, but still let it run
const LOG_ONLY = false;
// setupDatadogRum currently won't import the chunks if #datadog-rum-config is missing
// note: it's retried every call, so we need to keep observing across client-side navigations
let last;
const seen = new WeakSet();
const hook = () => {
const el = document.getElementById("datadog-rum-config");
if (!el || seen.has(el)) {
return;
}
seen.add(el);
if (!LOG_ONLY) {
el.remove();
}
try {
const {
sessionSampleRate,
profilingSampleRate,
remoteConfiguration,
} = JSON.parse(el.textContent);
const msg = `${LOG_ONLY ? "found" : "removed"} datadog-rum-config (sessionSampleRate=${sessionSampleRate}, profilingSampleRate=${profilingSampleRate}, remoteConfiguration=${remoteConfiguration?.id ?? "none"})`;
if (msg !== last) {
console.warn("[github-datadog]", (last = msg));
}
} catch (err) {
console.warn(`[github-datadog] ${LOG_ONLY ? "found" : "removed"} datadog-rum-config, but couldn't parse it`, err);
}
};
new MutationObserver(hook).observe(document, {
childList: true,
subtree: true,
});
hook();
// I had claude write this part for me based on the published SDK (v7) so I didn't have to reverse it myself
for (const name of ["_dd_s_v2", "_dd_s"]) {
const raw = document.cookie.split("; ").find((c) => c.startsWith(name + "="))?.slice(name.length + 1);
if (!raw) {
console.warn(`[github-datadog] ${name}: not set`);
continue;
}
const s = Object.fromEntries(decodeURIComponent(raw).split("&").map((kv) => kv.split("=", 2)));
const expire = Number(s.expire);
const live = !!s.id && !s.isExpired && expire > Date.now();
console.warn(
`[github-datadog] ${name}: ${live ? "live RUM session" : "no live session"}` +
(s.id ? `, created ${new Date(Number(s.created)).toISOString()}, expires ${new Date(expire).toISOString()}` : "") +
(s.forcedReplay ? ", forced replay" : ""),
);
}
})();
You can set LOG_ONLY to true if you want to confirm that it's enabled for you by default.
Note that you may need to filter the js console since it currently spams (unrelated) Referrer Policy: Ignoring the less restricted referrer policy “no-referrer-when-downgrade” for the cross-site request: https://github.githubassets.com/assets/... lines at info level.
I have only been running this for a few hours (about half of it while typing out the first draft of this), but the memory usage is already significantly lower (though still somewhat high), CPU usage remains low, and I haven't had any jankiness yet...
I'll probably open a GitHub Support ticket later if it continues to work well.