Skip to content

Enhanced navigation in .NET10: scroll happens before content renders, causing visual flash #64015

@GiampaoloGabba

Description

@GiampaoloGabba

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Description

Since .NET 10 (PR #60296), Blazor's enhanced navigation automatically resets scroll when navigating between different pages. However, the scroll happens before the new content is rendered, causing a visual flash where users see old page content during the scroll animation.

Problem

When navigating from Page A to Page B:

  1. User clicks link
  2. Scroll starts immediately
  3. While scrolling, old page content (Page A) is still visible
  4. New content (Page B) suddenly pops in during/after scroll
  5. Visual flash/glitch ruins the user experience

This makes enhanced navigation feel janky compared to traditional navigation.

Steps to Reproduce

  1. Create two pages with different routes and long content
  2. Scroll down on Page 1
  3. Click link to Page 2
  4. Observe: Scroll starts while Page 1 content is still visible, then Page 2 content suddenly appears

Example:

@page "/page1"
<h1>Page 1</h1>
<a href="/page2">Go to Page 2</a>
<div style="height: 2000px">Long content...</div>
@page "/page2"
<h1>Page 2</h1>
<a href="/page1">Go to Page 1</a>
<div style="height: 2000px">Long content...</div>

Video

scroll-bug.mp4

Note: In the video, browser network is throttled to 4G to make the issue more visible. The slower the connection, the more noticeable the flash becomes, but it occurs on all connection speeds.

Root Cause

Looking at PR #60296, the resetScrollIfNeeded() function is called synchronously during navigation, before the DOM update completes:

async function handleInternalNavigation(renderBatch, interceptedRouter) {
  if (shouldHandleEnhancedNav) {
    resetScrollIfNeeded(renderBatch); // ⚠️ Too early - DOM not updated yet
    const diffResult = await performInternalEnhancedNavigation(renderBatch);
    // ...
  }
}

The scroll runs before the browser paints the new content.

Proposed Fix

Delay scroll until after browser paints new content using requestAnimationFrame:

function resetScrollIfNeeded(renderBatch) {
  const shouldScroll = /* ... existing logic ... */;

  if (shouldScroll) {
    // Wait for browser to paint new content
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        window.scrollTo(0, 0);
      });
    });
  }
}

Double RAF ensures scroll happens after the next paint, when new content is visible.

Alternative: Provide API to disable automatic scroll

If fixing the timing is complex, at least provide an option to disable the automatic scroll behavior entirely:

Current Workaround

After trying multiple approaches (setTimeout, requestAnimationFrame, CSS hiding), we found only one ugly (but working) solution: intercept and block window.scrollTo during navigation.

let _originalScrollTo = null;

// Before navigation starts (enhancednavigationstart event)
if (!_originalScrollTo) {
  _originalScrollTo = window.scrollTo;
}

// Block Blazor's scrollTo during navigation
window.scrollTo = function() {
  console.log('Blocked premature scroll');
};

// After navigation completes (enhancedload event)
window.scrollTo = _originalScrollTo; // Restore original

// Now manually handle scroll as needed
window.scrollTo({ top: 0, behavior: 'smooth' });

This works but has significant downsides, its an ugly hack and you need to carefully manage your eventlistener to makee sure to apply scrollTo when needed.

Impact

Affects all Blazor SSR apps with enhanced navigation when:

  • Navigating between different routes
  • User is not at top of page
  • Pages have visually distinct content

This is the default behavior in .NET 10.

Environment

Related

Expected Behavior

No response

Steps To Reproduce

No response

Exceptions (if any)

No response

.NET Version

No response

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions