From 127da8feaa70dbae302d67384c880af3308db59e Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Sat, 4 Apr 2026 00:44:48 -0500 Subject: [PATCH] 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) --- ios/Platform/Platform/ContentView.swift | 5 +- .../Reader/Views/ArticleWebView.swift | 54 ++++++++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/ios/Platform/Platform/ContentView.swift b/ios/Platform/Platform/ContentView.swift index 90ed19b..61d28fe 100644 --- a/ios/Platform/Platform/ContentView.swift +++ b/ios/Platform/Platform/ContentView.swift @@ -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() } } diff --git a/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift b/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift index 8d1285f..99666b7 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift @@ -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 - // 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 {

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.

Quoted text for blockquote styling.
code { display: block; }
+ """ ), 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") + } + } } }