FIX: Optimize time to load paywalls#6694
Conversation
| let shouldActivateCarouselImage = self.isWithinImagePreloadWindow | ||
| let effectiveSize = self.size ?? self.viewModel.cachedMeasuredSize | ||
| Group { | ||
| if !shouldActivateCarouselImage { |
There was a problem hiding this comment.
Now, I'll be honest… I don't love that the image is aware of where it may be rendered, but this improved the redraw cycle by a notable amount. I can spend time considering how to clean up the responsibilities so the image view has no knowledge of the carousel state, but that can be a clean up pr IMO
There was a problem hiding this comment.
Maybe there's a way we can inject that into each ImageComponentView when the outside state changes, so that the image doesn't need to be aware of this?
Also I'm wondering if there's a way to tighten up the size ?? viewModel.cachedMeasuredSize dance somehow, since we're doing a variation of this in multiple places 🤔
There was a problem hiding this comment.
I've addressed the first part of your comment, but the second I left alone. Because: Functions make it harder for SwiftUI to intelligently compare and redraw at the right time so I'm reluctant to DRY this up when it's only 2 spots
Replace the ForceSizeCalculation environment key with RequestSizeCalculation and update usages. Deleted ForceSizeCalculation.swift and added RequestSizeCalculation.swift. Updated CarouselComponentView and ImageComponentView to use .requestSizeCalculation; refactored ImageComponentView to compute shouldForceSizeCalculation and reorganized the view/ZStack and RemoteImage render flow to rely on the new environment value. No behavioral change intended beyond the env key rename and minor view cleanup.
Generated by 🚫 Danger |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8801c93. Configure here.
Replace the @State PaywallPromoOfferCache with a @StateObject PromoOfferCacheOwner wrapper to hold the PaywallPromoOfferCache instance. Update initializers and usages in PresentingPaywallModifier and PresentingPaywallBindingModifier to reference promoOfferCacheOwner.cache. Add PromoOfferCacheOwner (@mainactor, availability annotations) so the promo offer cache is initialized once without published properties, preventing unnecessary paywall redraws.
4 builds increased size
RevenueCat 1.0 (1)
|
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 22.0 kB |
| RevenueCatUI.FileImageLoader.FileImageLoader | ⬆️ 1.8 kB |
| Code Signature | ⬆️ 1.2 kB |
| DYLD.Exports | ⬆️ 1.0 kB |
| 📝 RevenueCatUI.DecodedImageCache.DecodedImageCache | ⬆️ 876 B |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.local-source
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 10.6 kB (0.09%)
Total download size change: ⬆️ 2.3 kB (0.06%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 1.9 kB |
| 📝 RevenueCatUI.DecodedImageCache.imageAndSize(for) | ⬆️ 836 B |
| 📝 RevenueCatUI.DecodedImageCache.Objc Metadata | ⬆️ 791 B |
| RevenueCatUI.CarouselView.body | ⬆️ 760 B |
| Code Signature | ⬆️ 520 B |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.cocoapods
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 19.1 kB (0.07%)
Total download size change: ⬆️ 5.8 kB (0.1%)
Largest size changes
| Item | Install Size Change |
|---|---|
| DYLD.String Table | ⬆️ 7.3 kB |
| 📝 RevenueCatUI.DecodedImageCache.imageAndSize(for) | ⬆️ 836 B |
| 📝 RevenueCatUI.DecodedImageCache.Objc Metadata | ⬆️ 784 B |
| RevenueCatUI.CarouselView.body | ⬆️ 760 B |
| Code Signature | ⬆️ 728 B |
BinarySizeTest 1.0 (1)
com.revenuecat.binary-size-test.spm
⚖️ Compare build
📦 Install build
⏱️ Analyze build performance
Total install size change: ⬆️ 6.0 kB (0.06%)
Total download size change: ⬆️ 2.1 kB (0.05%)
Largest size changes
| Item | Install Size Change |
|---|---|
| 📝 RevenueCatUI.DecodedImageCache.imageAndSize(for) | ⬆️ 836 B |
| 📝 RevenueCatUI.DecodedImageCache.Objc Metadata | ⬆️ 771 B |
| RevenueCatUI.CarouselView.body | ⬆️ 760 B |
| Other | ⬆️ 3.7 kB |
🛸 Powered by Emerge Tools
5c9d66b to
3a970d5
Compare
The file was added in #6694 but not registered in the hand-maintained RevenueCat.xcodeproj used by the XCFramework build, breaking release-checks. Made-with: Cursor
The Dangerfile already detects Swift files that are added (or removed) on disk but not registered in the hand-maintained `RevenueCat.xcodeproj`. Until now, that mismatch was reported as a warning, so PRs could merge in a state that breaks the xcframework build (see #6713/#6694). Promote the existing `warn` to `fail` so the PR's required Danger check blocks merge until the project file is in sync. Made-with: Cursor





Checklist
purchases-androidand hybridsMotivation
Redraws were happening too frequently, causing UI hangs in some environments. Given a paywall with 27 images, from a warmed cache, I was experiencing a 711ms hang on paywall launch. These changes reduce that number to 112ms
Description
Several things are at play here:
promo offer eligibility was causing tree redraws for 2 reasons. The state object was the wrong choice since observation wasn't necessary in the view modifier, and we were passing the values into the function directly—I think causing swiftUI to fail to know when best to redraw.
The image draw cycle was reinvoking for various reasons, some of which were carousel issues because it kept trying to recalculate size when the image was just off the screen
Note
Medium Risk
Touches core paywall rendering and presentation paths (image loading, carousel sizing, and paywall modifiers), so regressions could show up as missing/incorrectly-sized images or stale promo-offer state despite being primarily performance-focused.
Overview
Reduces Paywalls V2 launch/render overhead by caching expensive work and avoiding unnecessary SwiftUI invalidations.
Adds a process-wide
DecodedImageCache(NSCache) so file-backed images aren’t repeatedly decoded when views are recreated, and updatesURL.asImageAndSize/FileImageLoaderto use it.Optimizes component rendering by memoizing measured image sizes in
ImageComponentViewModel, adding an environment flag (requestSizeCalculation) to skip size-measurement work for carousel pages that are off-screen, and refactoring eligibility/promo-offer lookups to stable locals to reduce rebuild churn.Updates
presentPaywallmodifiers to holdPaywallPromoOfferCacheinside a non-publishingPromoOfferCacheOwner, preventing paywall-wide redraws caused by cache observation while still reusing a single cache instance.Reviewed by Cursor Bugbot for commit 3a970d5. Bugbot is set up for automated code reviews on this repo. Configure here.