Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions v3/wep/proposals/keep-running-when-hidden/proposal.md
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.
Comment on lines +15 to +18

Copy link
Copy Markdown
Contributor

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:

#!/bin/bash
set -euo pipefail

echo "Repository files for proposal:"
git ls-files | grep -E 'v3/wep/proposals/keep-running-when-hidden|keep-running-when-hidden' || true

echo
echo "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 "Search for related identifiers:"
rg -n "KeepRunningWhenHidden|inactiveSchedulingPolicy|WKPreferences|SchedulingPolicy|KeepRunning|throttl|suspend|automatic|hidden|miniaturiz" . || true

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:

#!/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,140p'

echo
echo "Search for related identifiers:"
rg -n "KeepRunningWhenHidden|inactiveSchedulingPolicy|WKPreferences|SchedulingPolicy|KeepRunning|throttl|suspend|automatic|hidden|miniaturiz|window|attached|allocation|navigation" v3/wep/proposals/keep-running-when-hidden/proposal.md . || true

Repository: wailsapp/wails

Length of output: 50373


🏁 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,160p'

echo
echo "Exact identifier matches in proposal only:"
rg -n "KeepRunningWhenHidden|inactiveSchedulingPolicy|WKPreferences|SchedulingPolicy|KeepRunning|throttl|suspend|automatic|hidden|miniaturiz|window|at navigation time|allocation|navigation|true|false" v3/wep/proposals/keep-running-when-hidden/proposal.md

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.inactiveSchedulingPolicy is intended for WebViews idle and detached from the view hierarchy, not specifically for hidden NSWindow content. Verify that none prevents throttling for the hidden-window use case before exposing this option as described. Also replace “OS default (automatic)” with WKInactiveSchedulingPolicySuspend, and avoid “at navigation time” because the design says the value is assigned when the WKWebViewConfiguration is built before allocation.
[low_effort_and_medium_reward]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/wep/proposals/keep-running-when-hidden/proposal.md` around lines 15 - 18,
Update the macOS proposal contract to use WKInactiveSchedulingPolicySuspend
instead of “OS default (automatic)”, and describe KeepRunningWhenHidden as
assigned while constructing WKWebViewConfiguration before allocation rather than
“at navigation time”. Verify that the none policy actually prevents throttling
for hidden NSWindow content before exposing the option; otherwise revise or
remove the macOS exposure.

Source: MCP tools


## 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 200

Repository: 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 || true

Repository: 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 200

Repository: 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 || true

Repository: 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 || true

Repository: wailsapp/wails

Length of output: 33417


🌐 Web query:

Microsoft WebView2 PutIsVisible IsVisible efficiency mode issue 2861

💡 Result:

The issue referenced as #2861 in the Wails repository [1] relates to scenarios where WebView2 applications may fail to display or become unresponsive due to Windows Efficiency Mode. In Microsoft WebView2, Efficiency Mode is a resource-management feature that can throttle or suspend processes—typically renderer processes associated with the WebView2 control—when the system determines the control is inactive or not visible [2][3]. The PutIsVisible method (available via the ICoreWebView2Controller interface) [4][5][6] is the primary API used to manage the visibility state of a WebView2 control. The official guidance from Microsoft is to keep the IsVisible property set to true to indicate that the control is active and to avoid unnecessary throttling by Efficiency Mode [2][3]. Key technical points regarding this issue include: - Behavior: When IsVisible is set to false, WebView2 may purge memory or throttle activity [4]. While this is a documented performance optimization (e.g., when a window is minimized) [4], it can interfere with applications if the system incorrectly flags the WebView2 instance as inactive, leading to scenarios where the window fails to render or loses communication with the host application runtime [1]. - Wails Implementation: The Wails project has implemented workarounds for this behavior in its window management logic [7]. These workarounds include ensuring the window is shown and, if navigation is delayed, triggering a forced call to the show logic to prevent the WebView2 from remaining permanently invisible or suspended due to system-level Efficiency Mode management [7]. - Documentation: Microsoft notes that developers should handle WM_SIZE messages to toggle IsVisible (e.g., FALSE when minimized, TRUE when restored) to balance resource usage and performance correctly [4]. However, relying on this to avoid Efficiency Mode issues remains a common point of discussion, as developers sometimes require the control to stay active regardless of apparent UI visibility [3].

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 Show()/Hide() paths. Keep the same contract in that implementation, or change the option wording to mean only a hidden window that has never been shown.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/wep/proposals/keep-running-when-hidden/proposal.md` around lines 26 - 27,
Align the Windows preference wording with its implementation: either update the
hidden-window behavior to keep the WebView2 controller visible across later
hide, minimize, and restore transitions, or explicitly scope the option to
windows that are hidden and have never been shown. Update the Windows option
description in the proposal so it matches the chosen contract.

Source: 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.
Loading