Slower incremental Emit Swift module in Tuist-generated project vs legacy Xcode project

Hi all,
I’m seeing a reproducible incremental build slowdown after moving to a Tuist-generated project, and I’m trying to understand whether this is expected from project shape/
settings.

Observed behavior

When I modify a single Swift file and do an incremental rebuild:

Legacy non generated .xcodeproj: The Emit Swift module build phase takes ~4s

Tuist-generated .xcodeproj: Emit Swift module takes ~14s:

So the main regression appears in Emit Swift module, not in linking or unrelated phases.

Setup

  • Architecture: non-modularized - single main app target
  • Size: 600 Swift files, 56k LOC
  • I did cache all external dependencies Caching: tuist cache --external-only
  • tuist version 4.145.0 / macos 26.1

Dependencies

I have around 15 external dependencies .. nothing unusual

  • AppAuth
  • AppAuthCore
  • Collections
  • ComposableArchitecture
  • EventSource
  • FirebaseAnalytics
  • FirebaseCrashlytics
  • FirebaseMessaging
  • FirebasePerformance
  • GoogleSignIn
  • GoogleSignInSwift
  • Intercom
  • Mixpanel
  • StytchCore

Context I noticed in build settings

In the Tuist-generated project I see much heavier Swift compile inputs, including:

  • Large OTHER_SWIFT_FLAGS with many -Xcc -fmodule-map-file=…
  • Multiple -load-plugin-executable entries (macro plugins)

Question

Is this expected for a non-modularized medium-sized app with many external dependencies, even with tuist cache --external-only? Or did I mess something up in my Tuist project settings

If yes, what are the most effective knobs to reduce incremental Emit Swift module time in this setup?

The goal is of course to move to a modularized structure but that will take some time and until then the developer experience might be quite affected by this.

I’m aware that I don’t provide exact steps or project to reproduce but it’s because I did not manage to really isolate this in a sample project.

Any tips to investigate this furter?

thanks in advance!

Jan

Hey @JanC.

The slowdown you’re seeing is expected given your setup (non-modularized, 600 files, Firebase + ComposableArchitecture). Here’s why.

What’s happening

Tuist makes dependency resolution explicit by adding compiler flags to -OTHER_SWIFT_FLAGS:

  • -Xcc -fmodule-map-file=... for every C/ObjC dependency (and their transitive deps). Firebase alone pulls in dozens of sub-dependencies, each with its own module map.
  • -load-plugin-executable for Swift macro plugins (ComposableArchitecture uses macros heavily).

In a hand-crafted .xcodeproj, dependencies are typically resolved through framework search paths and implicit module discovery, which Xcode can optimize more aggressively. Tuist makes these explicit for correctness and reproducibility, but the trade-off is a heavier compiler invocation.

Every time the Swift compiler runs “Emit Swift module” (even incrementally), it must parse and validate every one of those flags, resolve module map paths, and load plugin executables. With a single 600-file target, every compilation carries the full set of flags. That’s the worst case.

What you can do now

  1. Inspect your flag count. Check the "Emit Swift module" compiler invocation in the build log, or run xcodebuild -showBuildSettings | grep OTHER_SWIFT_FLAGS. If you see duplicate -fmodule-map-file entries, that’s a bug worth reporting.
  2. Audit transitive dependencies. Firebase pulls in a huge transitive tree. Each sub-dependency with a module map adds flags. Check whether you actually need all Firebase components.

Medium-term

  1. Modularize progressively. Even extracting a few leaf modules would help significantly, since each module only carries flags for its own dependencies rather than the full set.

The ~3.5x slowdown (4s to 14s) is plausible for a non-modularized target of this size with heavy dependencies like Firebase and ComposableArchitecture. It’s not a misconfiguration, it’s an inherent cost of explicit dependency resolution that becomes pronounced in non-modularized projects. Modularizing is the most effective long-term fix.

1 Like

Many thanks Pedro for the detailed answer.

The list of fmodule-map-file and -load-plugin-executable matches the dependencies and there are no dupplicated (see below)

The plan is indeed to modularize the app rurther (and asap) so I think we’ll have to live with that until then :slight_smile:

Many thanks

cheers

-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/checkouts/firebase-ios-sdk/CoreOnly/Sources/module.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/checkouts/promises/Sources/FBLPromises/include/module.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/checkouts/swift-syntax/Sources/_SwiftSyntaxCShims/include/module.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/AppAuth/AppAuth.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/AppAuthCore/AppAuthCore.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/AppCheckCore/AppCheckCore.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/FirebaseAnalyticsTarget/FirebaseAnalyticsTarget.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/FirebaseAnalyticsWrapper/FirebaseAnalyticsWrapper.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/FirebaseCore/FirebaseCore.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/FirebaseCoreExtension/FirebaseCoreExtension.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/FirebaseCrashlytics/FirebaseCrashlytics.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/FirebaseInstallations/FirebaseInstallations.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/FirebaseMessaging/FirebaseMessaging.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/FirebaseSessionsObjC/FirebaseSessionsObjC.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GTMSessionFetcherCore/GTMSessionFetcherCore.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleAdsOnDeviceConversionTarget/GoogleAdsOnDeviceConversionTarget.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleAppMeasurementTarget/GoogleAppMeasurementTarget.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleDataTransport/GoogleDataTransport.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleSignIn/GoogleSignIn.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleUtilities_AppDelegateSwizzler/GoogleUtilities_AppDelegateSwizzler.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleUtilities_Environment/GoogleUtilities_Environment.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleUtilities_Logger/GoogleUtilities_Logger.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleUtilities_MethodSwizzler/GoogleUtilities_MethodSwizzler.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleUtilities_NSData/GoogleUtilities_NSData.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleUtilities_Network/GoogleUtilities_Network.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleUtilities_Reachability/GoogleUtilities_Reachability.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/GoogleUtilities_UserDefaults/GoogleUtilities_UserDefaults.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/RecaptchaEnterpriseWrapper/RecaptchaEnterpriseWrapper.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/RecaptchaInterop/RecaptchaInterop.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/UIKitNavigationShim/UIKitNavigationShim.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/nanopb/nanopb.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/stytch_ios_dfp/stytch_ios_dfp.modulemap -Xcc 
-fmodule-map-file=/Users/jan/myapp/MainApp/../Tuist/.build/tuist-derived/third_party_IsAppEncrypted/third_party_IsAppEncrypted.modulemap 
-load-plugin-executable /Users/jan/.cache/tuist/Binaries/22af13a0f72027799450560f6a7141ea/CasePathsMacros.macro#CasePathsMacros 
-load-plugin-executable /Users/jan/.cache/tuist/Binaries/62c75a2b6888451702bcb42d7f62c957/PerceptionMacros.macro#PerceptionMacros 
-load-plugin-executable /Users/jan/.cache/tuist/Binaries/66904d4ef00a1154525caefbc0eed0d1/ComposableArchitectureMacros.macro#ComposableArchitectureMacros 
-load-plugin-executable /Users/jan/.cache/tuist/Binaries/cfaaf8c66929b3edf4ed142c5bd3906f/DependenciesMacrosPlugin.macro#DependenciesMacrosPlugin