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 {
|
.task {
|
||||||
guard showReader else { return }
|
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()
|
await readerVM.loadInitial()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,20 +10,34 @@ final class ArticleRenderer {
|
|||||||
let webView: WKWebView
|
let webView: WKWebView
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
|
let t0 = CFAbsoluteTimeGetCurrent()
|
||||||
|
print("[WARMUP] ArticleRenderer.init started")
|
||||||
|
|
||||||
let config = WKWebViewConfiguration()
|
let config = WKWebViewConfiguration()
|
||||||
config.allowsInlineMediaPlayback = true
|
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.isOpaque = false
|
||||||
webView.backgroundColor = .clear
|
webView.backgroundColor = .clear
|
||||||
webView.scrollView.isScrollEnabled = true
|
webView.scrollView.isScrollEnabled = true
|
||||||
|
|
||||||
// Pre-warm with realistic content. An empty <html><body></body></html>
|
// Attach to the key window briefly to force GPU process launch.
|
||||||
// only warms the WebContent process. Loading the real CSS template with
|
// The WKWebView must be in a real window hierarchy for WebKit to
|
||||||
// sample text forces WebKit to also parse CSS, load/measure system fonts,
|
// spin up the GPU compositor. At .zero frame or detached, it skips
|
||||||
// initialize the layout engine, and spin up the compositing pipeline.
|
// GPU init — causing a ~3s stall on first real display.
|
||||||
// This shifts the ~3s first-render cost to app launch (background) so the
|
if let window = UIApplication.shared.connectedScenes
|
||||||
// first article open doesn't stall.
|
.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(
|
webView.loadHTMLString(ArticleHTMLBuilder.build(
|
||||||
title: "Warming up",
|
title: "Warming up",
|
||||||
url: nil,
|
url: nil,
|
||||||
@@ -38,8 +52,34 @@ final class ArticleRenderer {
|
|||||||
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
|
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
|
||||||
<blockquote>Quoted text for blockquote styling.</blockquote>
|
<blockquote>Quoted text for blockquote styling.</blockquote>
|
||||||
<pre><code>code { display: block; }</code></pre>
|
<pre><code>code { display: block; }</code></pre>
|
||||||
|
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" width="100" height="60">
|
||||||
"""
|
"""
|
||||||
), baseURL: nil)
|
), 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