tauri-plugin-macos-fps 0.1.0

Tauri v2 plugin that unlocks >60fps on macOS by disabling WKWebView's frame rate cap
Documentation

tauri-plugin-macos-fps

Crates.io License Rust

Unlock >60fps on macOS for Tauri v2 apps. One line of code. 120Hz ProMotion, 144Hz+ external displays — your Tauri app renders at the display's native refresh rate.

macOS 26+ (Tahoe): Apple removed the 60fps cap entirely in macOS 26. WKWebView now renders at native refresh rate by default. This plugin is a no-op on macOS 26+ and is most useful for users on macOS 13–15 (Ventura through Sequoia) where the cap is still enforced.

Not App Store safe. This plugin uses WebKit's private _features API. Apps distributed through the Mac App Store will be rejected. This is not a limitation of the plugin — Apple provides no public API to control WKWebView's frame rate. We investigated every alternative. Read why.


The problem

On macOS 13–15, WKWebView caps requestAnimationFrame at 60fps — regardless of your display's actual refresh rate. Your MacBook Pro has a 120Hz ProMotion display? Your Tauri app is stuck at 60fps. Your external monitor runs at 144Hz or 240Hz? Still 60fps.

This is tracked in:

The Tauri maintainers said "WKWebView simply does not expose settings for that."

They were right about the public API. But there is a private one.

The fix

WebKit has an internal preference called PreferPageRenderingUpdatesNear60FPSEnabled (defaults to true). This plugin sets it to false via the private _features API on WKPreferences — the same mechanism Safari uses internally.

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_macos_fps::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

That's it. Every webview now renders at your display's native refresh rate.

Install

Add to your Tauri app's src-tauri/Cargo.toml:

[dependencies]
tauri-plugin-macos-fps = "0.1"

Minimum supported Rust version: 1.85.

Try it yourself

A test app is included to verify the fix on your Mac:

git clone https://github.com/userFRM/tauri-plugin-macos-fps.git
cd tauri-plugin-macos-fps/examples/fps-test

# Install Tauri CLI if you don't have it
cargo install tauri-cli --version "^2"

# Run the test app
cargo tauri dev

The app shows a real-time FPS counter with a large display, min/max/avg stats, a live graph, and a toggle button to switch between locked (60fps) and unlocked (native) in real time.

Expected results

macOS version Without plugin With plugin
macOS 13–15 (Ventura–Sequoia) ~60 FPS 120 FPS on ProMotion, 144+ on external
macOS 26+ (Tahoe) Already native refresh rate No change (no-op)

macOS version compatibility

macOS 60fps cap exists? Plugin effect
13 Ventura Yes Unlocks native refresh rate
14 Sonoma Yes Unlocks native refresh rate
15 Sequoia Yes Unlocks native refresh rate
26 Tahoe No — Apple removed the cap No-op (already unlocked)

On macOS 26+, the PreferPageRenderingUpdatesNear60FPSEnabled preference still exists in WebKit's feature list and can be toggled, but WKWebView ignores it. The plugin detects this gracefully and logs success, though the toggle has no observable effect.

Tested on macOS 26.3.1 (Build 25D2128) with a 120Hz ProMotion display and a 165Hz external monitor — both ran at native refresh rate with or without the plugin.

Configuration

Optional. Set in tauri.conf.json to disable the plugin without removing it:

{
  "plugins": {
    "macos-fps": {
      "enabled": false
    }
  }
}

Default: enabled: true — all webviews are automatically unlocked.

Per-webview control

For fine-grained control, use the extension trait:

use tauri_plugin_macos_fps::MacFpsExt;

// Unlock native refresh rate on a specific webview:
webview.unlock_fps()?;

// Re-lock to 60fps:
webview.lock_fps()?;

How it works

WKWebView
  → .configuration  → WKWebViewConfiguration
    → .preferences   → WKPreferences
      → [WKPreferences _features]  → NSArray<_WKFeature *>
        → find key == "PreferPageRenderingUpdatesNear60FPSEnabled"
          → [preferences _setEnabled:NO forFeature:feature]

The plugin hooks into Tauri's on_webview_ready lifecycle event. Every webview is automatically unlocked as it's created. The private _features API returns all WebKit feature flags; we find our target key and toggle it off.

Why this can't be App Store safe

We exhaustively investigated every possible public API path. None of them work. Here's why:

NSUserDefaults — won't work

The obvious idea: set PreferPageRenderingUpdatesNear60FPSEnabled = false via NSUserDefaults (a public API) before the webview loads. We traced through the WebKit source code (WebPreferencesCocoa.mm) and found the blocker:

void WebPreferences::platformInitializeStore()
{
    // ...persistent preferences...
    if (!m_identifier)
        return;  // ← WKWebView with default config hits this and SKIPS reading
    FOR_EACH_PERSISTENT_WEBKIT_PREFERENCE(INITIALIZE_PREFERENCE_FROM_NSUSERDEFAULTS)
}

WKWebView created with a default WKWebViewConfiguration (which is what Tauri/wry uses) has an empty identifier. This causes platformGetBoolUserValueForKey() to return early — NSUserDefaults is never consulted for this preference. No matter what key format you use (WebKitPrefer..., WebKit2Prefer..., com.apple.WebKit.Prefer...), it won't be read.

We built a diagnostic app that tests 8 different NSUserDefaults key variations to confirm this.

Info.plist keys — won't work

CADisableMinimumFrameDurationOnPhone unlocks 120Hz for native Core Animation on iOS, but has zero effect on WKWebView's internal rendering on any platform. No documented Info.plist key affects WKWebView frame rate.

WKWebViewConfiguration / WKPreferences public API — doesn't exist

We checked every public property on WKWebViewConfiguration and WKPreferences through macOS 26. None affect rendering frame rate. Apple added no new frame-rate APIs in macOS 14, 15, or 26.

CADisplayLink / Core Animation — can't reach the compositor

WKWebView's rendering pipeline is entirely internal to the WebKit process. Setting preferredFrameRateRange on the webview's backing layer doesn't affect WebKit's internal compositor. You can't drive it from outside.

KVC (Key-Value Coding) backdoors — Apple is killing them

Recent WebKit commits now raise exceptions when apps use setValue:forKey: on undocumented WKWebViewConfiguration properties when linked against newer SDKs. Apple is actively closing this door.

The bottom line

The only way to toggle PreferPageRenderingUpdatesNear60FPSEnabled at runtime is through the private _features / _setEnabled:forFeature: API. There is no public alternative. Apple's own Safari browser uses this same private API internally.

We filed this research against WebKit Bug #294338. A future WebKit proposal for CSS animation-frame-rate may eventually provide a public path, but it hasn't shipped in any stable Safari release.

Who should use this plugin

This plugin is for apps distributed outside the Mac App Store:

  • Direct distribution (.dmg, Homebrew, GitHub releases, cargo install)
  • Internal / enterprise apps
  • Development and testing
  • Open source projects where users build from source

If you need Mac App Store distribution, you cannot use this plugin. There is currently no alternative — the only option is to wait for Apple to provide a public API or for macOS 26+ adoption to make the cap irrelevant.

Platform behavior

Platform Effect
macOS 13–15 (WKWebView) Unlocks native refresh rate (120Hz ProMotion, 144Hz+, etc.)
macOS 26+ (WKWebView) No-op — Apple removed the cap, already runs at native refresh rate
Windows (WebView2) No-op — WebView2 already renders at native refresh rate
Linux (WebKitGTK) No-op — typically 60fps, no known toggle
iOS (WKWebView) No-op — untested, may work in future versions

The plugin compiles and runs on all platforms. On non-macOS, init() registers a plugin that does nothing.

Graceful degradation

The plugin never crashes. If the private API changes in a future macOS version:

Scenario Behavior
_features returns nil Warning logged, app continues at 60fps
Target preference key not found Warning logged, app continues at 60fps
WKPreferences class missing Warning logged, app continues at 60fps
WKWebView pointer is null Warning logged, app continues at 60fps

All failure paths log via the standard log crate at WARN level and fall back silently to the default 60fps behavior.

Verify in any Tauri app

Paste this in your browser console or embed it in your frontend to measure actual FPS:

let frames = 0, lastTime = performance.now();
(function measure() {
    frames++;
    const now = performance.now();
    if (now - lastTime >= 1000) {
        console.log(`${Math.round(frames * 1000 / (now - lastTime))} FPS`);
        frames = 0;
        lastTime = now;
    }
    requestAnimationFrame(measure);
})();

Contributing

See CONTRIBUTING.md for guidelines.

License

Licensed under either of

at your option.

Links