perf: re-warm WebKit GPU on Reader tab appear if idle >60s
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

GPU process can exit due to idle timeout if user waits before opening
Reader. reWarmIfNeeded() checks elapsed time since last warm — if >60s,
re-attaches WKWebView to window and loads minimal HTML to restart the
GPU process. Called on ReaderTabView.onAppear.

No timers, no keep-alive loops. Just a timestamp check on tab appear.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 00:49:26 -05:00
parent 1579633da0
commit db77a6d34d
2 changed files with 21 additions and 5 deletions

View File

@@ -8,6 +8,7 @@ final class ArticleRenderer {
static let shared = ArticleRenderer() static let shared = ArticleRenderer()
let webView: WKWebView let webView: WKWebView
private var lastWarmTime: CFAbsoluteTime = 0
private init() { private init() {
let config = WKWebViewConfiguration() let config = WKWebViewConfiguration()
@@ -38,11 +39,11 @@ final class ArticleRenderer {
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" width="100" height="60"> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" width="100" height="60">
""" """
), baseURL: nil) ), baseURL: nil)
lastWarmTime = CFAbsoluteTimeGetCurrent()
} }
/// Attach WKWebView to the app window (invisible) to force GPU process /// Attach WKWebView to the app window (invisible) to force GPU process
/// launch. Must be called after window is available. Keeps the webview /// launch. Must be called after window is available.
/// attached — it will be moved to the article container on first use.
func attachToWindow() { func attachToWindow() {
guard webView.window == nil, guard webView.window == nil,
let window = UIApplication.shared.connectedScenes let window = UIApplication.shared.connectedScenes
@@ -51,10 +52,22 @@ final class ArticleRenderer {
webView.alpha = 0 webView.alpha = 0
window.addSubview(webView) window.addSubview(webView)
}
// Keep attached (don't remove) so GPU process stays alive. /// Re-warm if the GPU process may have exited due to idle timeout.
// The webview will be reparented to the article container /// Lightweight: only fires if >60s since last warm, and just reloads
// via removeFromSuperview + addSubview in makeUIView. /// a tiny HTML page + re-attaches to window if needed.
func reWarmIfNeeded() {
let elapsed = CFAbsoluteTimeGetCurrent() - lastWarmTime
guard elapsed > 60 else { return }
attachToWindow()
webView.loadHTMLString(
"<html><head><style>body{font-family:-apple-system;}</style></head>" +
"<body><p>warm</p></body></html>",
baseURL: nil
)
lastWarmTime = CFAbsoluteTimeGetCurrent()
} }
} }

View File

@@ -139,6 +139,9 @@ struct ReaderTabView: View {
FeedManagementSheet(vm: vm) FeedManagementSheet(vm: vm)
} }
} }
.onAppear {
ArticleRenderer.shared.reWarmIfNeeded()
}
} }
private var subTabs: [String] { ["Unread", "Starred", "All"] } private var subTabs: [String] { ["Unread", "Starred", "All"] }