Skip to content

Fix flash of unstyled content (FOUC) on page load - #833

Open
ankit wants to merge 1 commit into
mainfrom
worktree-css-fouc-fix
Open

Fix flash of unstyled content (FOUC) on page load#833
ankit wants to merge 1 commit into
mainfrom
worktree-css-fouc-fix

Conversation

@ankit

@ankit ankit commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Custom styles could briefly fail to apply on page load, causing a flash of unstyled content (FOUC) before Stylebot's CSS kicked in.

Root cause: the inject-css content script runs at document_start, but it still had to chrome.runtime.sendMessage the background service worker and wait for it to read chrome.storage.local before the <style> tag could be inserted. In Manifest V3, the service worker can be asleep between uses, and waking it up added enough latency to produce a visible flash — worse, the script only detected a failed lookup and retried every 300ms, up to 10 times, if the service worker wasn't ready in time.

Fix: the content script now reads chrome.storage.local directly and applies the existing URL-matching logic itself, entirely skipping the message round trip to the background page. chrome.storage is handled by the browser process, not the extension's service worker, so styles apply immediately regardless of whether the service worker is awake. The old retry-polling fallback is no longer needed and has been removed.

Changes

  • src/inject-css/index.ts: read chrome.storage.local directly instead of messaging the background page; drop the retry-polling fallback.
  • src/styles/ (new): extracted the pure getStylesForPage URL-matching logic and BackgroundPageUtils out of src/background/ into a shared package (@stylebot/styles) so both the background page and the content script can use it without the content script depending on background-only code.
  • src/background/listeners.ts: badge/icon updates now happen in the existing chrome.tabs.onUpdated listener, since they used to piggyback on the message the content script no longer sends.
  • Removed the GetStylesForIframe message type/handler, which was only ever sent by the old content script and is now dead code.
  • webpack.config.js: added a resolve alias for the new @stylebot/styles package.

Test plan

  • npx jest — all 76 tests pass
  • npx eslint --resolve-plugins-relative-to . src — no lint errors on touched files
  • yarn build compiles successfully
  • Manually loaded the built extension in Chrome and confirmed custom CSS applies immediately on page load/reload with no visible flash

@ankit ankit self-assigned this Sep 2, 2026
The inject-css content script ran at document_start, but still had to
message the background service worker and wait for it to read
chrome.storage.local before it could insert the <style> tag. When the
MV3 service worker was cold, waking it up added enough latency to
cause a visible flash of unstyled content before styles applied.

The content script now reads chrome.storage.local directly (handled
by the browser process, not the service worker) and applies the
existing URL-matching logic itself, removing the message round trip
and the 300ms x 10 retry-polling fallback that was compensating for it.

Extracted the pure getStylesForPage/BackgroundPageUtils matching logic
into a new shared src/styles package so both the background page and
the content script can use it. Moved badge-icon updates into the
existing tabs.onUpdated listener since they used to piggyback on the
now-removed message, and removed the GetStylesForIframe message type,
which was only ever sent by the old content script.
@ankit
ankit force-pushed the worktree-css-fouc-fix branch from b818076 to f182dea Compare September 2, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant