perf: force GPU process launch during warmup by attaching WKWebView to window
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

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:
Yusuf Suleman
2026-04-04 00:44:48 -05:00
parent 62f9a2503a
commit 127da8feaa
2 changed files with 51 additions and 8 deletions

View File

@@ -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()
}
}

View File

@@ -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")
}
}
}
}