-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[WEP] keep WebViews running while hidden #5920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| # Wails Enhancement Proposal (WEP) | ||
|
|
||
| ## Keep WebViews running while hidden | ||
|
|
||
| **WEP Number**: (leave blank, assigned on acceptance) | ||
| **Status**: Draft | ||
| **Author**: Julian Storer | ||
| **Created**: 2026-08-04 | ||
| **Discussion**: [optional link to any prior discussion of the idea] | ||
| **Implementor**: Julian Storer | ||
| **Target**: Wails v3 | ||
|
|
||
| ## Summary | ||
|
|
||
| Add an opt-in per-window preference that stops the platform WebView from | ||
| throttling JavaScript while the window is hidden. It is exposed as | ||
| `Mac.WebviewPreferences.KeepRunningWhenHidden` on macOS and | ||
| `Windows.KeepRunningWhenHidden` on Windows. Default behaviour is unchanged. | ||
|
|
||
| ## Motivation | ||
|
|
||
| Both platform WebViews aggressively throttle a hidden window's JavaScript: | ||
|
|
||
| - **macOS**: once another window in the application becomes active, a hidden | ||
| `WKWebView` has its scheduling reduced all the way to 0Hz. | ||
| - **Windows**: WebView2 moves a hidden window's controller into "efficiency | ||
| mode" (WebView2 issue #2861), driving the page's timers toward 0Hz. | ||
|
Comment on lines
+26
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Changed files/stat:"
git diff --stat || true
echo
echo "Candidate proposal file:"
fd -a 'proposal\.md$' . | sed 's#^\./##' | grep -F 'v3/wep/proposals/keep-running-when-hidden/proposal.md' || true
echo
echo "Relevant proposal excerpt:"
if [ -f v3/wep/proposals/keep-running-when-hidden/proposal.md ]; then
nl -ba v3/wep/proposals/keep-running-when-hidden/proposal.md | sed -n '1,120p'
fi
echo
echo "Repository-wide references to keep-running/hidden/IsVisible:"
rg -n "keep-running|hidden|is_visible|IsVisible|WebView2|PutIsVisible|controller|efficiency" . --glob '!**/.git/**' --glob '!**/node_modules/**' --glob '!**/build/**' --glob '!**/dist/**' | head -n 200Repository: wailsapp/wails Length of output: 340 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Changed files/stat:"
git diff --stat || true
echo
echo "Candidate proposal file:"
ls -la v3/wep/proposals/keep-running-when-hidden/proposal.md || true
echo
echo "Relevant proposal excerpt:"
awk '{printf "%6d\t%s\n", NR, $0}' v3/wep/proposals/keep-running-when-hidden/proposal.md | sed -n '1,140p'
echo
echo "Repository-wide references to keep-running/hidden/IsVisible:"
rg -n "keep-running|hidden|is_visible|IsVisible|WebView2|PutIsVisible|controller|efficiency" . --glob '!**/.git/**' --glob '!**/node_modules/**' --glob '!**/build/**' --glob '!**/dist/**' | head -n 200 || trueRepository: wailsapp/wails Length of output: 50371 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Proposal excerpt:"
awk '{printf "%6d\t%s\n", NR, $0}' v3/wep/proposals/keep-running-when-hidden/proposal.md | sed -n '1,120p'
echo
echo "Focused source references for proposal keywords:"
rg -n -C 3 "KeepRunningWhenHidden|keep-running-when-hidden|KeepRunningWhenHidden|controller.|PutIsVisible|IsVisible|navigationCompleted|HideWindow|hide.*window|min.*window|restore.*window|window hidden|hidden.*window" v3 --glob '!**/*.md' --glob '!**/docs/**' | head -n 300
echo
echo "Candidate window/windowing files in v3:"
git ls-files v3 | rg '(^v3/.*/.*(window|browser|webview|window\.)|(^v3/.*/windows)' | sed -n '1,160p'Repository: wailsapp/wails Length of output: 50372 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Focused Go references for windows WebViewOptions, KeepRunningWhenHidden, and PutIsVisible:"
rg -n -C 4 "KeepRunningWhenHidden|PutIsVisible|IsVisible|navigationCompleted|WebView.*Options|Webview.*Options|Chromium\\.(Show|Hide)|Show\\(\\)|Hide\\(" v3/internal v3/pkg v3/lib --glob '*.go' --max-count 200Repository: wailsapp/wails Length of output: 50371 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Files containing KeepRunningWhenHidden (Go and proposal):"
rg -n "KeepRunningWhenHidden" . --glob '*.{go,md}' || true
echo
echo "Files containing WebviewWindowOptions + KeepRunningWhenHidden context:"
rg -n "KeepRunningWhenHidden|WebviewWindowOptions|type WindowsWindow|type WindowOptions|type .*Window " . --glob '*.go' | head -n 240
echo
echo "Windows window/windowing Go candidates:"
git ls-files v3 | rg 'windows|window|webview|webkit|platform|platforms|runtime' | head -n 220
echo
echo "webview2 window implementation candidates containing PutIsVisible or navigationCompleted:"
rg -n -C 5 "navigationCompleted|PutIsVisible|Hide\\(\\)|Show\\(|Hidden|WindowsWindow|KeepRunningWhenHidden|ICoreWebView2Controller3|ICoreWebView2Controller2" v3/internal/webview2 --glob '*.go' --max-count 200 || trueRepository: wailsapp/wails Length of output: 35353 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "WindowsWindow option type and KeepRunningWhenHidden:"
awk '{printf "%6d\t%s\n", NR, $0}' v3/pkg/application/webview_window_options.go | sed -n '330,375p'
echo
echo "WindowsWebViewWindow field and constructor/options handling:"
awk '{printf "%6d\t%s\n", NR, $0}' v3/pkg/application/webview_window_windows.go | sed -n '1,180p'
rg -n -C 4 "navigationCompleted|PutIsVisible|IsVisible|Show\\(|Hide\\(|Hidden|W32|WWebView|windowsWebviewWindow|NewWithOptions|WebviewWindowOptions" v3/pkg/application/webview_window_windows.go | sed -n '1,260p'
echo
echo "WindowsWebViewWindow Hide.Show implementations nearby:"
awk '{printf "%6d\t%s\n", NR, $0}' v3/pkg/application/webview_window_windows.go | rg 'Hide|Show|Navigation|Controller|PutIsVisible|KeepRunningWhenHidden' -n -C 3 || trueRepository: wailsapp/wails Length of output: 33417 🌐 Web query:
💡 Result: The issue referenced as Citations:
Scope the Windows option to the hidden-and-never-shown case. The design says the Windows preference keeps the controller visible while the window is hidden, but the implementation leaves every later hide/minimize/restore transition on the existing 🤖 Prompt for AI AgentsSource: MCP tools |
||
|
|
||
| That is the right default for a normal window the user has minimised or | ||
| switched away from. It is wrong for an application that deliberately keeps a | ||
| WebView hidden and uses it as a worker — for example a headless WebView that | ||
| runs plugin or engine code while a *different* window is the visible UI, or an | ||
| offscreen host used for tests. Today such a WebView stalls the moment any other | ||
| window takes focus, and there is no supported way to keep it running. | ||
|
|
||
| There is no reliable pure-web workaround: the throttling happens below the page, | ||
| so timers, `postMessage` loops and workers hosted in that WebView all slow down | ||
| regardless of what the page does. | ||
|
|
||
| ## Detailed Design | ||
|
|
||
| A per-window, opt-in preference, off by default. | ||
|
|
||
| ### macOS | ||
|
|
||
| `WKPreferences.inactiveSchedulingPolicy` (macOS 14+ / iOS 17+) controls the | ||
| throttling. A new tristate field is added to `MacWebviewPreferences`: | ||
|
|
||
| ```go | ||
| // KeepRunningWhenHidden controls WKPreferences.inactiveSchedulingPolicy. | ||
| // true -> WKInactiveSchedulingPolicyNone (never throttle) | ||
| // false -> WKInactiveSchedulingPolicyThrottle (always throttle) | ||
| // unset -> platform default (unchanged) | ||
| KeepRunningWhenHidden optional.Bool | ||
| ``` | ||
|
|
||
| `inactiveSchedulingPolicy` is read from the `WKWebViewConfiguration`'s | ||
| preferences at navigation time, so it is assigned alongside the other | ||
| configuration-time preferences (`tabFocusesLinks`, `textInteractionEnabled`, | ||
| `elementFullscreenEnabled`) before the `WKWebView` is allocated. Left unset, the | ||
| configuration is not touched and the OS default (automatic) applies. | ||
|
|
||
| ### Windows | ||
|
|
||
| WebView2 exposes visibility through the controller's `IsVisible`. A new field is | ||
| added to `WindowsWindow`: | ||
|
|
||
| ```go | ||
| // KeepRunningWhenHidden keeps the WebView2 controller IsVisible=true even when | ||
| // the window is hidden, so WebView2 never enters efficiency mode. | ||
| KeepRunningWhenHidden bool | ||
| ``` | ||
|
|
||
| When set, a hidden-and-never-shown window keeps its controller `IsVisible=true` | ||
| in `navigationCompleted` instead of hiding it. This fills in the | ||
| `PutIsVisible(true)` path the code already anticipated with a TODO referencing | ||
| issue #2861, now that `Chromium.Show()`/`Hide()` exist. | ||
|
|
||
| ### Usage | ||
|
|
||
| ```go | ||
| app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ | ||
| Hidden: true, | ||
| Mac: application.MacWindow{ | ||
| WebviewPreferences: application.MacWebviewPreferences{ | ||
| KeepRunningWhenHidden: optional.NewBool(true), | ||
| }, | ||
| }, | ||
| Windows: application.WindowsWindow{ | ||
| KeepRunningWhenHidden: true, | ||
| }, | ||
| }) | ||
| ``` | ||
|
|
||
| ## Non-Goals | ||
|
|
||
| - Not a global/application-wide setting; it is per-window and opt-in. | ||
| - Does not change the default. A window that does not set it behaves exactly as | ||
| before, including full throttling when hidden. | ||
| - Not a power-management feature: keeping a hidden WebView running has a battery | ||
| cost, which is the caller's decision to make. | ||
| - No Linux implementation (see Platform Considerations). | ||
|
|
||
| ## Platform Considerations | ||
|
|
||
| - **macOS**: requires macOS 14+ for `inactiveSchedulingPolicy`. On older | ||
| systems the property is unavailable and the option is a no-op; the WebView | ||
| keeps the OS default behaviour. Tristate so callers can request "never | ||
| throttle", "always throttle", or "leave the platform default alone". | ||
| - **Windows**: WebView2. Binary — the controller is either kept visible or not. | ||
| - **Linux**: not implemented in this proposal. WebKitGTK does not throttle a | ||
| hidden WebView in the same way, so there is no equivalent knob to expose yet; | ||
| the field is simply absent from the Linux options. | ||
|
|
||
| The macOS field is an `optional.Bool` (three states) and the Windows field is a | ||
| plain `bool` (two states), because the underlying APIs differ: macOS has a | ||
| genuine third "platform default" state worth preserving, Windows does not. This | ||
| asymmetry is deliberate but is the main open question for review — an | ||
| alternative is to make both `optional.Bool` for surface consistency (see | ||
| Alternatives). | ||
|
|
||
| ## Pros/Cons | ||
|
|
||
| ### Pros | ||
|
|
||
| - Enables a supported "background worker WebView" pattern that is currently | ||
| impossible without stalling. | ||
| - Opt-in and per-window; zero effect on existing apps. | ||
| - Thin wrappers over first-party platform APIs, so low implementation and | ||
| maintenance cost. | ||
|
|
||
| ### Cons | ||
|
|
||
| - Two-platform-only; Linux has no equivalent, so behaviour is not uniform. | ||
| - macOS/Windows field types differ (tristate vs bool). | ||
| - Keeping hidden WebViews running costs CPU/battery if misused. | ||
|
|
||
| ## Alternatives Considered | ||
|
|
||
| - **Make both fields `optional.Bool`** for a consistent surface. Rejected for | ||
| the reference implementation because the Windows path has no meaningful third | ||
| state, but it is a reasonable outcome of review. | ||
| - **A single cross-platform option on `WebviewWindowOptions`** instead of one | ||
| field per platform block. Rejected: the semantics and availability differ per | ||
| platform (macOS version gate, no Linux support), and Wails already groups | ||
| platform-specific tuning under `Mac`/`Windows`/`Linux` blocks. | ||
| - **Do nothing / document the limitation.** Rejected: there is no page-level | ||
| workaround, so an app that needs a background WebView has no path today. | ||
|
|
||
| ## Backwards Compatibility | ||
|
|
||
| Fully backwards compatible. Both fields default to "unset"/false, which | ||
| reproduces today's behaviour exactly. No existing option is changed or removed. | ||
|
|
||
| ## Security and Privacy | ||
|
|
||
| No new capability is exposed to page content and no new data crosses the | ||
| bridge. The only effect is scheduling: an opted-in hidden WebView keeps | ||
| executing the code it was already running. There are no permission or | ||
| data-handling changes. | ||
|
|
||
| ## Test Plan | ||
|
|
||
| - Manual: a hidden WebView running a periodic timer (e.g. `postMessage` or a | ||
| visible tick counter in a second window). With the option off, the tick rate | ||
| collapses once another window is focused; with it on, the rate holds. | ||
| - macOS: verify the option is a no-op on < macOS 14 and does not error. | ||
| - Windows: verify a hidden-never-shown window keeps ticking and that a normally | ||
| shown/hidden window is unaffected. | ||
| - An example under `v3/examples` demonstrating a background worker WebView can | ||
| be added as part of implementation. | ||
|
|
||
| ## Reference Implementation | ||
|
|
||
| A working reference implementation exists for both platforms (macOS via | ||
| `inactiveSchedulingPolicy`, Windows via the controller `IsVisible` path). The | ||
| implementation PR will be linked here once opened. | ||
|
|
||
| ## Maintenance Plan | ||
|
|
||
| The change is a thin wrapper over `WKPreferences.inactiveSchedulingPolicy` and | ||
| the WebView2 controller `IsVisible`, both stable first-party APIs, so ongoing | ||
| maintenance is minimal and tracks the platform SDKs. Maintained alongside the | ||
| rest of the WebView option surface. | ||
|
|
||
| ## Conclusion | ||
|
|
||
| `KeepRunningWhenHidden` makes the "hidden WebView as a background worker" | ||
| pattern viable on macOS and Windows with a small, opt-in, backwards-compatible | ||
| addition, while leaving the throttling default untouched for every existing app. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
Repository: wailsapp/wails
Length of output: 297
🌐 Web query:
Apple WKPreferences inactiveSchedulingPolicy WebViews not in window default suspend thrott none💡 Result:
The inactiveSchedulingPolicy property in Apple's WKPreferences class is used to specify how a WKWebView handles tasks when it is not in a window, such as when it is in a background tab [1][2]. This policy is available for macOS 14.0+ and iOS 17.0+ [2]. The policy can be set to one of the following values, defined in the WKPreferences.InactiveSchedulingPolicy enumeration [3][2]: 1. suspend: This is the default policy [1]. The web view fully suspends tasks, pausing JavaScript execution and page layout when the web view is idle and detached from the view hierarchy [1][3][2]. 2. throttle: The web view limits processing (CPU-throttled) when idle, but does not fully suspend tasks [3][2]. 3. none: No special scheduling behavior is applied, allowing the web view to continue running normally even when idle [3][2]. Web views are generally exempted from these inactive scheduling policies if they are actively playing media, performing media capture, or engaging in other user-interactive activities [1][2].
Citations:
🏁 Script executed:
Repository: wailsapp/wails
Length of output: 50373
🏁 Script executed:
Repository: wailsapp/wails
Length of output: 11613
🌐 Web query:
Apple WKPreferences inactiveScheduling policy on hidden NSWindow WKWebView not no window💡 Result:
The inactiveSchedulingPolicy property in WKPreferences allows developers to control the scheduling behavior of a WKWebView when it is idle and detached from the view hierarchy (i.e., not in a window) [1][2]. Introduced in macOS 14.0 and iOS 17.0, this policy addresses issues where WebKit might aggressively throttle or suspend the WebContent process for web views that are hidden or not currently attached to a window [1][3][4]. The policy provides three modes defined by the WKPreferences.InactiveSchedulingPolicy enumeration [5][1]: 1. WKInactiveSchedulingPolicyNone: The web view runs tasks normally, with no special scheduling behavior applied, even when idle [1]. This is often used to prevent unwanted suspension of background processes or web views that are meant to stay active while hidden [3][4]. 2. WKInactiveSchedulingPolicyThrottle: The web view CPU-throttles tasks when idle but does not fully suspend them [1]. 3. WKInactiveSchedulingPolicySuspend: The web view fully suspends tasks (pausing JavaScript execution and page layout) when idle [1]. Web views are generally not considered idle if they are currently playing media or loading web pages [1][6]. Developers typically use the None policy when they need to ensure that a WKWebView continues to execute JavaScript or maintain state while it is hidden or detached from a visible window [3][4].
Citations:
Align the macOS contract with the API semantics.
WKPreferences.inactiveSchedulingPolicyis intended for WebViews idle and detached from the view hierarchy, not specifically for hiddenNSWindowcontent. Verify thatnoneprevents throttling for the hidden-window use case before exposing this option as described. Also replace “OS default (automatic)” withWKInactiveSchedulingPolicySuspend, and avoid “at navigation time” because the design says the value is assigned when theWKWebViewConfigurationis built before allocation.[low_effort_and_medium_reward]
🤖 Prompt for AI Agents
Source: MCP tools