Skip to content

Offscreen rendering fps (Tao backend) #57

Description

@mtpdog

Overall, the new WebView with the Tao backend is very good!

However, we’ve noticed a noticeable performance degradation compared to previous versions (at least on Mac). It’s not as smooth as before, especially in WebGL games or maps. It definitely no longer renders at 120 FPS.

Is this caused by offscreen rendering costs, or are there internal settings that limit the frame rate to around 30 FPS? If so, could the max_fps API be made public?

Activity

  1. self-assigned this
    on Aug 29, 2026
  2. kdroidFilter commented on Aug 30, 2026

    @kdroidFilter
    Collaborator

    Thanks for the detailed report. Short answer: there is no offscreen rendering and no frame-rate cap in the library, so there is no max_fps to expose — but I could not reproduce the slowdown either, so I would like your numbers before closing this.

    What the backend actually does

    The desktop backend embeds a real native view, it never renders the page into a bitmap:

    • macOS: the WKWebView NSView is a subview of the Tao window, below the Compose Metal layer, which punches a transparent hole over the WebView rect
    • Linux: the WebKit2GTK widget is reparented into Tao's content widget
    • Windows: WebView2 runs as a DirectComposition visual composited by DWM

    No backend contains frame-rate logic (grep for fps/frame/vsync/timer over webview-compose/src/jvmMain/native/** is empty). Pacing is the platform compositor's job.

    Measurements (macOS, M4, 90 Hz display, Nucleus Tao 2.5.5)

    Embedded WebView vs. the same page in a bare WKWebView in a plain NSWindow:

    Workload Embedded (Tao NativeView) Bare WKWebView
    Canvas 2D rAF 90 fps 90 fps
    WebGL (light) 90 fps 90 fps
    WebGL, GPU-bound shader 31–34 fps 32–35 fps

    Compose rendered at 90 fps in the same window at the same time. Blending was the obvious suspect, so I measured it separately at full-screen size (2560×1040) with an animated Compose overlay composited over the WebView every frame — page fps / Compose fps, plus GPU utilization while both run at the display rate:

    Compose overlay Light page GPU-bound page GPU util (light page)
    none 90 / 90 34 / 90 23.4 %
    64 dp animated bar 90 / 90 34 / 90 —
    full-window translucent scrim 90 / 90 34 / 90 22.9 %
    full-window opaque surface 90 / 90 34 / 90 —
    bare WKWebView, opaque window 90 34 27.6 %

    No measurable blending penalty. But my display is 2560×1040 @1x at 90 Hz on a desktop M4 (~2.7 Mpx/frame); a Retina ProMotion laptop is ~7.7 Mpx at 120 Hz, roughly 4× the compositing bandwidth and no fan — so this does not rule out your case, it only says I cannot see it here.

    Could you run the diagnostics? (v1.0.2)

    The visual e2e suite now reports two rendering measurements — ./gradlew :e2e-desktop:run, then look for:

    Passed  R01  Rendering  requestAnimationFrame rate   90 fps
    Passed  R02  Rendering  WebGL renderer               Apple GPU
    

    What would help most, from your ProMotion Mac:

    1. The R01 / R02 values (R01 should be ~120 fps there; R02 must name a GPU — a software renderer would explain slow WebGL immediately)
    2. The same numbers in your own app, and whether Compose content overlaps the WebView
    3. The display refresh rate, and whether the window is on the internal Retina display or an external one

    One trap worth ruling out first: R01 is reported as Skipped when the document is hidden. Every engine suspends requestAnimationFrame for a fully covered or backgrounded window — I hit exactly that during this investigation and a bare WKWebView behaves identically. So keep the window in front while measuring.

    Also relevant if you are on battery: an opaque Compose overlay does not stop the WebView underneath — it keeps rendering at full speed behind it, so the GPU work is duplicated. Hide or dispose it rather than covering it.

    Details and how to reproduce: README section "Rendering model & frame rate" (#58).

  3. mtpdog commented on Aug 30, 2026

    @mtpdog
    Author

    Thank you for your detailed answer!
    Since this turned out to be a native view (perfect!) the cause is most likely in some other changes in our code. We didn't perform one-to-one comparison so this was just an impression (possibly wrong based on your explanation).

    You may close this issue for now I think. If we get some updates on this (or some actually measured numbers) I will reopen it. Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions