From da8444640c1d95503078ad89aee3b28cc9c1ef0a Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 26 Jan 2026 15:40:12 -0500 Subject: [PATCH 1/6] perf: Disable GutenbergKit preloading if GutenbergKit is disabled The preloading is unnecessary and also lead to confusing cache states. The invalid cache states were addressed in a separate commit. --- .../Blog Dashboard/ViewModel/BlogDashboardViewModel.swift | 8 ++++++-- .../Post/Controllers/PostListViewController.swift | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift index 4a4136cbef63..51a7841091bf 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift @@ -128,7 +128,9 @@ final class BlogDashboardViewModel { } func viewWillAppear() { - EditorDependencyManager.shared.prefetchDependencies(for: self.blog) + if RemoteFeatureFlag.newGutenberg.enabled() { + EditorDependencyManager.shared.prefetchDependencies(for: self.blog) + } quickActionsViewModel.viewWillAppear() } @@ -146,7 +148,9 @@ final class BlogDashboardViewModel { self.loadCardsFromCache() self.loadCards() - EditorDependencyManager.shared.prefetchDependencies(for: blog) + if RemoteFeatureFlag.newGutenberg.enabled() { + EditorDependencyManager.shared.prefetchDependencies(for: blog) + } } func clearEditorCache(_ completion: @escaping () -> Void) { diff --git a/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift b/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift index 5943da8b72fa..678d455f0363 100644 --- a/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift @@ -40,7 +40,7 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP self?.handleRefreshNoResultsViewController($0) } - if let blog = self.blog { + if let blog = self.blog, RemoteFeatureFlag.newGutenberg.enabled() { EditorDependencyManager.shared.prefetchDependencies(for: blog) } } From 49911714013b006db113f221a9c3bc7c62fd16ba Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 26 Jan 2026 16:04:32 -0500 Subject: [PATCH 2/6] perf: Consolidate duplicative editor warmups Warming and preloading the editor in both the MySiteViewController and BlogDashboardViewModel is redundant. The former embeds the latter. --- .../ViewModel/BlogDashboardViewModel.swift | 35 +++++++++++++++---- .../Blog/My Site/MySiteViewController.swift | 27 -------------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift index 51a7841091bf..0fbe7e86979a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import CoreData +import GutenbergKit import WordPressData import WordPressKit import WordPressCore @@ -30,6 +31,9 @@ final class BlogDashboardViewModel { private var blog: Blog + /// Tracks the last blog for which the editor was warmed up to avoid redundant warmups. + private static var lastWarmedUpBlogID: NSManagedObjectID? + private var error: Error? private let wordpressClient: WordPressClient? @@ -128,9 +132,7 @@ final class BlogDashboardViewModel { } func viewWillAppear() { - if RemoteFeatureFlag.newGutenberg.enabled() { - EditorDependencyManager.shared.prefetchDependencies(for: self.blog) - } + warmUpEditorIfNeeded(for: self.blog) quickActionsViewModel.viewWillAppear() } @@ -148,9 +150,7 @@ final class BlogDashboardViewModel { self.loadCardsFromCache() self.loadCards() - if RemoteFeatureFlag.newGutenberg.enabled() { - EditorDependencyManager.shared.prefetchDependencies(for: blog) - } + warmUpEditorIfNeeded(for: blog) } func clearEditorCache(_ completion: @escaping () -> Void) { @@ -189,6 +189,29 @@ final class BlogDashboardViewModel { private extension BlogDashboardViewModel { + /// Warms up the editor for the given blog if it hasn't been warmed up already. + /// This avoids duplicative warmups when the site hasn't changed. + func warmUpEditorIfNeeded(for blog: Blog) { + guard RemoteFeatureFlag.newGutenberg.enabled() else { + return + } + + guard blog.objectID != Self.lastWarmedUpBlogID else { + // Editor already warmed up for this blog + return + } + + Self.lastWarmedUpBlogID = blog.objectID + + let configuration = EditorConfiguration(blog: blog) + + // WebKit warmup - pre-compile HTML/JS (shaves ~100-200ms) + GutenbergKit.EditorViewController.warmup(configuration: configuration) + + // Data prefetch - pre-fetch settings, assets, preload list + EditorDependencyManager.shared.prefetchDependencies(for: blog) + } + func registerNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(showDraftsCardIfNeeded), name: .newPostCreated, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(showScheduledCardIfNeeded), name: .newPostScheduled, object: nil) diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift index 28c049b09160..0e0d32f74567 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift @@ -33,7 +33,6 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite } private var currentSection: Section = .dashboard - private static var lastWarmedUpBlogID: NSManagedObjectID? @objc private(set) lazy var scrollView: UIScrollView = { @@ -341,29 +340,6 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite configureNavBarAppearance(animated: true) } - // MARK: - Editor Warmup - - /// Warms up the editor for the given blog if it hasn't been warmed up already. - /// This avoids duplicative warmups when the site hasn't changed. - private func warmUpEditorIfNeeded(for blog: Blog) { - guard blog.objectID != Self.lastWarmedUpBlogID else { - // Editor already warmed up for this blog - return - } - - Self.lastWarmedUpBlogID = blog.objectID - - let configuration = EditorConfiguration(blog: blog) - - // 1. WebKit warmup - pre-compile HTML/JS (shaves ~100-200ms) - GutenbergKit.EditorViewController.warmup(configuration: configuration) - - // 2. Data prefetch - pre-fetch settings, assets, preload list via EditorDependencyManager - Task { - await EditorDependencyManager.shared.prefetchDependencies(for: blog) - } - } - // MARK: - Main Blog /// This VC is prepared to either show the details for a blog, or show a no-results VC configured to let the user know they have no blogs. @@ -403,9 +379,6 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite showDashboard(for: blog) } - if RemoteFeatureFlag.newGutenberg.enabled() { - warmUpEditorIfNeeded(for: blog) - } } @objc From bda16ecf88b88e72556c6f3988f770d0d458f60b Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 26 Jan 2026 16:53:56 -0500 Subject: [PATCH 3/6] perf: Remove PostListViewController editor preloading This is arguably duplicative of the preloading within BlogDashboardViewModel. --- .../ViewRelated/Post/Controllers/PostListViewController.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift b/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift index 678d455f0363..e06b9e2f102a 100644 --- a/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Controllers/PostListViewController.swift @@ -39,10 +39,6 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP refreshNoResultsViewController = { [weak self] in self?.handleRefreshNoResultsViewController($0) } - - if let blog = self.blog, RemoteFeatureFlag.newGutenberg.enabled() { - EditorDependencyManager.shared.prefetchDependencies(for: blog) - } } private lazy var createButtonCoordinator: CreateButtonCoordinator = { From d250ed71932cd8a837962739607d914866e2d311 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 26 Jan 2026 16:58:13 -0500 Subject: [PATCH 4/6] fix: Invalidate editor asset bundle when toggling plugin support When preloading GutenbergKit while plugins are disabled, an empty editor bundle is cached. The editor continues using this this bundle until it expires or the pull-to-refresh gesture is performed on the My Site view. If the plugins feature is later enabled, the empty bundle is used and leads to confusing plugin loading failed warnings. To avoid unexpectedly empty bundles, we invalidate all bundles when the plugins feature flag changes. --- .../Editor/EditorDependencyManager.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift b/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift index 21b3c2616814..9734b3bc0f18 100644 --- a/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift +++ b/WordPress/Classes/Utility/Editor/EditorDependencyManager.swift @@ -29,6 +29,11 @@ final class EditorDependencyManager: Sendable { /// Cached dependencies keyed by blog's ObjectID string representation. private let cache = LockingHashMap() + /// Tracks the `newGutenbergPlugins` flag value at the time the cache was last populated. + /// Used to detect when the flag changes and invalidate all stale entries. + private let pluginsFlagLock = NSLock() + private var _lastPluginsFlagValue: Bool? + /// Currently running prefetch tasks, keyed by blog's ObjectID string. private let prefetchTasks = LockingTaskHashMap() @@ -83,6 +88,16 @@ final class EditorDependencyManager: Sendable { return nil } + // Check if the plugins flag changed since we last cached + let currentPluginsFlagValue = RemoteFeatureFlag.newGutenbergPlugins.enabled() + let lastFlagValue = pluginsFlagLock.withLock { _lastPluginsFlagValue } + if let lastFlagValue, lastFlagValue != currentPluginsFlagValue { + // Flag changed - invalidate all cached entries + DDLogInfo("EditorDependencyManager: Plugins flag changed (\(lastFlagValue) -> \(currentPluginsFlagValue)), invalidating all cached dependencies") + cache.removeAll() + prefetchTasks.removeAll() + } + // Don't prefetch if we already have cached dependencies if cache[key] != nil { return nil @@ -95,6 +110,7 @@ final class EditorDependencyManager: Sendable { do { let dependencies = try await service.prepare() self.cache[key] = dependencies + self.pluginsFlagLock.withLock { self._lastPluginsFlagValue = currentPluginsFlagValue } } catch { // Prefetch failed - editor will fall back to async loading DDLogError("EditorDependencyManager: Failed to prefetch dependencies: \(error)") @@ -145,6 +161,7 @@ final class EditorDependencyManager: Sendable { /// Clears all cached dependencies. func invalidateAll() { cache.removeAll() + pluginsFlagLock.withLock { _lastPluginsFlagValue = nil } prefetchTasks.removeAll() // No need to use `removeAll` for the `invalidationTasks` } From a98a5c5df80d4163f169f22d8245ee5a2b6d7378 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 26 Jan 2026 17:17:14 -0500 Subject: [PATCH 5/6] fix: Always prefetch editor dependencies GutenbergKit's dependency prefetch manages its own cache and avoids re-fetching dependencies. --- .../ViewModel/BlogDashboardViewModel.swift | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift index 0fbe7e86979a..00b64cfb7bea 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/ViewModel/BlogDashboardViewModel.swift @@ -189,26 +189,27 @@ final class BlogDashboardViewModel { private extension BlogDashboardViewModel { - /// Warms up the editor for the given blog if it hasn't been warmed up already. - /// This avoids duplicative warmups when the site hasn't changed. + /// Warms up the editor for the given blog. + /// + /// This performs two operations: + /// 1. WebKit warmup (once per blog) - pre-compiles HTML/JS + /// 2. Data prefetch (always called) - fetches settings, assets, preload list + /// + /// The prefetch is always called because `EditorDependencyManager` handles its own + /// caching and needs to detect when the plugins feature flag changes. func warmUpEditorIfNeeded(for blog: Blog) { guard RemoteFeatureFlag.newGutenberg.enabled() else { return } - guard blog.objectID != Self.lastWarmedUpBlogID else { - // Editor already warmed up for this blog - return + // WebKit warmup - only needed once per blog (shaves ~100-200ms) + if blog.objectID != Self.lastWarmedUpBlogID { + Self.lastWarmedUpBlogID = blog.objectID + let configuration = EditorConfiguration(blog: blog) + GutenbergKit.EditorViewController.warmup(configuration: configuration) } - Self.lastWarmedUpBlogID = blog.objectID - - let configuration = EditorConfiguration(blog: blog) - - // WebKit warmup - pre-compile HTML/JS (shaves ~100-200ms) - GutenbergKit.EditorViewController.warmup(configuration: configuration) - - // Data prefetch - pre-fetch settings, assets, preload list + // Data prefetch - always call to allow EditorDependencyManager to detect flag changes EditorDependencyManager.shared.prefetchDependencies(for: blog) } From 17a82622ecfc18b5b41c36980ea6abb64652ee39 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 26 Jan 2026 17:58:04 -0500 Subject: [PATCH 6/6] build: Update GutenbergKit version --- Modules/Package.resolved | 5 ++--- Modules/Package.swift | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Modules/Package.resolved b/Modules/Package.resolved index 4e572da38b64..378107f66551 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "024ca0929c05dc22af0ce33abb347be2db737ddaa09348ee3a09c1181b56a628", + "originHash" : "dc6a09055cd83c98ea9491f1049daa2890dfd7ada78f143c12a9e2274d443cdb", "pins" : [ { "identity" : "alamofire", @@ -149,8 +149,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "8addad7fd018985dd3f8b15cfcc0d028cdc189b3", - "version" : "0.13.0" + "revision" : "6c3a438dc9a6e5dbe118810d9b98052086261a0b" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index 938b41a641f6..38cd859e50c0 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -56,7 +56,7 @@ let package = Package( .package(url: "https://github.com/wordpress-mobile/wpxmlrpc", from: "0.9.0"), .package(url: "https://github.com/wordpress-mobile/NSURL-IDN", revision: "b34794c9a3f32312e1593d4a3d120572afa0d010"), .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), - .package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.13.0"), + .package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "6c3a438dc9a6e5dbe118810d9b98052086261a0b"), // We can't use wordpress-rs branches nor commits here. Only tags work. .package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20260114"), .package(