Skip to content

fix: eliminate dark-mode flash in MonochromeUI/NewBootstrap#992

Merged
PythonSmall-Q merged 15 commits into
devfrom
claude/elegant-volta-u94vc7
Jun 20, 2026
Merged

fix: eliminate dark-mode flash in MonochromeUI/NewBootstrap#992
PythonSmall-Q merged 15 commits into
devfrom
claude/elegant-volta-u94vc7

Conversation

@boomzero

@boomzero boomzero commented Jun 14, 2026

Copy link
Copy Markdown
Member

Closes #343

What does this PR aim to accomplish?

The MonochromeUI dark mode flashed a light-coloured page on every load. This happened because Bootstrap 5.3.3 was fetched from CDN at document-idle — the page painted with the old Bootstrap 3 light theme first, then snapped to dark once the CDN response arrived.

How does this PR accomplish the above?

Root cause: Bootstrap CSS was loaded asynchronously from CDN inside the main async IIFE, which runs after the page has already painted with the site's original Bootstrap 3 stylesheet.

Fix — three cooperating layers:

  1. @run-at document-start + @resource BootstrapCSS
    Bootstrap 5.3.3 CSS is now declared as a @resource (cached by the userscript manager on install) and a @require for its JS. An early synchronous IIFE runs before the first paint and:

    • reads UserScript-Setting-Theme from localStorage and sets data-bs-theme on <html> immediately
    • injects Bootstrap 5.3.3 CSS from the local cache (no network round-trip)
    • injects the correct skin CSS (MonochromeSkinCSS or NewBootstrapSkinCSS, plus AddAnimation/AddColorText if enabled)
    • hides the page with html { opacity: 0 !important } to prevent any CLS while the old stylesheets are still in the CSSOM
    • arms a MutationObserver to intercept old Bootstrap/theme <link> tags as they are inserted by the HTML parser
  2. IIFE-time cleanup (at DOMContentLoaded)
    The main async IIFE now waits for DOMContentLoaded at startup (needed since the script now runs at document-start). Immediately after that wait — before await fetch(loginpage.php) — it:

    • disconnects the MutationObserver
    • removes any old Bootstrap/theme <link> elements still in the DOM (catches anything the preload scanner fetched before the observer could intercept)
    • removes the opacity: 0 hide style, revealing the page in its correct final state
  3. Late cleanup fallback
    The original link-removal loop inside if (UtilityEnabled("NewBootstrap")) is kept as a belt-and-suspenders pass for edge cases.

First-install / cache-miss graceful degradation:
If GM_getResourceText("BootstrapCSS") returns null (resource not yet cached on first install), the early block exits without hiding the page or blocking stylesheets. The IIFE falls back to adding Bootstrap 5.3.3 CSS via CDN link (same as the old behaviour). After the first page load the resource is cached and all subsequent loads use the embedded path.

Other changes:

  • MonochromeSkinCSS and NewBootstrapSkinCSS extracted to top-level const so they are shared between the early block and the rest of the script (no duplication)
  • The Style element created at document-idle no longer re-injects the skin CSS (already done early); it is kept for the dynamic CSS additions appended later in the IIFE

Confirmation

  • I have read the contributor's guide
  • My code includes comments where the logic is non-obvious
  • I have tested my changes in a browser — Note: this was developed and iterated in a remote Claude Code session without direct browser access. The PR author confirmed behaviour at each iteration step. Final browser confirmation by a human reviewer is needed before merge.
  • I am willing to provide support for this contribution if needed
  • My contribution is compatible with GNU General Public License v3.0
  • No duplicate PRs exist for this change
  • This change provides value to the community (eliminates a jarring UX issue in dark mode)
  • I accept that this PR may be rejected
  • This is a freely given contribution
  • Tested in both new UI and classic UI — only the new UI (MonochromeUI + NewBootstrap) path was changed; the classic UI code path is untouched. When NewBootstrap is disabled the early block exits immediately and the script behaves identically to before.

claude added 7 commits June 14, 2026 07:38
Bootstrap 5.3.3 CSS was previously fetched from CDN at document-idle,
causing a flash of the old light-themed page before dark mode took effect.

Changes:
- Add @run-at document-start so an early shim can run before first paint
- Add @resource BootstrapCSS + @require Bootstrap JS bundle so both are
  cached by the userscript manager (no CDN round-trip on page load)
- Add @grant GM_getResourceText
- Early synchronous IIFE: reads saved theme/settings from localStorage,
  sets data-bs-theme on <html> immediately, injects Bootstrap CSS and the
  MonochromeUI or default skin CSS before the browser paints anything, and
  uses a MutationObserver to drop the page's own old Bootstrap/theme link
  tags before they are ever fetched
- Extract MonochromeSkinCSS and NewBootstrapSkinCSS to top-level consts so
  they are available both to the early block and to the late Style element
  (which still applies them at document-end for any dynamic page additions)
- Remove Bootstrap CSS + JS from the runtime CDN resources array
- Remove the now-redundant post-load link-removal loop, data-bs-theme
  assignment, and earlyStyle CSS-variable injection from the NewBootstrap block
- Add DOMContentLoaded wait at the start of the main async IIFE so all
  existing DOM-dependent code continues to work correctly under document-start

https://claude.ai/code/session_01B1RgyUvtsWWhS2hdNiUhZb
The early block now owns the MonochromeSkinCSS / NewBootstrapSkinCSS
injection. The Style element created at document-end is kept (other
code appends to it further down the IIFE), but it no longer re-sets
the skin CSS that was already applied synchronously at document-start.

https://claude.ai/code/session_01B1RgyUvtsWWhS2hdNiUhZb
If GM_getResourceText("BootstrapCSS") returns null (the @resource has
not been downloaded yet on first install or update), the early block was
still setting up the MutationObserver that blocked the page's original
Bootstrap CSS — leaving the page with no Bootstrap CSS at all.

Fix: bail out of the early block before the observer is set up when the
resource is unavailable, so the page's original stylesheets load as a
fallback (flash still happens, but the page renders correctly).

Also add a CDN fallback entry to the runtime resources array: on the
rare load where the @resource wasn't ready, Bootstrap 5.3.3 CSS is
fetched from CDN instead, maintaining the same behaviour as before this
PR. Subsequent page loads use the cached @resource and no CDN request.

https://claude.ai/code/session_01B1RgyUvtsWWhS2hdNiUhZb
The browser preload scanner starts fetching old Bootstrap/theme
stylesheets before the document-start MutationObserver can remove
the link elements. Once fetched, those stylesheets are applied and
override our early-injected Bootstrap 5 + skin CSS — causing the new
UI to render for a moment and then immediately revert.

Restoring the IIFE-time link removal loop fixes this: removing a
<link> from the DOM un-applies its stylesheet from the CSSOM
immediately, regardless of whether the preload scanner had already
fetched it. The two layers now work together:

  1. MutationObserver (document-start): best-effort early block that
     prevents old CSS from ever loading when the timing works out.
  2. Link removal loop (document-idle): guaranteed cleanup that
     removes any preload-scanner-fetched stylesheets after the DOM
     is ready, eliminating the revert.

https://claude.ai/code/session_01B1RgyUvtsWWhS2hdNiUhZb
The visible flash was caused by old Bootstrap 3 CSS sitting in the CSSOM
from the moment DOMContentLoaded fired until the IIFE's link removal loop
ran — a window that includes a full loginpage.php network round-trip.

Move the definitive link removal into the early block's DOMContentLoaded
handler so it fires at the earliest possible moment after the DOM is
ready, before any async work in the main IIFE. The observer is also
disconnected at the same time.

The IIFE-level link removal loop is kept as a belt-and-suspenders
fallback for edge cases (e.g. early block disabled due to cache miss).

https://claude.ai/code/session_01B1RgyUvtsWWhS2hdNiUhZb
No JavaScript approach can reliably prevent the browser preload scanner
from fetching and visibly applying old Bootstrap CSS before DOMContentLoaded.
Attempting to evict stylesheets at DOMContentLoaded still leaves a window
where old CSS is visible, causing CLS.

Instead, hide the page immediately at document-start with
  html { opacity: 0 !important; }
so the user never sees the intermediate broken state. The page is revealed
in the DOMContentLoaded handler, after old stylesheet links have been
removed and the correct Bootstrap 5 + skin CSS is already in place.
A 3-second safety-net timeout ensures the page is always revealed even
if something unexpected prevents DOMContentLoaded from firing.

This is the standard FOUC prevention pattern. The hidden window is just
HTML parse time (~50-150 ms for a server-rendered page), after which the
user sees the correct final state with zero CLS.

The hide style is only injected when the @resource is available
(_earlyBootstrapInjected = true), so the fallback path (cache miss on
first install) continues to show the page immediately.

https://claude.ai/code/session_01B1RgyUvtsWWhS2hdNiUhZb
The DOMContentLoaded listener registered inside the early IIFE's
sandbox context was not firing reliably, causing the opacity:0 FOUC
prevention style to only be removed by the 3-second safety-net
timeout — resulting in a mandatory 3 s blank page on every load.

Fix: expose the FOUC style element and MutationObserver via top-level
variables (_foucStyle, _earlyObs) and perform the reveal + link
cleanup at the top of the main async IIFE, immediately after its
DOMContentLoaded wait. The IIFE's own DOMContentLoaded mechanism is
proven to work, so the reveal is now reliable.

The early block still hides the page at document-start and arms the
MutationObserver for best-effort link interception; the IIFE takes
care of teardown and the final CSSOM cleanup at DOMContentLoaded time,
before the loginpage.php fetch begins.

https://claude.ai/code/session_01B1RgyUvtsWWhS2hdNiUhZb

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @boomzero, your pull request is larger than the review limit of 150000 diff characters

@hendragon-bot hendragon-bot Bot added the user-script This issue or pull request is related to the main user script label Jun 14, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 14, 2026

Copy link
Copy Markdown

Deploying xmoj-script-dev-channel with  Cloudflare Pages  Cloudflare Pages

Latest commit: 69a65f5
Status: ✅  Deploy successful!
Preview URL: https://14cdc70c.xmoj-script-dev-channel.pages.dev
Branch Preview URL: https://claude-elegant-volta-u94vc7.xmoj-script-dev-channel.pages.dev

View logs

@boomzero

Copy link
Copy Markdown
Member Author

This, btw does not completely fix the issue, but greatly alleviates it

@boomzero boomzero changed the title Refactor and enhance XMOJ.user.js with improved features fix page flashes in dark mode Jun 14, 2026
@boomzero

Copy link
Copy Markdown
Member Author

Sorry PR description is trash

@boomzero

Copy link
Copy Markdown
Member Author

But fixes page flashes in dark mode

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da0075c70e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread XMOJ.user.js
@boomzero boomzero changed the title fix page flashes in dark mode fix: eliminate dark-mode flash in MonochromeUI/NewBootstrap Jun 14, 2026
github-actions Bot and others added 5 commits June 14, 2026 08:36
When GM_getResourceText("BootstrapCSS") returns null (first install or
@resource not yet cached), the early block exits before injecting
MonochromeSkinCSS/NewBootstrapSkinCSS. The Style element in the IIFE
now injects the skin CSS (plus AddAnimation/AddColorText overrides)
whenever _earlyBootstrapInjected is false, so the skin is always
applied regardless of cache state.

https://claude.ai/code/session_01B1RgyUvtsWWhS2hdNiUhZb
@boomzero boomzero requested a review from PythonSmall-Q June 14, 2026 08:52
@PythonSmall-Q

PythonSmall-Q commented Jun 14, 2026 via email

Copy link
Copy Markdown
Member

@PythonSmall-Q PythonSmall-Q left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it still flashes

@boomzero

Copy link
Copy Markdown
Member Author

It's better though, and sometimes doesn't flash

@PythonSmall-Q PythonSmall-Q merged commit 2ff4536 into dev Jun 20, 2026
9 of 10 checks passed
@PythonSmall-Q PythonSmall-Q deleted the claude/elegant-volta-u94vc7 branch June 20, 2026 09:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XXL user-script This issue or pull request is related to the main user script

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants