perf: force GPU process launch during warmup by attaching WKWebView to window
THEORY: WKWebView at frame .zero or detached from window skips GPU compositor init. First real display triggers GPU process launch (~3s). FIX: Create WKWebView at screen bounds, attach to key window (alpha=0) during warmup. WebKit launches GPU process while user is on Home tab. Remove from window after 2s (GPU process stays alive). Also: ensureAttachedToWindow() fallback if init runs before window exists. Called from ContentView.task where window is guaranteed. Added 1x1 transparent GIF in warmup HTML to force image decoder init. Kept all debug logging for verification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,7 +94,10 @@ struct MainTabView: View {
|
||||
}
|
||||
.task {
|
||||
guard showReader else { return }
|
||||
_ = ArticleRenderer.shared
|
||||
let renderer = ArticleRenderer.shared
|
||||
// Window is now available — ensure WKWebView is attached
|
||||
// so GPU compositor launches during startup, not first article tap
|
||||
renderer.ensureAttachedToWindow()
|
||||
await readerVM.loadInitial()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,20 +10,34 @@ final class ArticleRenderer {
|
||||
let webView: WKWebView
|
||||
|
||||
private init() {
|
||||
let t0 = CFAbsoluteTimeGetCurrent()
|
||||
print("[WARMUP] ArticleRenderer.init started")
|
||||
|
||||
let config = WKWebViewConfiguration()
|
||||
config.allowsInlineMediaPlayback = true
|
||||
|
||||
webView = WKWebView(frame: .zero, configuration: config)
|
||||
// Create at screen size (not .zero) so WebKit initializes the
|
||||
// GPU compositor during warmup, not on first article open.
|
||||
let screenBounds = UIScreen.main.bounds
|
||||
webView = WKWebView(frame: screenBounds, configuration: config)
|
||||
webView.isOpaque = false
|
||||
webView.backgroundColor = .clear
|
||||
webView.scrollView.isScrollEnabled = true
|
||||
|
||||
// Pre-warm with realistic content. An empty <html><body></body></html>
|
||||
// only warms the WebContent process. Loading the real CSS template with
|
||||
// sample text forces WebKit to also parse CSS, load/measure system fonts,
|
||||
// initialize the layout engine, and spin up the compositing pipeline.
|
||||
// This shifts the ~3s first-render cost to app launch (background) so the
|
||||
// first article open doesn't stall.
|
||||
// Attach to the key window briefly to force GPU process launch.
|
||||
// The WKWebView must be in a real window hierarchy for WebKit to
|
||||
// spin up the GPU compositor. At .zero frame or detached, it skips
|
||||
// GPU init — causing a ~3s stall on first real display.
|
||||
if let window = UIApplication.shared.connectedScenes
|
||||
.compactMap({ $0 as? UIWindowScene }).first?.windows.first {
|
||||
webView.alpha = 0 // invisible
|
||||
window.addSubview(webView)
|
||||
print("[WARMUP] attached to window")
|
||||
} else {
|
||||
print("[WARMUP] no window available yet — will attach later")
|
||||
}
|
||||
|
||||
// Load realistic content to warm CSS parsing, font loading, layout
|
||||
webView.loadHTMLString(ArticleHTMLBuilder.build(
|
||||
title: "Warming up",
|
||||
url: nil,
|
||||
@@ -38,8 +52,34 @@ final class ArticleRenderer {
|
||||
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
|
||||
<blockquote>Quoted text for blockquote styling.</blockquote>
|
||||
<pre><code>code { display: block; }</code></pre>
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" width="100" height="60">
|
||||
"""
|
||||
), baseURL: nil)
|
||||
|
||||
// Remove from window after a delay (GPU process stays alive)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak webView] in
|
||||
webView?.removeFromSuperview()
|
||||
webView?.alpha = 1
|
||||
print("[WARMUP] detached from window +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms")
|
||||
}
|
||||
|
||||
print("[WARMUP] init complete +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms")
|
||||
}
|
||||
|
||||
/// Call this after app window is available if init ran too early
|
||||
func ensureAttachedToWindow() {
|
||||
guard webView.window == nil else { return }
|
||||
if let window = UIApplication.shared.connectedScenes
|
||||
.compactMap({ $0 as? UIWindowScene }).first?.windows.first {
|
||||
webView.alpha = 0
|
||||
window.addSubview(webView)
|
||||
print("[WARMUP] late-attached to window")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
||||
self?.webView.removeFromSuperview()
|
||||
self?.webView.alpha = 1
|
||||
print("[WARMUP] late-detached from window")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user