How Lottie Animation is Integrated in Telegram-iOS: A Technical Deep Dive

Telegram-iOS integrates Lottie animations through a dual-path architecture that uses LottieComponent and LottieAnimationComponent for UI elements, and LottieMetalAnimatedStickerNode for Metal-accelerated stickers, sharing a common resource pipeline that handles .tgs decompression via TGGUnzipData and caching through AnimationCacheState.

The Telegram Messenger iOS app leverages Lottie vector animations to power everything from animated stickers to loading spinners and transition effects. According to the source code in the TelegramMessenger/Telegram-iOS repository, the implementation splits into two specialized tracks: a ComponentFlow-based UI system for interface animations and a Metal-accelerated rendering pipeline for stickers. Both systems rely on the same underlying resource management, decompression utilities, and caching strategies to deliver smooth 60 fps performance across the supported iOS device range.

Architecture Overview

The Lottie animation integration in Telegram-iOS follows a layered architecture with distinct entry points for different use cases:

  • UI Components: LottieComponent and LottieAnimationComponent wrap Lottie animations into the app's component system, handling bundle resources, remote files, and playback modes
  • Animated Stickers: LottieMetalAnimatedStickerNode provides Metal-accelerated rendering for .tgs sticker files with pre-serialized render tree caching
  • Shared Pipeline: Both paths use TGGUnzipData for .tgs decompression, AnimationCacheState for Metal render caching, and LottieComponent.ResourceContent for media box integration

This separation allows the UI team to add animations declaratively while the stickers team optimizes for battery efficiency through custom Metal shaders.

UI Animation Components

LottieComponent.swift

The LottieComponent class in submodules/TelegramUI/Components/LottieComponent/Sources/LottieComponent.swift serves as the high-level wrapper for adding Lottie animations anywhere in the UI hierarchy. It abstracts file loading, decompression, and playback control through a declarative API.

The component supports multiple content sources via the Content protocol:

  • AppBundleContent: Loads JSON or .tgs files directly from the app bundle
  • ResourceContent: Streams animation data from TelegramMediaFile objects via the media box

Key configuration options include StartingPosition (begin, end, or fractional frame), looping behavior, and color overlays:

public final class LottieComponent: Component {
    public enum StartingPosition: Equatable {
        case begin
        case end
        case fraction(Double)
    }

    public init(
        content: Content,
        color: UIColor? = nil,
        placeholderColor: UIColor? = nil,
        startingPosition: StartingPosition = .end,
        size: CGSize? = nil,
        loop: Bool = false,
        playOnce: ActionSlot<Void>? = nil
    ) { … }
}

LottieAnimationComponent.swift

For finer control within the ComponentFlow system, LottieAnimationComponent in submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift embeds a native Lottie AnimationView with support for tag-based matching and per-element color overrides.

The AnimationItem struct defines playback behavior through its Mode enum:

public struct AnimationItem: Equatable {
    public enum StillPosition { case begin, end }
    public enum Mode: Equatable {
        case still(position: StillPosition)
        case animating(loop: Bool)
        case animateTransitionFromPrevious
    }

    public var name: String
    public var mode: Mode
    public var range: (CGFloat, CGFloat)?
    public var speed: CGFloat = 1.0
    public var waitForCompletion: Bool = true
}

The View.update(component:availableSize:transition:) method handles view reuse logic, loading animations either from bundle paths or by decompressing .tgs data on the fly:

if let url = getAppBundle().url(forResource: component.animation.name, withExtension: "json"),
   let maybeAnimation = Animation.filepath(url.path) {
    animation = maybeAnimation
} else if let url = getAppBundle().url(forResource: component.animation.name,
                                      withExtension: "tgs"),
          let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)),
          let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) {
    animation = try? Animation.from(data: unpackedData, strategy: .codable)
}

Practical Example: Chat Input Typing Indicator

To add a looping typing animation to a chat input field using the component system:

let typingComponent = LottieAnimationComponent(
    animation: .init(name: "input_typing", mode: .animating(loop: true)),
    colors: [:],
    tag: nil,
    size: CGSize(width: 24, height: 24)
)

// Integration in component tree
let typingNode = AnyComponent(typingComponent)

Animated Stickers and Metal Rendering

Metal-Accelerated Sticker Nodes

For animated stickers, Telegram-iOS implements LottieMetalAnimatedStickerNode in submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift. This class uses a custom Metal pipeline (LottieCpp with custom shaders) rather than the standard Lottie iOS library to achieve hardware-accelerated rendering.

The node conforms to the AnimatedStickerNode protocol and handles setup through:

public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode {
    public func setup(
        source: AnimatedStickerNodeSource,
        width: Int,
        height: Int,
        playbackMode: AnimatedStickerPlaybackMode,
        mode: AnimatedStickerMode
    ) {
        // Resolves source to file path
        // Requests cached binary from AnimationCacheState
        // Decodes via LottieAnimationContainer
        // Creates Metal render pipelines
        // Drives frames via display link
    }
}

Resource Loading and Caching

The Metal implementation uses AnimationCacheState to serialize render trees into binary buffers. When cacheLottieMetalAnimation(path:) is called, the system:

  1. Reads the .tgs file and decompresses via TGGUnzipData
  2. Builds a LottieAnimationContainer from the JSON
  3. Serializes each frame's render tree
  4. Queues the task (max 2 concurrent) and writes to disk for subsequent fast loads

This pre-serialization eliminates JSON parsing overhead during playback, enabling smooth 60 fps animation even on older devices.

Fallback Rendering

When Metal is unavailable, the system falls back to AnimatedStickerNode (CPU-based RLottie rendering) located in the AnimatedStickerNode framework submodule. Both implementations share the same resource handling pipeline but differ in their rendering backends.

Loading Animations from the Media Box

For custom sticker packs and remote animations, the repository provides LottieComponent.ResourceContent in submodules/TelegramUI/Components/LottieComponentResourceContent/Sources/LottieComponentResourceContent.swift. This class bridges the gap between Telegram's media storage system and the Lottie rendering pipeline.

The load(_:) method implements a sophisticated loading strategy:

public final class ResourceContent: LottieComponent.Content {
    private let context: AccountContext
    private let file: TelegramMediaFile
    
    override public func load(_ f: @escaping (LottieComponent.ContentData) -> Void) -> Disposable {
        // Attempts synchronous disk read if file exists
        // Subscribes to mediaBox.resourceData for download progress
        // Decompresses via TGGUnzipData upon completion
        // Returns animation data or placeholder
    }
}

Usage example for a sticker from the media box:

let resource = LottieComponent.ResourceContent(
    context: accountContext,
    file: stickerFile,
    attemptSynchronously: true,
    providesPlaceholder: true
)

let stickerComponent = LottieComponent(
    content: resource,
    size: CGSize(width: 80, height: 80),
    loop: false
)

The component automatically displays a placeholder while fetching the animation from Telegram's servers, then transitions to the decompressed Lottie playback once the .tgs data arrives.

Key Implementation Files

File Purpose Location
LottieComponent.swift High-level UI component with bundle and remote loading submodules/TelegramUI/Components/LottieComponent/Sources/
LottieAnimationComponent.swift ComponentFlow wrapper exposing playback modes submodules/Components/LottieAnimationComponent/Sources/
LottieComponentResourceContent.swift Media box integration for TelegramMediaFile submodules/TelegramUI/Components/LottieComponentResourceContent/Sources/
LottieMetalAnimatedStickerNode.swift Metal-accelerated sticker rendering submodules/TelegramUI/Components/LottieMetal/Sources/
AnimatedStickerNode.swift Fallback CPU-based RLottie implementation AnimatedStickerNode submodule
AnimationCacheState Render tree serialization and caching Embedded within LottieMetalAnimatedStickerNode.swift

Summary

  • Telegram-iOS uses a dual-path architecture where LottieComponent and LottieAnimationComponent handle UI animations, while LottieMetalAnimatedStickerNode manages sticker rendering through custom Metal shaders.
  • Both systems share decompression logic via TGGUnzipData for .tgs files and leverage AnimationCacheState to cache pre-serialized render trees for optimal performance.
  • Resource loading adapts to source type: bundle files load synchronously while TelegramMediaFile objects stream through the media box with placeholder support.
  • The Metal implementation prioritizes 60 fps performance by serializing animation frames into binary buffers and limiting concurrent processing to 2 tasks.

Frequently Asked Questions

What is the difference between LottieComponent and LottieAnimationComponent in Telegram-iOS?

LottieComponent provides a higher-level abstraction with built-in support for placeholders, color overlays, and content sources from both app bundles and remote media files. LottieAnimationComponent offers finer-grained control within the ComponentFlow system, exposing direct access to Lottie's AnimationView, tag-based view matching, and specific frame ranges. Use LottieComponent for standard UI animations and LottieAnimationComponent when you need precise playback control or integration with complex component trees.

How does Telegram-iOS handle the .tgs file format for stickers?

The app treats .tgs files as gzipped JSON payloads. When loading a sticker, the code calls TGGUnzipData(data, 5 * 1024 * 1024) to decompress the file (with a 5MB limit), then parses the resulting JSON through Animation.from(data:strategy:) or LottieAnimationContainer depending on whether the Metal or CPU rendering path is active. The decompressed data feeds into the respective animation pipelines for frame generation.

Why does Telegram use Metal for animated stickers instead of standard Lottie?

The LottieMetalAnimatedStickerNode implementation uses custom Metal shaders and the LottieCpp framework to pre-serialize render trees into binary cache files. This approach eliminates JSON parsing during playback and enables GPU-accelerated rendering, achieving consistent 60 fps performance even on older iOS devices. The standard Lottie iOS library runs on the CPU, which consumes more battery and performs poorly with complex vector animations at sticker sizes.

How can I load a Lottie animation from a TelegramMediaFile in my own component?

Import LottieComponentResourceContent and instantiate ResourceContent with your AccountContext and TelegramMediaFile. Pass this as the content parameter when initializing LottieComponent. Set attemptSynchronously: true for instant display if cached, and providesPlaceholder: true to show a temporary view while the file downloads from Telegram's servers. The component handles the rest, including decompression and playback setup.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →