A Decade of Facebook for iOS: How Scale Reshaped an App

Facebook for iOS (FBiOS) is the oldest mobile codebase at Meta. Since its native rewrite in 2012, it has been shaped by thousands of engineers and shipped to billions of users, while still supporting hundreds of engineers iterating on it at once. The result is a codebase that looks nothing like a typical iOS project: a mix of C++, Objective-C(++), and Swift; dozens of dynamically loaded libraries (dylibs); nearly zero raw Apple SDK usage; and heavy reliance on code generation via the Buck build system. Without Buck's heavy caching, a full build would consume an entire workday.

This architecture wasn't a deliberate design from day one—it emerged from a decade of evolution driven by the need to support a growing engineering team, maintain stability, and preserve the user experience. Here’s a look at the pivotal decisions along the way.

2014: Building a New UI Foundation

Two years after the native rewrite, News Feed's codebase started showing reliability cracks. Its data models were backed by Core Data, whose mutable objects clashed with News Feed's multithreaded architecture. Combined with Apple's Model-View-Controller pattern, the bidirectional data flow produced nondeterministic code that was hard to debug and reproduce.

Looking for a fix, one engineer studied React, then rising in the JavaScript community. React's declarative design and one-way data flow simplified the imperative logic causing issues on the web. That model seemed perfectly suited for News Feed—but there was a catch: Apple's SDK had no declarative UI. Swift wouldn't be announced for months, and SwiftUI was years away.

So the team built its own. After a few months of work and migrating News Feed onto the new declarative UI and data model, FBiOS saw a 50 percent performance improvement. The new framework was open-sourced as ComponentKit. It remains the default for native UI in Facebook, delivering gains through view reuse, view flattening, and background layout computation. ComponentKit also inspired Android's Litho and later SwiftUI.

Trading Apple's standard tools for custom infrastructure was a deliberate trade-off: a better user experience justified the cost of having new engineers learn proprietary patterns instead of leaning on industry-standard knowledge.

2015: The Feature Explosion

By 2015, Meta had committed fully to its “Mobile First” strategy, and FBiOS's daily contributor count surged. As more products were integrated, launch time degraded—by year's end, startup was so slow (nearly 30 seconds) that the OS threatened to kill the app.

Several factors drove the slowdown, with two shaping the architecture long-term:

  • Unbounded pre-main growth: The app's size grew with every feature, extending the time before main() even executed.
  • Unrestricted module access: Each product could pull any app resource at startup, leading to a tragedy-of-the-commons where everyone hogged launch time for snappy navigation later.

Fixing this would fundamentally change how product engineers wrote code.

2016: Dylibs and the Modularity Shift

Pre-main overhead was only a fraction of the 30-second launch, but it grew without bound. To stop that growth, engineers moved large swaths of product code into lazily loaded dylibs—code that didn't need to load before main().

The initial structure was simple:

Facebook iOS

Two product dylibs (FBCamera, NotOnStartup) housed feature code, with a third (FBShared) for common code between the dylibs and the main app binary. The fix worked beautifully, curbing startup's unbounded growth and letting most code drift into dylibs as new products came and went without impact on launch speed.

But dylibs disrupted assumptions baked into core systems. Runtime APIs like NSClassFromString() now risked failures if a class lived in an unloaded dylib. Many foundational abstractions relied on enumerating all loaded classes, so they had to be rethought. New linker errors also appeared—if startup code referenced a class inside a dylib, engineers hit errors like:

Undefined symbols for architecture arm64:
  "_OBJC_CLASS_$_SomeClass", referenced from:
      objc-class-ref in libFBSomeLibrary-9032370.a(FBSomeFile.mm.o)

The fix required wrapping calls in a helper that loads a dylib on demand:

Suddenly:

int main() {
  DoSomething(context);
}

Became:

int main() {
  FBCallFunctionInDylib(
    NotOnStatupFramework,
    DoSomething,
    context
  );
}

It worked but left code smells: app-specific dylib enums were hard-coded across call sites, and using the wrong enum failed only at runtime. The only safeguard against startup regressions was a runtime-based system, leading to delayed releases from last-minute issues. Despite the roughness, the change marked a major architectural inflection point that would drive the next few years of rework.

2017: Making Architecture Static

With dylibs in place, two gaps had to close: the module registration system could no longer rely on runtime discovery, and engineers needed build-time certainty that no startup codepath would trigger a dylib load. The answer came from Buck, Meta's open source build system.

Buck declares each target (app, dylib, library) with explicit configuration:

apple_binary(
  name = "Facebook",
  ...
  deps = [
    ":NotOnStartup#shared",
    ":FBCamera#shared",
  ],
)

apple_library(
  name = "NotOnStartup",
  srcs = [
    "SomeFile.mm",
  ],
  labels = ["special_label"],
  deps = [
    ":PokesModule",
    ...
  ],
)

Every target lists its dependencies, compiler flags, sources, and more. When buck build runs, Buck assembles this into a queryable graph:

$ buck query “deps(:Facebook)”
> :NotOnStartup
> :FBCamera

$ buck query “attrfilter(labels, special_label, deps(:Facebook))”
> :NotOnStartup

Leveraging that graph, FBiOS generated queries that produced a holistic view of all classes and functions at build time. That static, build-time insight become the foundation for the app's next-generation architecture—turning runtime guessing games into compile-time guarantees.

Generated abstractions and the plugin era

With Buck able to query the dependency graph, FBiOS could build a live mapping of functions and classes to their containing dylibs. That mapping, generated on the fly during each build, became the input for code generation that hid the dylib enum from call sites:

static std::unordered_map<const char *, Dylib> functionToDylib {{
  { "DoSomething", Dylib.NotOnStartup },
  { "FBSomeClass", Dylib.SomeOtherOne },
  ...
}};

Code generation had two major advantages. First, because the code was regenerated from local input, there was nothing to check into source control — and therefore no merge conflicts. With an engineering body that could double every year, that was a substantial efficiency win. Second, the call helper no longer needed an app-specific dylib (and was renamed FBCallFunction); it simply read from a static mapping produced for each application at build time.

{
  "functions": {
    "DoSomething": Dylib.NotOnStartup,
    ...
  },
  "classes": {
    "FBSomeClass": Dylib.SomeOtherOne
  }
}

The combination of Buck queries and code generation proved sturdy enough to become the basis of a new plugin system that eventually replaced the runtime-based app-module system.

Failures move from runtime to build time

Migrating infrastructure to the Buck-powered plugin system let FBiOS convert many runtime failures into build-time warnings. When the app is built, Buck can render a graph showing where every plugin lives in the application. From that graph, the plugin system can surface errors such as:

  • “Plugin D, E could trigger a load of a dylib. This is not allowed, since the caller of these plugins lives in the app’s startup path.”
  • “There is no plugin for rendering Profiles found in the app … this means that navigating to that screen will not work.”
  • “There are two plugins for rendering Groups (Plugin A, Plugin B). One of them should be removed.”

Facebook iOS

Under the old app-module system these would surface as lazy runtime assertions. Now a successful build gives engineers confidence that the app will not fail from missing functionality, from dylibs loading during startup, or from invariants violated in the module runtime.

The price of generated infrastructure

The plugin system improved reliability, gave engineers faster feedback, and made it trivial to share code across Meta’s other mobile apps — but it did not come free:

  • Plugin errors are not documented on Stack Overflow and can be confusing to debug.
  • A plugin system built on Buck and generated code is far from conventional iOS development.
  • Plugins add an indirection layer; where most apps keep a registry file listing features, FBiOS generates that list, making it surprisingly hard to locate.

The trade-off appears worthwhile. Engineers can change code that is shared across many apps at Meta and be confident that, as long as the plugin system is satisfied, no app will crash on a rarely exercised code path. Teams like News Feed and Groups can expose an extension point for plugins, and product teams can integrate without touching core code.

Swift forces a new language strategy

Not all architectural pressure came from scale. By 2020, Apple’s SDK was increasingly Swift-only, and sentiment inside FBiOS was shifting toward adopting more Swift. The time had come to reconcile with Swift’s presence in the codebase.

FBiOS had long leaned on C++ to build abstractions, benefiting from C++’s zero-overhead principle in code size. But C++ does not interoperate with Swift yet. For most FBiOS APIs — ComponentKit being the notable example — a shim layer would be required for Swift usage, which meant added code bloat.

Facebook iOS

That constraint prompted a language strategy defining where each language should be used:

Facebook iOS

The resulting guidance: product-facing APIs and code should not contain C++, so that Swift and Apple’s future Swift-only APIs could be used freely. Plugins became the mechanism for hiding C++ implementations from most engineers while keeping them in place under the hood.

This marked a shift in how FBiOS engineers approach abstraction. Since 2014, the dominant factors in framework design had been contributions to app size and expressiveness — the reason ComponentKit chose Objective-C++ over Objective-C. Swift’s arrival was the first time developer efficiency trumped both, and the team expects that trend to continue.

Looking back, and forward

From 2014 onward, FBiOS architecture accumulated significant custom infrastructure:

  • In-house abstractions like ComponentKit and GraphQL.
  • Dylibs to keep pre-main times minimal and startup fast.
  • A Buck-powered plugin system abstracting dylibs away from engineers and enabling code sharing between apps.
  • Language guidelines determining where each language is appropriate, with the codebase gradually moving to match.

Meanwhile, Apple’s platform moved in a direction that made some of those custom solutions less necessary:

  • Newer phones are substantially faster, lowering the cost of code loading.
  • OS improvements such as dyld3 and chain fixups make loading faster in software.
  • SwiftUI provides a declarative API sharing many concepts with ComponentKit.
  • Improved SDKs and APIs (like interruptible animations in iOS 8) cover needs that previously required custom frameworks.

As experiences become more widely shared across Facebook, Messenger, Instagram, and WhatsApp, FBiOS is revisiting its optimizations to identify where it can move closer to platform orthodoxy. The lesson so far: the easiest way to share code is either to use what the platform gives you for free, or to build something virtually dependency-free that integrates cleanly across all apps.