Summary
The reported issue—severe lag and visual artifacts during window resize when hosting WebView2 in a WPF application—is a classic rendering pipeline synchronization problem. It occurs because the native WebView2 window (DirectComposition/Direct2D surface) and the WPF composition tree (Visual Studio Direct2D or GDI interop) operate on asynchronous rendering loops. When the WPF container is resized, the WebView2 surface invalidation does not align with the WPF render thread’s frame timing. This causes the browser to continue drawing to an outdated or partially re-sized surface buffer, resulting in tearing, flickering, and high CPU usage.
The lag is exacerbated by the WebView2CompositionControl. Unlike the standard WebView2 control (which hosts a Win32 window handle), the composition control is designed for better visual integration in WPF but imposes stricter requirements on layout stability and render thread synchronization.
Root Cause
The root cause is a mismatch between WPF’s Retained Mode rendering and WebView2’s Immediate Mode rendering.
- WPF Retained Mode: WPF uses a high-level retained mode graphics system. It keeps a list of visual objects and renders them on a dedicated render thread. When a window resizes, WPF triggers a layout pass, invalidates the visual tree, and eventually reallocates the composition target surface.
- WebView2 Immediate Mode: WebView2 relies on the Chromium rendering engine, which uses direct rendering (Direct2D) to a swap chain. This is an immediate mode operation optimized for speed, not strict synchronization with external UI frameworks.
- The Disconnect: When you resize the WPF
Window, theWebView2CompositionControlreceives a resize event. However, the underlying WebView2 process (a separatemsedgewebview2.exeprocess) has its own render loop. The sequence of events usually looks like this:- WPF resizes the hosting control.
- WPF requests the WebView2 surface to resize.
- The browser process acknowledges the resize but continues rendering frames based on its previous buffer size for a few cycles due to thread scheduling latency.
- WPF attempts to composite the WebView2 surface into the visual tree while the browser is reallocating its swap chain.
- The result is a buffer mismatch, causing the “ghosting” or artifacts, and the high CPU load from constant reallocation attempts.
Furthermore, embedding the control inside a ScrollViewer adds an additional layer of layout transformation complexity, confusing the browser’s logic for calculating its coordinate space and hit-testing boundaries.
Why This Happens in Real Systems
This pattern is ubiquitous in hybrid UI architectures (e.g., Electron, UWP WebView2, WPF with DirectX interop).
- Process Isolation: WebView2 runs the rendering engine in a separate process (GPU Process) for security and stability. Inter-process communication (IPC) introduces latency. During a rapid resize (mouse dragging), the “main” UI thread sends resize commands faster than the GPU process can acknowledge and create new backing surfaces.
- Lack of Surface Locking: Standard WebView2 controls do not explicitly lock the bitmap surface during a resize operation in a way that WPF’s
CompositionTargetexpects. If WPF requests a bitmap while the browser is resizing it, it may read incomplete data or stale data. - DPI Scaling: In WPF, DPI scaling calculations can differ between the host OS and the embedded web content. If the DPI awareness isn’t perfectly aligned (e.g., WPF using Per-Monitor V2 and WebView2 defaulting to system), the renderer may constantly recalculate pixel density, adding to the lag.
Real-World Impact
- Perceived Performance: The application feels “heavy” and unresponsive. A resize that should be 60 FPS feels like 10-15 FPS, destroying the user experience.
- Visual Corruption: “Ghosting” (previous frame artifacts remaining on screen) breaks visual continuity. In a timeline application, this makes reading data impossible during interaction.
- Resource Exhaustion: Continuous resizing triggers repeated allocations of texture memory. This can lead to memory fragmentation and GPU memory pressure, potentially causing the browser process to crash or the entire application to hang if the memory spike is severe.
- Input Latency: Because the render thread is blocked waiting for buffers, UI thread inputs (like mouse moves during resize) may queue up, resulting in cursor lag.
Example or Code (if necessary and relevant)
To demonstrate the synchronization issue, we can visualize the lifecycle of a resize event. The following C# pseudo-code illustrates the race condition between WPF’s layout pass and the WebView2 update.
// This is conceptual code illustrating the race condition, not the actual WPF internals.
// Do not run this.
void OnWindowResize(object sender, SizeChangedEventArgs e)
{
// 1. WPF Layout pass invalidates the visual tree.
this.InvalidateVisual();
// 2. WPF begins re-rendering the composition tree.
// It calculates the new bounds for the WebView2 control.
// 3. We attempt to sync the WebView2 size.
// PROBLEM: This is an async call across the process boundary.
// Even if awaited, the GPU process might be in the middle of a frame.
if (WebView != null)
{
// This command goes to msedgewebview2.exe
// The browser might be rendering a frame based on the *old* size
// while this command is in flight.
WebView.CoreWebView2Controller.IsVisible = false; // Hiding reduces flicker temporarily
ResizeWebView(e.NewSize);
WebView.CoreWebView2Controller.IsVisible = true;
}
// 4. The browser receives the resize, allocates a new swap chain.
// Meanwhile, WPF might try to draw the *old* surface on the *new* geometry.
// Result: Artifacts (stretching/squashing of the last frame).
}
How Senior Engineers Fix It
Senior engineers approach this by decoupling the visual update from the logical resize and minimizing IPC overhead.
-
Debouncing Resize Events:
Do not resize the WebView2 on every pixel change of the WPF window. Instead, implement a timer-based debouncer. When the resize starts, suspend WebView2 updates. When the user releases the mouse (resize stops), trigger the resize. This drastically reduces the number of heavy IPC calls.Implementation Logic:
- On
SizeChangedevent, start/reset aDispatcherTimer(e.g., 150ms). - When the timer ticks (user pauses resizing), execute the WebView2 resize logic.
- On
-
Visual Hiding During Transition:
As seen in the pseudo-code, settingIsVisible = falseduring the resize operation prevents the browser from trying to render a partially valid surface.- Key Tactic: Call
Controller.IsVisible = falseimmediately on resize start. Perform the resize. CallController.IsVisible = trueon resize end.
- Key Tactic: Call
-
Optimized Composition Control Usage:
UseWebView2CompositionControlstrictly for visual integration, but ensure theScrollViewerlogic is handled correctly. TheScrollVieweroften interferes with the browser’s internal scroll (if content overflows). It is better to disable browser scrolling (overflow: hiddenin CSS) and let WPF handle the scrolling via theScrollViewer, or vice versa, but never both simultaneously.- CSS Fix: Ensure
#myCanvasenforces strict sizing handled by JavaScript, not just CSS, to force the browser to commit a render frame immediately upon resize.
- CSS Fix: Ensure
-
Force Rendering Loop Integration:
Instead of letting the browser auto-render, utilizeCoreWebView2Controller.FrameRequested(if available in the specific version) or manually trigger rendering through WPF’sCompositionTarget.Renderingevent to hint the browser to draw a frame only when WPF is ready to composite it. However, for simple apps, the debouncing approach is the most robust and least intrusive.
Why Juniors Miss It
- Assumption of Single-Threaded UI: Juniors often assume UI updates are atomic. They don’t realize that
Window_Resizein WPF and the browser’sOnPaintare distinct, asynchronous processes running on different threads (UI thread vs. GPU process). - Focus on Logic, Not Timing: The code provided by the junior developer focuses entirely on what to draw (the grid), not when to draw it. They miss that
resizeis an event stream, not a discrete state. - Underestimating IPC Cost: They treat resizing the native control like resizing a standard WPF rectangle. They fail to account for the latency of marshaling data across the WPF process boundary to the
msedgewebview2.exeprocess. - Lack of Profiling: Without tools like
msedge.exe --enable-features=msEdgeDevToolsWdpor WPF performance counters, the source of the lag (render thread vs. main thread) is invisible. They often blame “WebView2 being slow” rather than their implementation strategy.