diff --git a/src/libs/UnreadIndicatorUpdater/updateUnread/index.ts b/src/libs/UnreadIndicatorUpdater/updateUnread/index.ts index 43218d4d880a..484a383b543e 100644 --- a/src/libs/UnreadIndicatorUpdater/updateUnread/index.ts +++ b/src/libs/UnreadIndicatorUpdater/updateUnread/index.ts @@ -1,5 +1,14 @@ /** * Web browsers have a tab title and favicon which can be updated to show there are unread comments + * + * FIX for #86679: Document title flickers when navigating + * + * Three changes from original: + * 1. Gate document.title = '' behind popstate flag — only clear on back/forward nav + * (the Chrome bug this works around). Normal navigation skips the blank intermediate. + * 2. Coalesce rapid calls via requestAnimationFrame — multiple triggers during a single + * navigation transition produce one frame-level write instead of N competing setTimeout(0)s. + * 3. Skip no-op writes — if the computed title matches current document.title, don't touch it. */ import CONFIG from '@src/CONFIG'; import type UpdateUnread from './types'; @@ -7,48 +16,83 @@ import type UpdateUnread from './types'; let unreadTotalCount = 0; let currentPageTitle = ''; +// rAF coalescing state +let pendingRAF: number | null = null; + +// Only set true by popstate (actual back/forward navigation), +// which is the only case the Chrome title-reversion workaround is needed for. +let isPopstateNavigation = false; + /** - * Set the current page-specific title (called by useDocumentTitle hook) - * @param title - The page-specific title + * Compute the target title from current state */ -function setPageTitle(title: string) { - currentPageTitle = title; - // Immediately update the document title when page title changes - updateDocumentTitle(); +function getTargetTitle(): string { + const hasUnread = unreadTotalCount !== 0; + const baseTitle = currentPageTitle || CONFIG.SITE_TITLE; + return hasUnread ? `(${unreadTotalCount}) ${baseTitle}` : baseTitle; } /** - * Update the actual document title and favicon + * Commit title + favicon in a single rAF callback. + * Multiple scheduleCommit() calls within the same frame produce one write. */ -function updateDocumentTitle() { - const hasUnread = unreadTotalCount !== 0; - // This setTimeout is required because due to how react rendering messes with the DOM, the document title can't be modified synchronously, and we must wait until all JS is done - // running before setting the title. - setTimeout(() => { - // There is a Chrome browser bug that causes the title to revert back to the previous when we are navigating back. Setting the title to an empty string - // seems to improve this issue. +function commitTitleAndFavicon() { + pendingRAF = null; + const target = getTargetTitle(); + + // Only apply the Chrome back-nav workaround when we actually came from popstate + if (isPopstateNavigation) { document.title = ''; + isPopstateNavigation = false; + } + + // Skip no-op writes — prevents unnecessary paint cycles + if (target !== document.title) { + document.title = target; + } + + // Update favicon + const hasUnread = unreadTotalCount !== 0; + const favicon = document.getElementById('favicon'); + if (favicon instanceof HTMLLinkElement) { + favicon.href = hasUnread ? CONFIG.FAVICON.UNREAD : CONFIG.FAVICON.DEFAULT; + } +} - // Use page-specific title if available, otherwise use the default SITE_TITLE - const baseTitle = currentPageTitle || CONFIG.SITE_TITLE; - document.title = hasUnread ? `(${unreadTotalCount}) ${baseTitle}` : baseTitle; +/** + * Schedule a single rAF-coalesced title commit. + * If already scheduled, the pending one is cancelled — only the latest state wins. + */ +function scheduleCommit() { + if (pendingRAF !== null) { + cancelAnimationFrame(pendingRAF); + } + pendingRAF = requestAnimationFrame(commitTitleAndFavicon); +} - const favicon = document.getElementById('favicon'); - if (favicon instanceof HTMLLinkElement) { - favicon.href = hasUnread ? CONFIG.FAVICON.UNREAD : CONFIG.FAVICON.DEFAULT; - } - }, 0); +/** + * Set the current page-specific title (called by useDocumentTitle hook) + * @param title - The page-specific title + */ +function setPageTitle(title: string) { + currentPageTitle = title; + scheduleCommit(); } /** - * Set the page title on web + * Update unread count and schedule a title commit */ const updateUnread: UpdateUnread = (totalCount) => { unreadTotalCount = totalCount; - updateDocumentTitle(); + scheduleCommit(); }; +/** + * Popstate handler — only this path triggers the Chrome workaround. + * Non-route-changing popstates are harmless (no-op if title unchanged). + */ window.addEventListener('popstate', () => { + isPopstateNavigation = true; updateUnread(unreadTotalCount); });