diff --git a/ios/Platform/Platform/ContentView.swift b/ios/Platform/Platform/ContentView.swift index 61d28fe..35db154 100644 --- a/ios/Platform/Platform/ContentView.swift +++ b/ios/Platform/Platform/ContentView.swift @@ -95,9 +95,9 @@ struct MainTabView: View { .task { guard showReader else { return } let renderer = ArticleRenderer.shared - // Window is now available — ensure WKWebView is attached - // so GPU compositor launches during startup, not first article tap - renderer.ensureAttachedToWindow() + // Window is now available — attach WKWebView to force GPU + // compositor launch during startup, not first article tap + renderer.attachToWindow() await readerVM.loadInitial() } } diff --git a/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift b/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift index f425ec0..577e6cd 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ArticleView.swift @@ -13,7 +13,6 @@ struct ArticleView: View { self.vm = vm _currentEntry = State(initialValue: entry) _articleContent = State(initialValue: entry.articleHTML) - print("[ART-OPEN] init contentLen=\(entry.articleHTML.count)") } var body: some View { @@ -60,16 +59,12 @@ struct ArticleView: View { } } .task { - let t0 = CFAbsoluteTimeGetCurrent() - print("[ART-OPEN] .task started") - let entryId = entry.id if let idx = vm.entries.firstIndex(where: { $0.id == entryId }), !vm.entries[idx].isRead { vm.entries[idx].status = "read" } currentEntry = vm.entries.first(where: { $0.id == entryId }) ?? currentEntry - print("[ART-OPEN] mark-read done +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms") Task { let api = ReaderAPI() @@ -77,18 +72,12 @@ struct ArticleView: View { vm.counters = try? await api.getCounters() } - let t1 = CFAbsoluteTimeGetCurrent() do { let fullEntry = try await ReaderAPI().getEntry(id: entryId) - let t2 = CFAbsoluteTimeGetCurrent() - print("[ART-OPEN] getEntry returned +\(Int((t2-t0)*1000))ms contentLen=\(fullEntry.articleHTML.count)") - let fullHTML = fullEntry.articleHTML + if !fullHTML.isEmpty && fullHTML.count > articleContent.count { articleContent = fullHTML - print("[ART-OPEN] articleContent updated +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms") - } else { - print("[ART-OPEN] content NOT upgraded (existing=\(articleContent.count) new=\(fullHTML.count))") } var merged = fullEntry @@ -97,10 +86,7 @@ struct ArticleView: View { merged.starred = local.starred } currentEntry = merged - } catch { - print("[ART-OPEN] getEntry FAILED: \(error)") - } - print("[ART-OPEN] .task complete +\(Int((CFAbsoluteTimeGetCurrent()-t0)*1000))ms") + } catch {} } } diff --git a/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift b/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift index 99666b7..79da55b 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ArticleWebView.swift @@ -10,34 +10,17 @@ final class ArticleRenderer { let webView: WKWebView private init() { - let t0 = CFAbsoluteTimeGetCurrent() - print("[WARMUP] ArticleRenderer.init started") - let config = WKWebViewConfiguration() config.allowsInlineMediaPlayback = true - // 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) + // Create at screen size so WebKit initializes the GPU compositor + // during warmup, not on first article open. + webView = WKWebView(frame: UIScreen.main.bounds, configuration: config) webView.isOpaque = false webView.backgroundColor = .clear webView.scrollView.isScrollEnabled = true - // 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 + // Load realistic content to warm CSS, fonts, layout, image decoder webView.loadHTMLString(ArticleHTMLBuilder.build( title: "Warming up", url: nil, @@ -55,31 +38,23 @@ final class ArticleRenderer { """ ), 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") - } - } + /// Attach WKWebView to the app window (invisible) to force GPU process + /// launch. Must be called after window is available. Keeps the webview + /// attached — it will be moved to the article container on first use. + func attachToWindow() { + guard webView.window == nil, + let window = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }).first?.windows.first + else { return } + + webView.alpha = 0 + window.addSubview(webView) + + // Keep attached (don't remove) so GPU process stays alive. + // The webview will be reparented to the article container + // via removeFromSuperview + addSubview in makeUIView. } } @@ -89,7 +64,6 @@ struct ArticleWebView: UIViewRepresentable { let html: String func makeUIView(context: Context) -> UIView { - print("[ART-WV] makeUIView") let container = UIView() container.backgroundColor = .clear @@ -115,8 +89,6 @@ struct ArticleWebView: UIViewRepresentable { let isUpgrade = context.coordinator.lastHTML != nil context.coordinator.lastHTML = newHTML - print("[ART-WV] updateUIView isUpgrade=\(isUpgrade) htmlLen=\(newHTML.count)") - if isUpgrade { // Content upgrade (partial → full): swap only #article-body contents. // Header + outer document structure stay intact. Scroll preserved. @@ -178,7 +150,6 @@ struct ArticleWebView: UIViewRepresentable { class Coordinator: NSObject, WKNavigationDelegate { var lastHTML: String? - var loadStart: CFAbsoluteTime = 0 func webView( _ webView: WKWebView, @@ -193,15 +164,5 @@ struct ArticleWebView: UIViewRepresentable { } decisionHandler(.allow) } - - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { - loadStart = CFAbsoluteTimeGetCurrent() - print("[ART-WV] didStartNavigation") - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - let elapsed = Int((CFAbsoluteTimeGetCurrent() - loadStart) * 1000) - print("[ART-WV] didFinish +\(elapsed)ms") - } } }